1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.bubbles; 18 19 import static android.view.View.LAYOUT_DIRECTION_RTL; 20 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.content.res.Resources; 24 import android.graphics.Insets; 25 import android.graphics.Point; 26 import android.graphics.PointF; 27 import android.graphics.Rect; 28 import android.graphics.RectF; 29 import android.util.Log; 30 import android.view.Surface; 31 import android.view.WindowInsets; 32 import android.view.WindowManager; 33 import android.view.WindowMetrics; 34 35 import androidx.annotation.VisibleForTesting; 36 37 import com.android.launcher3.icons.IconNormalizer; 38 import com.android.wm.shell.R; 39 40 /** 41 * Keeps track of display size, configuration, and specific bubble sizes. One place for all 42 * placement and positioning calculations to refer to. 43 */ 44 public class BubblePositioner { 45 private static final String TAG = BubbleDebugConfig.TAG_WITH_CLASS_NAME 46 ? "BubblePositioner" 47 : BubbleDebugConfig.TAG_BUBBLES; 48 49 /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/ 50 public static final int NUM_VISIBLE_WHEN_RESTING = 2; 51 /** Indicates a bubble's height should be the maximum available space. **/ 52 public static final int MAX_HEIGHT = -1; 53 /** The max percent of screen width to use for the flyout on large screens. */ 54 public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f; 55 /** The max percent of screen width to use for the flyout on phone. */ 56 public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f; 57 /** The percent of screen width for the expanded view on a large screen. **/ 58 private static final float EXPANDED_VIEW_LARGE_SCREEN_LANDSCAPE_WIDTH_PERCENT = 0.48f; 59 /** The percent of screen width for the expanded view on a large screen. **/ 60 private static final float EXPANDED_VIEW_LARGE_SCREEN_PORTRAIT_WIDTH_PERCENT = 0.70f; 61 /** The percent of screen width for the expanded view on a small tablet. **/ 62 private static final float EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT = 0.72f; 63 /** The percent of screen width for the expanded view when shown in the bubble bar. **/ 64 private static final float EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT = 0.7f; 65 /** The percent of screen width for the expanded view when shown in the bubble bar. **/ 66 private static final float EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT = 0.4f; 67 68 private Context mContext; 69 private WindowManager mWindowManager; 70 private Rect mScreenRect; 71 private @Surface.Rotation int mRotation = Surface.ROTATION_0; 72 private Insets mInsets; 73 private boolean mImeVisible; 74 private int mImeHeight; 75 private boolean mIsLargeScreen; 76 private boolean mIsSmallTablet; 77 78 private Rect mPositionRect; 79 private int mDefaultMaxBubbles; 80 private int mMaxBubbles; 81 private int mBubbleSize; 82 private int mSpacingBetweenBubbles; 83 private int mBubblePaddingTop; 84 private int mBubbleOffscreenAmount; 85 private int mStackOffset; 86 87 private int mExpandedViewMinHeight; 88 private int mExpandedViewLargeScreenWidth; 89 private int mExpandedViewLargeScreenInsetClosestEdge; 90 private int mExpandedViewLargeScreenInsetFurthestEdge; 91 92 private int mOverflowWidth; 93 private int mExpandedViewPadding; 94 private int mPointerMargin; 95 private int mPointerWidth; 96 private int mPointerHeight; 97 private int mPointerOverlap; 98 private int mManageButtonHeight; 99 private int mOverflowHeight; 100 private int mMinimumFlyoutWidthLargeScreen; 101 102 private PointF mRestingStackPosition; 103 private int[] mPaddings = new int[4]; 104 105 private boolean mShowingInBubbleBar; 106 private final Point mBubbleBarPosition = new Point(); 107 BubblePositioner(Context context, WindowManager windowManager)108 public BubblePositioner(Context context, WindowManager windowManager) { 109 mContext = context; 110 mWindowManager = windowManager; 111 update(); 112 } 113 114 /** 115 * Available space and inset information. Call this when config changes 116 * occur or when added to a window. 117 */ update()118 public void update() { 119 WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); 120 if (windowMetrics == null) { 121 return; 122 } 123 WindowInsets metricInsets = windowMetrics.getWindowInsets(); 124 Insets insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() 125 | WindowInsets.Type.statusBars() 126 | WindowInsets.Type.displayCutout()); 127 128 final Rect bounds = windowMetrics.getBounds(); 129 Configuration config = mContext.getResources().getConfiguration(); 130 mIsLargeScreen = config.smallestScreenWidthDp >= 600; 131 if (mIsLargeScreen) { 132 float largestEdgeDp = Math.max(config.screenWidthDp, config.screenHeightDp); 133 mIsSmallTablet = largestEdgeDp < 960; 134 } else { 135 mIsSmallTablet = false; 136 } 137 138 if (BubbleDebugConfig.DEBUG_POSITIONER) { 139 Log.w(TAG, "update positioner:" 140 + " rotation: " + mRotation 141 + " insets: " + insets 142 + " isLargeScreen: " + mIsLargeScreen 143 + " isSmallTablet: " + mIsSmallTablet 144 + " showingInBubbleBar: " + mShowingInBubbleBar 145 + " bounds: " + bounds); 146 } 147 updateInternal(mRotation, insets, bounds); 148 } 149 150 @VisibleForTesting 151 public void updateInternal(int rotation, Insets insets, Rect bounds) { 152 mRotation = rotation; 153 mInsets = insets; 154 155 mScreenRect = new Rect(bounds); 156 mPositionRect = new Rect(bounds); 157 mPositionRect.left += mInsets.left; 158 mPositionRect.top += mInsets.top; 159 mPositionRect.right -= mInsets.right; 160 mPositionRect.bottom -= mInsets.bottom; 161 162 Resources res = mContext.getResources(); 163 mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); 164 mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing); 165 mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); 166 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); 167 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 168 mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); 169 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); 170 171 if (mShowingInBubbleBar) { 172 mExpandedViewLargeScreenWidth = isLandscape() 173 ? (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT) 174 : (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT); 175 } else if (mIsSmallTablet) { 176 mExpandedViewLargeScreenWidth = (int) (bounds.width() 177 * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT); 178 } else { 179 mExpandedViewLargeScreenWidth = isLandscape() 180 ? (int) (bounds.width() * EXPANDED_VIEW_LARGE_SCREEN_LANDSCAPE_WIDTH_PERCENT) 181 : (int) (bounds.width() * EXPANDED_VIEW_LARGE_SCREEN_PORTRAIT_WIDTH_PERCENT); 182 } 183 if (mIsLargeScreen) { 184 if (isLandscape() && !mIsSmallTablet) { 185 mExpandedViewLargeScreenInsetClosestEdge = res.getDimensionPixelSize( 186 R.dimen.bubble_expanded_view_largescreen_landscape_padding); 187 mExpandedViewLargeScreenInsetFurthestEdge = bounds.width() 188 - mExpandedViewLargeScreenInsetClosestEdge 189 - mExpandedViewLargeScreenWidth; 190 } else { 191 final int centeredInset = (bounds.width() - mExpandedViewLargeScreenWidth) / 2; 192 mExpandedViewLargeScreenInsetClosestEdge = centeredInset; 193 mExpandedViewLargeScreenInsetFurthestEdge = centeredInset; 194 } 195 } else { 196 mExpandedViewLargeScreenInsetClosestEdge = mExpandedViewPadding; 197 mExpandedViewLargeScreenInsetFurthestEdge = mExpandedViewPadding; 198 } 199 200 mOverflowWidth = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_overflow_width); 201 mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); 202 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); 203 mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); 204 mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap); 205 mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_total_height); 206 mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); 207 mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); 208 mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize( 209 R.dimen.bubbles_flyout_min_width_large_screen); 210 211 mMaxBubbles = calculateMaxBubbles(); 212 } 213 214 /** 215 * @return the maximum number of bubbles that can fit on the screen when expanded. If the 216 * screen size / screen density is too small to support the default maximum number, then 217 * the number will be adjust to something lower to ensure everything is presented nicely. 218 */ 219 private int calculateMaxBubbles() { 220 // Use the shortest edge. 221 // In portrait the bubbles should align with the expanded view so subtract its padding. 222 // We always show the overflow so subtract one bubble size. 223 int padding = showBubblesVertically() ? 0 : (mExpandedViewPadding * 2); 224 int availableSpace = Math.min(mPositionRect.width(), mPositionRect.height()) 225 - padding 226 - mBubbleSize; 227 // Each of the bubbles have spacing because the overflow is at the end. 228 int howManyFit = availableSpace / (mBubbleSize + mSpacingBetweenBubbles); 229 if (howManyFit < mDefaultMaxBubbles) { 230 // Not enough space for the default. 231 return howManyFit; 232 } 233 return mDefaultMaxBubbles; 234 } 235 236 237 /** 238 * @return a rect of available screen space accounting for orientation, system bars and cutouts. 239 * Does not account for IME. 240 */ 241 public Rect getAvailableRect() { 242 return mPositionRect; 243 } 244 245 /** 246 * @return a rect of the screen size. 247 */ 248 public Rect getScreenRect() { 249 return mScreenRect; 250 } 251 252 /** 253 * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its 254 * inset is not included here. 255 */ 256 public Insets getInsets() { 257 return mInsets; 258 } 259 260 /** @return whether the device is in landscape orientation. */ 261 public boolean isLandscape() { 262 return mContext.getResources().getConfiguration().orientation 263 == Configuration.ORIENTATION_LANDSCAPE; 264 } 265 266 /** @return whether the screen is considered large. */ 267 public boolean isLargeScreen() { 268 return mIsLargeScreen; 269 } 270 271 /** 272 * Indicates how bubbles appear when expanded. 273 * 274 * When false, bubbles display at the top of the screen with the expanded view 275 * below them. When true, bubbles display at the edges of the screen with the expanded view 276 * to the left or right side. 277 */ 278 public boolean showBubblesVertically() { 279 return isLandscape() || mIsLargeScreen; 280 } 281 282 /** Size of the bubble. */ 283 public int getBubbleSize() { 284 return mBubbleSize; 285 } 286 287 /** The amount of padding at the top of the screen that the bubbles avoid when being placed. */ 288 public int getBubblePaddingTop() { 289 return mBubblePaddingTop; 290 } 291 292 /** The amount the stack hang off of the screen when collapsed. */ 293 public int getStackOffScreenAmount() { 294 return mBubbleOffscreenAmount; 295 } 296 297 /** Offset of bubbles in the stack (i.e. how much they overlap). */ 298 public int getStackOffset() { 299 return mStackOffset; 300 } 301 302 /** Size of the visible (non-overlapping) part of the pointer. */ 303 public int getPointerSize() { 304 return mPointerHeight - mPointerOverlap; 305 } 306 307 /** The maximum number of bubbles that can be displayed comfortably on screen. */ 308 public int getMaxBubbles() { 309 return mMaxBubbles; 310 } 311 312 /** The height for the IME if it's visible. **/ 313 public int getImeHeight() { 314 return mImeVisible ? mImeHeight : 0; 315 } 316 317 /** Return top position of the IME if it's visible */ 318 public int getImeTop() { 319 if (mImeVisible) { 320 return getScreenRect().bottom - getImeHeight() - getInsets().bottom; 321 } 322 return 0; 323 } 324 325 /** Sets whether the IME is visible. **/ 326 public void setImeVisible(boolean visible, int height) { 327 mImeVisible = visible; 328 mImeHeight = height; 329 } 330 331 private int getExpandedViewLargeScreenInsetFurthestEdge(boolean isOverflow) { 332 if (isOverflow && mIsLargeScreen) { 333 return mScreenRect.width() 334 - mExpandedViewLargeScreenInsetClosestEdge 335 - mOverflowWidth; 336 } 337 return mExpandedViewLargeScreenInsetFurthestEdge; 338 } 339 340 /** 341 * Calculates the padding for the bubble expanded view. 342 * 343 * Some specifics: 344 * On large screens the width of the expanded view is restricted via this padding. 345 * On phone landscape the bubble overflow expanded view is also restricted via this padding. 346 * On large screens & landscape no top padding is set, the top position is set via translation. 347 * On phone portrait top padding is set as the space between the tip of the pointer and the 348 * bubble. 349 * When the overflow is shown it doesn't have the manage button to pad out the bottom so 350 * padding is added. 351 */ 352 public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) { 353 final int pointerTotalHeight = getPointerSize(); 354 final int expandedViewLargeScreenInsetFurthestEdge = 355 getExpandedViewLargeScreenInsetFurthestEdge(isOverflow); 356 if (mIsLargeScreen) { 357 // Note: 358 // If we're in portrait OR if we're a small tablet, then the two insets values will 359 // be equal. If we're landscape and a large tablet, the two values will be different. 360 // [left, top, right, bottom] 361 mPaddings[0] = onLeft 362 ? mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight 363 : expandedViewLargeScreenInsetFurthestEdge; 364 mPaddings[1] = 0; 365 mPaddings[2] = onLeft 366 ? expandedViewLargeScreenInsetFurthestEdge 367 : mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight; 368 // Overflow doesn't show manage button / get padding from it so add padding here 369 mPaddings[3] = isOverflow ? mExpandedViewPadding : 0; 370 return mPaddings; 371 } else { 372 int leftPadding = mInsets.left + mExpandedViewPadding; 373 int rightPadding = mInsets.right + mExpandedViewPadding; 374 final float expandedViewWidth = isOverflow 375 ? mOverflowWidth 376 : mExpandedViewLargeScreenWidth; 377 if (showBubblesVertically()) { 378 if (!onLeft) { 379 rightPadding += mBubbleSize - pointerTotalHeight; 380 leftPadding += isOverflow 381 ? (mPositionRect.width() - rightPadding - expandedViewWidth) 382 : 0; 383 } else { 384 leftPadding += mBubbleSize - pointerTotalHeight; 385 rightPadding += isOverflow 386 ? (mPositionRect.width() - leftPadding - expandedViewWidth) 387 : 0; 388 } 389 } 390 // [left, top, right, bottom] 391 mPaddings[0] = leftPadding; 392 mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin; 393 mPaddings[2] = rightPadding; 394 mPaddings[3] = 0; 395 return mPaddings; 396 } 397 } 398 399 /** Gets the y position of the expanded view if it was top-aligned. */ 400 public float getExpandedViewYTopAligned() { 401 final int top = getAvailableRect().top; 402 if (showBubblesVertically()) { 403 return top - mPointerWidth + mExpandedViewPadding; 404 } else { 405 return top + mBubbleSize + mPointerMargin; 406 } 407 } 408 409 /** 410 * Calculate the maximum height the expanded view can be depending on where it's placed on 411 * the screen and the size of the elements around it (e.g. padding, pointer, manage button). 412 */ 413 public int getMaxExpandedViewHeight(boolean isOverflow) { 414 // Subtract top insets because availableRect.height would account for that 415 int expandedContainerY = (int) getExpandedViewYTopAligned() - getInsets().top; 416 int paddingTop = showBubblesVertically() 417 ? 0 418 : mPointerHeight; 419 // Subtract pointer size because it's laid out in LinearLayout with the expanded view. 420 int pointerSize = showBubblesVertically() 421 ? mPointerWidth 422 : (mPointerHeight + mPointerMargin); 423 int bottomPadding = isOverflow ? mExpandedViewPadding : mManageButtonHeight; 424 return getAvailableRect().height() 425 - expandedContainerY 426 - paddingTop 427 - pointerSize 428 - bottomPadding; 429 } 430 431 /** 432 * Determines the height for the bubble, ensuring a minimum height. If the height should be as 433 * big as available, returns {@link #MAX_HEIGHT}. 434 */ 435 public float getExpandedViewHeight(BubbleViewProvider bubble) { 436 boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey()); 437 if (isOverflow && showBubblesVertically() && !mIsLargeScreen) { 438 // overflow in landscape on phone is max 439 return MAX_HEIGHT; 440 } 441 float desiredHeight = isOverflow 442 ? mOverflowHeight 443 : ((Bubble) bubble).getDesiredHeight(mContext); 444 desiredHeight = Math.max(desiredHeight, mExpandedViewMinHeight); 445 if (desiredHeight > getMaxExpandedViewHeight(isOverflow)) { 446 return MAX_HEIGHT; 447 } 448 return desiredHeight; 449 } 450 451 /** 452 * Gets the y position for the expanded view. This is the position on screen of the top 453 * horizontal line of the expanded view. 454 * 455 * @param bubble the bubble being positioned. 456 * @param bubblePosition the x position of the bubble if showing on top, the y position of the 457 * bubble if showing vertically. 458 * @return the y position for the expanded view. 459 */ 460 public float getExpandedViewY(BubbleViewProvider bubble, float bubblePosition) { 461 boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey()); 462 float expandedViewHeight = getExpandedViewHeight(bubble); 463 float topAlignment = getExpandedViewYTopAligned(); 464 if (!showBubblesVertically() || expandedViewHeight == MAX_HEIGHT) { 465 // Top-align when bubbles are shown at the top or are max size. 466 return topAlignment; 467 } 468 // If we're here, we're showing vertically & developer has made height less than maximum. 469 int manageButtonHeight = isOverflow ? mExpandedViewPadding : mManageButtonHeight; 470 float pointerPosition = getPointerPosition(bubblePosition); 471 float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight; 472 float topIfCentered = pointerPosition - (expandedViewHeight / 2); 473 if (topIfCentered > mPositionRect.top && mPositionRect.bottom > bottomIfCentered) { 474 // Center it 475 return pointerPosition - mPointerWidth - (expandedViewHeight / 2f); 476 } else if (topIfCentered <= mPositionRect.top) { 477 // Top align 478 return topAlignment; 479 } else { 480 // Bottom align 481 return mPositionRect.bottom - manageButtonHeight - expandedViewHeight - mPointerWidth; 482 } 483 } 484 485 /** 486 * The position the pointer points to, the center of the bubble. 487 * 488 * @param bubblePosition the x position of the bubble if showing on top, the y position of the 489 * bubble if showing vertically. 490 * @return the position the tip of the pointer points to. The x position if showing on top, the 491 * y position if showing vertically. 492 */ 493 public float getPointerPosition(float bubblePosition) { 494 // TODO: I don't understand why it works but it does - why normalized in portrait 495 // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation? 496 final float normalizedSize = IconNormalizer.getNormalizedCircleSize( 497 getBubbleSize()); 498 return showBubblesVertically() 499 ? bubblePosition + (getBubbleSize() / 2f) 500 : bubblePosition + (normalizedSize / 2f) - mPointerWidth; 501 } 502 503 private int getExpandedStackSize(int numberOfBubbles) { 504 return (numberOfBubbles * mBubbleSize) 505 + ((numberOfBubbles - 1) * mSpacingBetweenBubbles); 506 } 507 508 /** 509 * Returns the position of the bubble on-screen when the stack is expanded. 510 * 511 * @param index the index of the bubble in the stack. 512 * @param state state information about the stack to help with calculations. 513 * @return the position of the bubble on-screen when the stack is expanded. 514 */ 515 public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) { 516 boolean showBubblesVertically = showBubblesVertically(); 517 boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection() 518 == LAYOUT_DIRECTION_RTL; 519 520 int onScreenIndex; 521 if (showBubblesVertically || !isRtl) { 522 onScreenIndex = index; 523 } else { 524 // If bubbles are shown horizontally, check if RTL language is used. 525 // If RTL is active, position first bubble on the right and last on the left. 526 // Last bubble has screen index 0 and first bubble has max screen index value. 527 onScreenIndex = state.numberOfBubbles - 1 - index; 528 } 529 530 final float positionInRow = onScreenIndex * (mBubbleSize + mSpacingBetweenBubbles); 531 final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); 532 final float centerPosition = showBubblesVertically 533 ? mPositionRect.centerY() 534 : mPositionRect.centerX(); 535 // alignment - centered on the edge 536 final float rowStart = centerPosition - (expandedStackSize / 2f); 537 float x; 538 float y; 539 if (showBubblesVertically) { 540 int inset = mExpandedViewLargeScreenInsetClosestEdge; 541 y = rowStart + positionInRow; 542 int left = mIsLargeScreen 543 ? inset - mExpandedViewPadding - mBubbleSize 544 : mPositionRect.left; 545 int right = mIsLargeScreen 546 ? mPositionRect.right - inset + mExpandedViewPadding 547 : mPositionRect.right - mBubbleSize; 548 x = state.onLeft 549 ? left 550 : right; 551 } else { 552 y = mPositionRect.top + mExpandedViewPadding; 553 x = rowStart + positionInRow; 554 } 555 556 if (showBubblesVertically && mImeVisible) { 557 return new PointF(x, getExpandedBubbleYForIme(onScreenIndex, state)); 558 } 559 return new PointF(x, y); 560 } 561 562 /** 563 * Returns the position of the bubble on-screen when the stack is expanded and the IME 564 * is showing. 565 * 566 * @param index the index of the bubble in the stack. 567 * @param state information about the stack state (# of bubbles, selected bubble). 568 * @return y position of the bubble on-screen when the stack is expanded. 569 */ 570 private float getExpandedBubbleYForIme(int index, BubbleStackView.StackViewState state) { 571 final float top = getAvailableRect().top + mExpandedViewPadding; 572 if (!showBubblesVertically()) { 573 // Showing horizontally: align to top 574 return top; 575 } 576 577 // Showing vertically: might need to translate the bubbles above the IME. 578 // Add spacing here to provide a margin between top of IME and bottom of bubble row. 579 final float bottomHeight = getImeHeight() + mInsets.bottom + (mSpacingBetweenBubbles * 2); 580 final float bottomInset = mScreenRect.bottom - bottomHeight; 581 final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); 582 final float centerPosition = mPositionRect.centerY(); 583 final float rowBottom = centerPosition + (expandedStackSize / 2f); 584 final float rowTop = centerPosition - (expandedStackSize / 2f); 585 float rowTopForIme = rowTop; 586 if (rowBottom > bottomInset) { 587 // We overlap with IME, must shift the bubbles 588 float translationY = rowBottom - bottomInset; 589 rowTopForIme = Math.max(rowTop - translationY, top); 590 if (rowTop - translationY < top) { 591 // Even if we shift the bubbles, they will still overlap with the IME. 592 // Hide the overflow for a lil more space: 593 final float expandedStackSizeNoO = getExpandedStackSize(state.numberOfBubbles - 1); 594 final float centerPositionNoO = showBubblesVertically() 595 ? mPositionRect.centerY() 596 : mPositionRect.centerX(); 597 final float rowBottomNoO = centerPositionNoO + (expandedStackSizeNoO / 2f); 598 final float rowTopNoO = centerPositionNoO - (expandedStackSizeNoO / 2f); 599 translationY = rowBottomNoO - bottomInset; 600 rowTopForIme = rowTopNoO - translationY; 601 } 602 } 603 // Check if the selected bubble is within the appropriate space 604 final float selectedPosition = rowTopForIme 605 + (state.selectedIndex * (mBubbleSize + mSpacingBetweenBubbles)); 606 if (selectedPosition < top) { 607 // We must always keep the selected bubble in view so we'll have to allow more overlap. 608 rowTopForIme = top; 609 } 610 return rowTopForIme + (index * (mBubbleSize + mSpacingBetweenBubbles)); 611 } 612 613 /** 614 * @return the width of the bubble flyout (message originating from the bubble). 615 */ 616 public float getMaxFlyoutSize() { 617 if (isLargeScreen()) { 618 return Math.max(mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN, 619 mMinimumFlyoutWidthLargeScreen); 620 } 621 return mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT; 622 } 623 624 /** 625 * @return whether the stack is considered on the left side of the screen. 626 */ 627 public boolean isStackOnLeft(PointF currentStackPosition) { 628 if (currentStackPosition == null) { 629 currentStackPosition = getRestingPosition(); 630 } 631 final int stackCenter = (int) currentStackPosition.x + mBubbleSize / 2; 632 return stackCenter < mScreenRect.width() / 2; 633 } 634 635 /** 636 * Sets the stack's most recent position along the edge of the screen. This is saved when the 637 * last bubble is removed, so that the stack can be restored in its previous position. 638 */ 639 public void setRestingPosition(PointF position) { 640 if (mRestingStackPosition == null) { 641 mRestingStackPosition = new PointF(position); 642 } else { 643 mRestingStackPosition.set(position); 644 } 645 } 646 647 /** The position the bubble stack should rest at when collapsed. */ 648 public PointF getRestingPosition() { 649 if (mRestingStackPosition == null) { 650 return getDefaultStartPosition(); 651 } 652 return mRestingStackPosition; 653 } 654 655 /** 656 * Returns whether the {@link #getRestingPosition()} is equal to the default start position 657 * initialized for bubbles, if {@code true} this means the user hasn't moved the bubble 658 * from the initial start position (or they haven't received a bubble yet). 659 */ 660 public boolean hasUserModifiedDefaultPosition() { 661 PointF defaultStart = getDefaultStartPosition(); 662 return mRestingStackPosition != null 663 && !mRestingStackPosition.equals(defaultStart); 664 } 665 666 /** 667 * Returns the stack position to use if we don't have a saved location or if user education 668 * is being shown, for a normal bubble. 669 */ 670 public PointF getDefaultStartPosition() { 671 return getDefaultStartPosition(false /* isAppBubble */); 672 } 673 674 /** 675 * The stack position to use if we don't have a saved location or if user education 676 * is being shown. 677 * 678 * @param isAppBubble whether this start position is for an app bubble or not. 679 */ 680 public PointF getDefaultStartPosition(boolean isAppBubble) { 681 final int layoutDirection = mContext.getResources().getConfiguration().getLayoutDirection(); 682 // Normal bubbles start on the left if we're in LTR, right otherwise. 683 // TODO (b/294284894): update language around "app bubble" here 684 // App bubbles start on the right in RTL, left otherwise. 685 final boolean startOnLeft = isAppBubble 686 ? layoutDirection == LAYOUT_DIRECTION_RTL 687 : layoutDirection != LAYOUT_DIRECTION_RTL; 688 final RectF allowableStackPositionRegion = getAllowableStackPositionRegion( 689 1 /* default starts with 1 bubble */); 690 if (isLargeScreen()) { 691 // We want the stack to be visually centered on the edge, so we need to base it 692 // of a rect that includes insets. 693 final float desiredY = mScreenRect.height() / 2f - (mBubbleSize / 2f); 694 final float offset = desiredY / mScreenRect.height(); 695 return new BubbleStackView.RelativeStackPosition( 696 startOnLeft, 697 offset) 698 .getAbsolutePositionInRegion(allowableStackPositionRegion); 699 } else { 700 final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset( 701 R.dimen.bubble_stack_starting_offset_y); 702 // TODO: placement bug here because mPositionRect doesn't handle the overhanging edge 703 return new BubbleStackView.RelativeStackPosition( 704 startOnLeft, 705 startingVerticalOffset / mPositionRect.height()) 706 .getAbsolutePositionInRegion(allowableStackPositionRegion); 707 } 708 } 709 710 /** 711 * Returns the region that the stack position must stay within. This goes slightly off the left 712 * and right sides of the screen, below the status bar/cutout and above the navigation bar. 713 * While the stack position is not allowed to rest outside of these bounds, it can temporarily 714 * be animated or dragged beyond them. 715 */ 716 public RectF getAllowableStackPositionRegion(int bubbleCount) { 717 final RectF allowableRegion = new RectF(getAvailableRect()); 718 final int imeHeight = getImeHeight(); 719 final float bottomPadding = bubbleCount > 1 720 ? mBubblePaddingTop + mStackOffset 721 : mBubblePaddingTop; 722 allowableRegion.left -= mBubbleOffscreenAmount; 723 allowableRegion.top += mBubblePaddingTop; 724 allowableRegion.right += mBubbleOffscreenAmount - mBubbleSize; 725 allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize; 726 return allowableRegion; 727 } 728 729 /** 730 * Navigation bar has an area where system gestures can be started from. 731 * 732 * @return {@link Rect} for system navigation bar gesture zone 733 */ 734 public Rect getNavBarGestureZone() { 735 // Gesture zone height from the bottom 736 int gestureZoneHeight = mContext.getResources().getDimensionPixelSize( 737 com.android.internal.R.dimen.navigation_bar_gesture_height); 738 Rect screen = getScreenRect(); 739 return new Rect( 740 screen.left, 741 screen.bottom - gestureZoneHeight, 742 screen.right, 743 screen.bottom); 744 } 745 746 // 747 // Bubble bar specific sizes below. 748 // 749 750 /** 751 * Sets whether bubbles are showing in the bubble bar from launcher. 752 */ 753 public void setShowingInBubbleBar(boolean showingInBubbleBar) { 754 mShowingInBubbleBar = showingInBubbleBar; 755 } 756 757 /** 758 * Sets the position of the bubble bar in screen coordinates. 759 * 760 * @param offsetX the offset of the bubble bar from the edge of the screen on the X axis 761 * @param offsetY the offset of the bubble bar from the edge of the screen on the Y axis 762 */ 763 public void setBubbleBarPosition(int offsetX, int offsetY) { 764 mBubbleBarPosition.set( 765 getAvailableRect().width() - offsetX, 766 getAvailableRect().height() + mInsets.top + mInsets.bottom - offsetY); 767 } 768 769 /** 770 * How wide the expanded view should be when showing from the bubble bar. 771 */ 772 public int getExpandedViewWidthForBubbleBar(boolean isOverflow) { 773 return isOverflow ? mOverflowWidth : mExpandedViewLargeScreenWidth; 774 } 775 776 /** 777 * How tall the expanded view should be when showing from the bubble bar. 778 */ 779 public int getExpandedViewHeightForBubbleBar(boolean isOverflow) { 780 return isOverflow 781 ? mOverflowHeight 782 : getExpandedViewBottomForBubbleBar() - mInsets.top - mExpandedViewPadding; 783 } 784 785 /** The bottom position of the expanded view when showing above the bubble bar. */ 786 public int getExpandedViewBottomForBubbleBar() { 787 return mBubbleBarPosition.y - mExpandedViewPadding; 788 } 789 790 /** 791 * The amount of padding from the edge of the screen to the expanded view when in bubble bar. 792 */ 793 public int getBubbleBarExpandedViewPadding() { 794 return mExpandedViewPadding; 795 } 796 797 /** 798 * Returns the on screen co-ordinates of the bubble bar. 799 */ 800 public Point getBubbleBarPosition() { 801 return mBubbleBarPosition; 802 } 803 } 804