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 com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Insets; 24 import android.graphics.PointF; 25 import android.graphics.Rect; 26 import android.graphics.RectF; 27 import android.view.Surface; 28 import android.view.WindowManager; 29 30 import androidx.annotation.VisibleForTesting; 31 32 import com.android.internal.protolog.common.ProtoLog; 33 import com.android.launcher3.icons.IconNormalizer; 34 import com.android.wm.shell.R; 35 import com.android.wm.shell.common.bubbles.BubbleBarLocation; 36 37 /** 38 * Keeps track of display size, configuration, and specific bubble sizes. One place for all 39 * placement and positioning calculations to refer to. 40 */ 41 public class BubblePositioner { 42 43 /** The screen edge the bubble stack is pinned to */ 44 public enum StackPinnedEdge { 45 LEFT, 46 RIGHT 47 } 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 small tablet. **/ 58 private static final float EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT = 0.72f; 59 /** The percent of screen width for the expanded view when shown in the bubble bar. **/ 60 private static final float EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT = 0.7f; 61 /** The percent of screen width for the expanded view when shown in the bubble bar. **/ 62 private static final float EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT = 0.4f; 63 64 private Context mContext; 65 private DeviceConfig mDeviceConfig; 66 private Rect mScreenRect; 67 private @Surface.Rotation int mRotation = Surface.ROTATION_0; 68 private Insets mInsets; 69 private boolean mImeVisible; 70 private int mImeHeight; 71 private Rect mPositionRect; 72 private int mDefaultMaxBubbles; 73 private int mMaxBubbles; 74 private int mBubbleSize; 75 private int mSpacingBetweenBubbles; 76 private int mBubblePaddingTop; 77 private int mBubbleOffscreenAmount; 78 private int mStackOffset; 79 private int mBubbleElevation; 80 81 private int mExpandedViewMinHeight; 82 private int mExpandedViewLargeScreenWidth; 83 private int mExpandedViewLargeScreenInsetClosestEdge; 84 private int mExpandedViewLargeScreenInsetFurthestEdge; 85 86 private int mOverflowWidth; 87 private int mExpandedViewPadding; 88 private int mPointerMargin; 89 private int mPointerWidth; 90 private int mPointerHeight; 91 private int mPointerOverlap; 92 private int mManageButtonHeightIncludingMargins; 93 private int mManageButtonHeight; 94 private int mOverflowHeight; 95 private int mMinimumFlyoutWidthLargeScreen; 96 97 private PointF mRestingStackPosition; 98 99 private boolean mShowingInBubbleBar; 100 private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT; 101 private int mBubbleBarTopOnScreen; 102 BubblePositioner(Context context, WindowManager windowManager)103 public BubblePositioner(Context context, WindowManager windowManager) { 104 mContext = context; 105 mDeviceConfig = DeviceConfig.create(context, windowManager); 106 update(mDeviceConfig); 107 } 108 109 /** 110 * Available space and inset information. Call this when config changes 111 * occur or when added to a window. 112 */ update(DeviceConfig deviceConfig)113 public void update(DeviceConfig deviceConfig) { 114 mDeviceConfig = deviceConfig; 115 ProtoLog.d(WM_SHELL_BUBBLES, "update positioner: " 116 + "rotation=%d insets=%s largeScreen=%b " 117 + "smallTablet=%b isBubbleBar=%b bounds=%s", 118 mRotation, deviceConfig.getInsets(), deviceConfig.isLargeScreen(), 119 deviceConfig.isSmallTablet(), mShowingInBubbleBar, 120 deviceConfig.getWindowBounds()); 121 updateInternal(mRotation, deviceConfig.getInsets(), deviceConfig.getWindowBounds()); 122 } 123 124 @VisibleForTesting updateInternal(int rotation, Insets insets, Rect bounds)125 public void updateInternal(int rotation, Insets insets, Rect bounds) { 126 BubbleStackView.RelativeStackPosition prevStackPosition = null; 127 if (mRestingStackPosition != null && mScreenRect != null && !mScreenRect.equals(bounds)) { 128 // Save the resting position as a relative position with the previous bounds, at the 129 // end of the update we'll restore it based on the new bounds. 130 prevStackPosition = new BubbleStackView.RelativeStackPosition(getRestingPosition(), 131 getAllowableStackPositionRegion(1)); 132 } 133 mRotation = rotation; 134 mInsets = insets; 135 136 mScreenRect = new Rect(bounds); 137 mPositionRect = new Rect(bounds); 138 mPositionRect.left += mInsets.left; 139 mPositionRect.top += mInsets.top; 140 mPositionRect.right -= mInsets.right; 141 mPositionRect.bottom -= mInsets.bottom; 142 143 Resources res = mContext.getResources(); 144 mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); 145 mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing); 146 mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); 147 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); 148 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 149 mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); 150 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); 151 mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); 152 153 if (mShowingInBubbleBar) { 154 mExpandedViewLargeScreenWidth = Math.min( 155 res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width), 156 mPositionRect.width() - 2 * mExpandedViewPadding 157 ); 158 } else if (mDeviceConfig.isSmallTablet()) { 159 mExpandedViewLargeScreenWidth = (int) (bounds.width() 160 * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT); 161 } else { 162 mExpandedViewLargeScreenWidth = 163 res.getDimensionPixelSize(R.dimen.bubble_expanded_view_largescreen_width); 164 } 165 if (mDeviceConfig.isLargeScreen()) { 166 if (mDeviceConfig.isSmallTablet()) { 167 final int centeredInset = (bounds.width() - mExpandedViewLargeScreenWidth) / 2; 168 mExpandedViewLargeScreenInsetClosestEdge = centeredInset; 169 mExpandedViewLargeScreenInsetFurthestEdge = centeredInset; 170 } else { 171 mExpandedViewLargeScreenInsetClosestEdge = res.getDimensionPixelSize( 172 R.dimen.bubble_expanded_view_largescreen_landscape_padding); 173 mExpandedViewLargeScreenInsetFurthestEdge = bounds.width() 174 - mExpandedViewLargeScreenInsetClosestEdge 175 - mExpandedViewLargeScreenWidth; 176 } 177 } else { 178 mExpandedViewLargeScreenInsetClosestEdge = mExpandedViewPadding; 179 mExpandedViewLargeScreenInsetFurthestEdge = mExpandedViewPadding; 180 } 181 182 mOverflowWidth = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_overflow_width); 183 mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); 184 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); 185 mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); 186 mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap); 187 mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_height); 188 mManageButtonHeightIncludingMargins = 189 mManageButtonHeight 190 + 2 * res.getDimensionPixelSize(R.dimen.bubble_manage_button_margin); 191 mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); 192 mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); 193 mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize( 194 R.dimen.bubbles_flyout_min_width_large_screen); 195 196 mMaxBubbles = calculateMaxBubbles(); 197 198 if (prevStackPosition != null) { 199 // Get the new resting position based on the updated values 200 mRestingStackPosition = prevStackPosition.getAbsolutePositionInRegion( 201 getAllowableStackPositionRegion(1)); 202 } 203 } 204 205 /** 206 * @return the maximum number of bubbles that can fit on the screen when expanded. If the 207 * screen size / screen density is too small to support the default maximum number, then 208 * the number will be adjust to something lower to ensure everything is presented nicely. 209 */ calculateMaxBubbles()210 private int calculateMaxBubbles() { 211 // Use the shortest edge. 212 // In portrait the bubbles should align with the expanded view so subtract its padding. 213 // We always show the overflow so subtract one bubble size. 214 int padding = showBubblesVertically() ? 0 : (mExpandedViewPadding * 2); 215 int availableSpace = Math.min(mPositionRect.width(), mPositionRect.height()) 216 - padding 217 - mBubbleSize; 218 // Each of the bubbles have spacing because the overflow is at the end. 219 int howManyFit = availableSpace / (mBubbleSize + mSpacingBetweenBubbles); 220 if (howManyFit < mDefaultMaxBubbles) { 221 // Not enough space for the default. 222 return howManyFit; 223 } 224 return mDefaultMaxBubbles; 225 } 226 227 228 /** 229 * @return a rect of available screen space accounting for orientation, system bars and cutouts. 230 * Does not account for IME. 231 */ getAvailableRect()232 public Rect getAvailableRect() { 233 return mPositionRect; 234 } 235 236 /** 237 * @return a rect of the screen size. 238 */ getScreenRect()239 public Rect getScreenRect() { 240 return mScreenRect; 241 } 242 243 /** 244 * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its 245 * inset is not included here. 246 */ getInsets()247 public Insets getInsets() { 248 return mInsets; 249 } 250 251 /** @return whether the device is in landscape orientation. */ isLandscape()252 public boolean isLandscape() { 253 return mDeviceConfig.isLandscape(); 254 } 255 256 /** 257 * On large screen (not small tablet), while in portrait, expanded bubbles are aligned to 258 * the bottom of the screen. 259 * 260 * @return whether bubbles are bottom aligned while expanded 261 */ areBubblesBottomAligned()262 public boolean areBubblesBottomAligned() { 263 return isLargeScreen() 264 && !mDeviceConfig.isSmallTablet() 265 && !isLandscape(); 266 } 267 268 /** @return whether the screen is considered large. */ isLargeScreen()269 public boolean isLargeScreen() { 270 return mDeviceConfig.isLargeScreen(); 271 } 272 273 /** 274 * Indicates how bubbles appear when expanded. 275 * 276 * When false, bubbles display at the top of the screen with the expanded view 277 * below them. When true, bubbles display at the edges of the screen with the expanded view 278 * to the left or right side. 279 */ showBubblesVertically()280 public boolean showBubblesVertically() { 281 return isLandscape() || mDeviceConfig.isLargeScreen(); 282 } 283 284 /** Size of the bubble. */ getBubbleSize()285 public int getBubbleSize() { 286 return mBubbleSize; 287 } 288 289 /** The amount of padding at the top of the screen that the bubbles avoid when being placed. */ getBubblePaddingTop()290 public int getBubblePaddingTop() { 291 return mBubblePaddingTop; 292 } 293 294 /** The amount the stack hang off of the screen when collapsed. */ getStackOffScreenAmount()295 public int getStackOffScreenAmount() { 296 return mBubbleOffscreenAmount; 297 } 298 299 /** Offset of bubbles in the stack (i.e. how much they overlap). */ getStackOffset()300 public int getStackOffset() { 301 return mStackOffset; 302 } 303 304 /** Size of the visible (non-overlapping) part of the pointer. */ getPointerSize()305 public int getPointerSize() { 306 return mPointerHeight - mPointerOverlap; 307 } 308 309 /** The maximum number of bubbles that can be displayed comfortably on screen. */ getMaxBubbles()310 public int getMaxBubbles() { 311 return mMaxBubbles; 312 } 313 314 /** The height for the IME if it's visible. **/ getImeHeight()315 public int getImeHeight() { 316 return mImeVisible ? mImeHeight : 0; 317 } 318 319 /** Return top position of the IME if it's visible */ getImeTop()320 public int getImeTop() { 321 if (mImeVisible) { 322 return getScreenRect().bottom - getImeHeight() - getInsets().bottom; 323 } 324 return 0; 325 } 326 327 /** Returns whether the IME is visible. */ isImeVisible()328 public boolean isImeVisible() { 329 return mImeVisible; 330 } 331 332 /** Sets whether the IME is visible. **/ setImeVisible(boolean visible, int height)333 public void setImeVisible(boolean visible, int height) { 334 mImeVisible = visible; 335 mImeHeight = height; 336 } 337 getExpandedViewLargeScreenInsetFurthestEdge(boolean isOverflow)338 private int getExpandedViewLargeScreenInsetFurthestEdge(boolean isOverflow) { 339 if (isOverflow && mDeviceConfig.isLargeScreen()) { 340 return mScreenRect.width() 341 - mExpandedViewLargeScreenInsetClosestEdge 342 - mOverflowWidth; 343 } 344 return mExpandedViewLargeScreenInsetFurthestEdge; 345 } 346 347 /** 348 * Calculates the padding for the bubble expanded view. 349 * 350 * Some specifics: 351 * On large screens the width of the expanded view is restricted via this padding. 352 * On phone landscape the bubble overflow expanded view is also restricted via this padding. 353 * On large screens & landscape no top padding is set, the top position is set via translation. 354 * On phone portrait top padding is set as the space between the tip of the pointer and the 355 * bubble. 356 * When the overflow is shown it doesn't have the manage button to pad out the bottom so 357 * padding is added. 358 */ getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow)359 public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) { 360 final int pointerTotalHeight = getPointerSize(); 361 final int expandedViewLargeScreenInsetFurthestEdge = 362 getExpandedViewLargeScreenInsetFurthestEdge(isOverflow); 363 int[] paddings = new int[4]; 364 if (mDeviceConfig.isLargeScreen()) { 365 // Note: 366 // If we're in portrait OR if we're a small tablet, then the two insets values will 367 // be equal. If we're landscape and a large tablet, the two values will be different. 368 // [left, top, right, bottom] 369 paddings[0] = onLeft 370 ? mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight 371 : expandedViewLargeScreenInsetFurthestEdge; 372 paddings[1] = 0; 373 paddings[2] = onLeft 374 ? expandedViewLargeScreenInsetFurthestEdge 375 : mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight; 376 // Overflow doesn't show manage button / get padding from it so add padding here 377 paddings[3] = isOverflow ? mExpandedViewPadding : 0; 378 return paddings; 379 } else { 380 int leftPadding = mInsets.left + mExpandedViewPadding; 381 int rightPadding = mInsets.right + mExpandedViewPadding; 382 if (showBubblesVertically()) { 383 if (!onLeft) { 384 rightPadding += mBubbleSize - pointerTotalHeight; 385 leftPadding += isOverflow 386 ? (mPositionRect.width() - rightPadding - mOverflowWidth) 387 : 0; 388 } else { 389 leftPadding += mBubbleSize - pointerTotalHeight; 390 rightPadding += isOverflow 391 ? (mPositionRect.width() - leftPadding - mOverflowWidth) 392 : 0; 393 } 394 } 395 // [left, top, right, bottom] 396 paddings[0] = leftPadding; 397 paddings[1] = showBubblesVertically() ? 0 : mPointerMargin; 398 paddings[2] = rightPadding; 399 paddings[3] = 0; 400 return paddings; 401 } 402 } 403 404 /** Returns the width of the task view content. */ getTaskViewContentWidth(boolean onLeft)405 public int getTaskViewContentWidth(boolean onLeft) { 406 int[] paddings = getExpandedViewContainerPadding(onLeft, /* isOverflow = */ false); 407 int pointerOffset = showBubblesVertically() ? getPointerSize() : 0; 408 return mScreenRect.width() - paddings[0] - paddings[2] - pointerOffset; 409 } 410 411 /** Gets the y position of the expanded view if it was top-aligned. */ getExpandedViewYTopAligned()412 public int getExpandedViewYTopAligned() { 413 final int top = getAvailableRect().top; 414 if (showBubblesVertically()) { 415 return top - mPointerWidth + mExpandedViewPadding; 416 } else { 417 return top + mBubbleSize + mPointerMargin; 418 } 419 } 420 421 /** 422 * Calculate the maximum height the expanded view can be depending on where it's placed on 423 * the screen and the size of the elements around it (e.g. padding, pointer, manage button). 424 */ getMaxExpandedViewHeight(boolean isOverflow)425 public int getMaxExpandedViewHeight(boolean isOverflow) { 426 if (mDeviceConfig.isLargeScreen() && !mDeviceConfig.isSmallTablet() && !isOverflow) { 427 return getExpandedViewHeightForLargeScreen(); 428 } 429 // Subtract top insets because availableRect.height would account for that 430 int expandedContainerY = getExpandedViewYTopAligned() - getInsets().top; 431 int paddingTop = showBubblesVertically() 432 ? 0 433 : mPointerHeight; 434 // Subtract pointer size because it's laid out in LinearLayout with the expanded view. 435 int pointerSize = showBubblesVertically() 436 ? mPointerWidth 437 : (mPointerHeight + mPointerMargin); 438 int bottomPadding = isOverflow ? mExpandedViewPadding : mManageButtonHeightIncludingMargins; 439 return getAvailableRect().height() 440 - expandedContainerY 441 - paddingTop 442 - pointerSize 443 - bottomPadding; 444 } 445 446 /** 447 * Returns the height to use for the expanded view when showing on a large screen. 448 */ getExpandedViewHeightForLargeScreen()449 public int getExpandedViewHeightForLargeScreen() { 450 // the expanded view height on large tablets is calculated based on the shortest screen 451 // size and is the same in both portrait and landscape 452 int maxVerticalInset = Math.max(mInsets.top, mInsets.bottom); 453 int shortestScreenSide = Math.min(getScreenRect().height(), getScreenRect().width()); 454 // Subtract pointer size because it's laid out in LinearLayout with the expanded view. 455 return shortestScreenSide - maxVerticalInset * 2 456 - mManageButtonHeight - mPointerWidth - mExpandedViewPadding * 2; 457 } 458 459 /** 460 * Determines the height for the bubble, ensuring a minimum height. If the height should be as 461 * big as available, returns {@link #MAX_HEIGHT}. 462 */ getExpandedViewHeight(BubbleViewProvider bubble)463 public float getExpandedViewHeight(BubbleViewProvider bubble) { 464 boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey()); 465 if (isOverflow && showBubblesVertically() && !mDeviceConfig.isLargeScreen()) { 466 // overflow in landscape on phone is max 467 return MAX_HEIGHT; 468 } 469 float desiredHeight = isOverflow 470 ? mOverflowHeight 471 : ((Bubble) bubble).getDesiredHeight(mContext); 472 desiredHeight = Math.max(desiredHeight, mExpandedViewMinHeight); 473 if (desiredHeight > getMaxExpandedViewHeight(isOverflow)) { 474 return MAX_HEIGHT; 475 } 476 return desiredHeight; 477 } 478 479 /** 480 * Gets the y position for the expanded view. This is the position on screen of the top 481 * horizontal line of the expanded view. 482 * 483 * @param bubble the bubble being positioned. 484 * @param bubblePosition the x position of the bubble if showing on top, the y position of the 485 * bubble if showing vertically. 486 * @return the y position for the expanded view. 487 */ getExpandedViewY(BubbleViewProvider bubble, float bubblePosition)488 public float getExpandedViewY(BubbleViewProvider bubble, float bubblePosition) { 489 boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey()); 490 float expandedViewHeight = getExpandedViewHeight(bubble); 491 int topAlignment = getExpandedViewYTopAligned(); 492 int manageButtonHeight = 493 isOverflow ? mExpandedViewPadding : mManageButtonHeightIncludingMargins; 494 495 // On large screen portrait bubbles are bottom aligned. 496 if (areBubblesBottomAligned() && expandedViewHeight == MAX_HEIGHT) { 497 return mPositionRect.bottom - manageButtonHeight 498 - getExpandedViewHeightForLargeScreen() - mPointerWidth; 499 } 500 501 if (!showBubblesVertically() || expandedViewHeight == MAX_HEIGHT) { 502 // Top-align when bubbles are shown at the top or are max size. 503 return topAlignment; 504 } 505 506 // If we're here, we're showing vertically & developer has made height less than maximum. 507 float pointerPosition = getPointerPosition(bubblePosition); 508 float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight; 509 float topIfCentered = pointerPosition - (expandedViewHeight / 2); 510 if (topIfCentered > mPositionRect.top && mPositionRect.bottom > bottomIfCentered) { 511 // Center it 512 return pointerPosition - mPointerWidth - (expandedViewHeight / 2f); 513 } else if (topIfCentered <= mPositionRect.top) { 514 // Top align 515 return topAlignment; 516 } else { 517 // Bottom align 518 return mPositionRect.bottom - manageButtonHeight - expandedViewHeight - mPointerWidth; 519 } 520 } 521 522 /** 523 * The position the pointer points to, the center of the bubble. 524 * 525 * @param bubblePosition the x position of the bubble if showing on top, the y position of the 526 * bubble if showing vertically. 527 * @return the position the tip of the pointer points to. The x position if showing on top, the 528 * y position if showing vertically. 529 */ getPointerPosition(float bubblePosition)530 public float getPointerPosition(float bubblePosition) { 531 // TODO: I don't understand why it works but it does - why normalized in portrait 532 // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation? 533 final float normalizedSize = IconNormalizer.getNormalizedCircleSize( 534 getBubbleSize()); 535 return showBubblesVertically() 536 ? bubblePosition + (getBubbleSize() / 2f) 537 : bubblePosition + (normalizedSize / 2f) - mPointerWidth; 538 } 539 getExpandedStackSize(int numberOfBubbles)540 private int getExpandedStackSize(int numberOfBubbles) { 541 return (numberOfBubbles * mBubbleSize) 542 + ((numberOfBubbles - 1) * mSpacingBetweenBubbles); 543 } 544 545 /** 546 * Returns the position of the bubble on-screen when the stack is expanded. 547 * 548 * @param index the index of the bubble in the stack. 549 * @param state state information about the stack to help with calculations. 550 * @return the position of the bubble on-screen when the stack is expanded. 551 */ getExpandedBubbleXY(int index, BubbleStackView.StackViewState state)552 public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) { 553 boolean showBubblesVertically = showBubblesVertically(); 554 555 int onScreenIndex; 556 if (showBubblesVertically || !mDeviceConfig.isRtl()) { 557 onScreenIndex = index; 558 } else { 559 // If bubbles are shown horizontally, check if RTL language is used. 560 // If RTL is active, position first bubble on the right and last on the left. 561 // Last bubble has screen index 0 and first bubble has max screen index value. 562 onScreenIndex = state.numberOfBubbles - 1 - index; 563 } 564 final float positionInRow = onScreenIndex * (mBubbleSize + mSpacingBetweenBubbles); 565 final float rowStart = getBubbleRowStart(state); 566 float x; 567 float y; 568 if (showBubblesVertically) { 569 int inset = mExpandedViewLargeScreenInsetClosestEdge; 570 y = rowStart + positionInRow; 571 int left = mDeviceConfig.isLargeScreen() 572 ? inset - mExpandedViewPadding - mBubbleSize 573 : mPositionRect.left; 574 int right = mDeviceConfig.isLargeScreen() 575 ? mPositionRect.right - inset + mExpandedViewPadding 576 : mPositionRect.right - mBubbleSize; 577 x = state.onLeft 578 ? left 579 : right; 580 } else { 581 y = mPositionRect.top + mExpandedViewPadding; 582 x = rowStart + positionInRow; 583 } 584 585 if (showBubblesVertically && mImeVisible) { 586 return new PointF(x, getExpandedBubbleYForIme(onScreenIndex, state)); 587 } 588 return new PointF(x, y); 589 } 590 getBubbleRowStart(BubbleStackView.StackViewState state)591 private float getBubbleRowStart(BubbleStackView.StackViewState state) { 592 final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); 593 final float rowStart; 594 if (areBubblesBottomAligned()) { 595 final float expandedViewHeight = getExpandedViewHeightForLargeScreen(); 596 final float expandedViewBottom = mScreenRect.bottom 597 - Math.max(mInsets.bottom, mInsets.top) 598 - mManageButtonHeight - mPointerWidth; 599 final float expandedViewCenter = expandedViewBottom - (expandedViewHeight / 2f); 600 rowStart = expandedViewCenter - (expandedStackSize / 2f); 601 } else { 602 final float centerPosition = showBubblesVertically() 603 ? mPositionRect.centerY() 604 : mPositionRect.centerX(); 605 rowStart = centerPosition - (expandedStackSize / 2f); 606 } 607 return rowStart; 608 } 609 610 /** 611 * Returns the position of the bubble on-screen when the stack is expanded and the IME 612 * is showing. 613 * 614 * @param index the index of the bubble in the stack. 615 * @param state information about the stack state (# of bubbles, selected bubble). 616 * @return y position of the bubble on-screen when the stack is expanded. 617 */ getExpandedBubbleYForIme(int index, BubbleStackView.StackViewState state)618 private float getExpandedBubbleYForIme(int index, BubbleStackView.StackViewState state) { 619 final float top = getAvailableRect().top + mExpandedViewPadding; 620 if (!showBubblesVertically()) { 621 // Showing horizontally: align to top 622 return top; 623 } 624 625 // Showing vertically: might need to translate the bubbles above the IME. 626 // Add spacing here to provide a margin between top of IME and bottom of bubble row. 627 final float bottomHeight = getImeHeight() + mInsets.bottom + (mSpacingBetweenBubbles * 2); 628 final float bottomInset = mScreenRect.bottom - bottomHeight; 629 final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); 630 final float rowTop = getBubbleRowStart(state); 631 final float rowBottom = rowTop + expandedStackSize; 632 float rowTopForIme = rowTop; 633 if (rowBottom > bottomInset) { 634 // We overlap with IME, must shift the bubbles 635 float translationY = rowBottom - bottomInset; 636 rowTopForIme = Math.max(rowTop - translationY, top); 637 if (rowTop - translationY < top) { 638 // Even if we shift the bubbles, they will still overlap with the IME. 639 // Hide the overflow for a lil more space: 640 final float expandedStackSizeNoO = getExpandedStackSize(state.numberOfBubbles - 1); 641 final float centerPositionNoO = showBubblesVertically() 642 ? mPositionRect.centerY() 643 : mPositionRect.centerX(); 644 final float rowBottomNoO = centerPositionNoO + (expandedStackSizeNoO / 2f); 645 final float rowTopNoO = centerPositionNoO - (expandedStackSizeNoO / 2f); 646 translationY = rowBottomNoO - bottomInset; 647 rowTopForIme = rowTopNoO - translationY; 648 } 649 } 650 // Check if the selected bubble is within the appropriate space 651 final float selectedPosition = rowTopForIme 652 + (state.selectedIndex * (mBubbleSize + mSpacingBetweenBubbles)); 653 if (selectedPosition < top) { 654 // We must always keep the selected bubble in view so we'll have to allow more overlap. 655 rowTopForIme = top; 656 } 657 return rowTopForIme + (index * (mBubbleSize + mSpacingBetweenBubbles)); 658 } 659 660 /** 661 * @return the width of the bubble flyout (message originating from the bubble). 662 */ getMaxFlyoutSize()663 public float getMaxFlyoutSize() { 664 if (isLargeScreen()) { 665 return Math.max(mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN, 666 mMinimumFlyoutWidthLargeScreen); 667 } 668 return mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT; 669 } 670 671 /** 672 * Returns the z translation a specific bubble should use. When expanded we keep a slight 673 * translation to ensure proper ordering when animating to / from collapsed state. When 674 * collapsed, only the top two bubbles appear so only their shadows show. 675 */ getZTranslation(int index, boolean isOverflow, boolean isExpanded)676 public float getZTranslation(int index, boolean isOverflow, boolean isExpanded) { 677 if (isOverflow) { 678 return 0f; // overflow is lowest 679 } 680 return isExpanded 681 // When expanded use minimal amount to keep order 682 ? getMaxBubbles() - index 683 // When collapsed, only the top two bubbles have elevation 684 : index < NUM_VISIBLE_WHEN_RESTING 685 ? (getMaxBubbles() * mBubbleElevation) - index 686 : 0; 687 } 688 689 /** The elevation to use for bubble UI elements. */ getBubbleElevation()690 public int getBubbleElevation() { 691 return mBubbleElevation; 692 } 693 694 /** 695 * @return whether the stack is considered on the left side of the screen. 696 */ isStackOnLeft(PointF currentStackPosition)697 public boolean isStackOnLeft(PointF currentStackPosition) { 698 if (currentStackPosition == null) { 699 currentStackPosition = getRestingPosition(); 700 } 701 final int stackCenter = (int) currentStackPosition.x + mBubbleSize / 2; 702 return stackCenter < mScreenRect.width() / 2; 703 } 704 705 /** 706 * Sets the stack's most recent position along the edge of the screen. This is saved when the 707 * last bubble is removed, so that the stack can be restored in its previous position. 708 */ setRestingPosition(PointF position)709 public void setRestingPosition(PointF position) { 710 if (mRestingStackPosition == null) { 711 mRestingStackPosition = new PointF(position); 712 } else { 713 mRestingStackPosition.set(position); 714 } 715 } 716 717 /** The position the bubble stack should rest at when collapsed. */ getRestingPosition()718 public PointF getRestingPosition() { 719 if (mRestingStackPosition == null) { 720 return getDefaultStartPosition(); 721 } 722 return mRestingStackPosition; 723 } 724 725 /** 726 * Returns whether the {@link #getRestingPosition()} is equal to the default start position 727 * initialized for bubbles, if {@code true} this means the user hasn't moved the bubble 728 * from the initial start position (or they haven't received a bubble yet). 729 */ hasUserModifiedDefaultPosition()730 public boolean hasUserModifiedDefaultPosition() { 731 PointF defaultStart = getDefaultStartPosition(); 732 return mRestingStackPosition != null 733 && !mRestingStackPosition.equals(defaultStart); 734 } 735 736 /** 737 * Returns the stack position to use if we don't have a saved location or if user education 738 * is being shown, for a normal bubble. 739 */ getDefaultStartPosition()740 public PointF getDefaultStartPosition() { 741 return getDefaultStartPosition(false /* isAppBubble */); 742 } 743 744 /** 745 * The stack position to use if we don't have a saved location or if user education 746 * is being shown. 747 * 748 * @param isAppBubble whether this start position is for an app bubble or not. 749 */ getDefaultStartPosition(boolean isAppBubble)750 public PointF getDefaultStartPosition(boolean isAppBubble) { 751 // Normal bubbles start on the left if we're in LTR, right otherwise. 752 // TODO (b/294284894): update language around "app bubble" here 753 // App bubbles start on the right in RTL, left otherwise. 754 final boolean startOnLeft = isAppBubble ? mDeviceConfig.isRtl() : !mDeviceConfig.isRtl(); 755 return getStartPosition(startOnLeft ? StackPinnedEdge.LEFT : StackPinnedEdge.RIGHT); 756 } 757 758 /** 759 * The stack position to use if user education is being shown. 760 * 761 * @param stackPinnedEdge the screen edge the stack is pinned to. 762 */ getStartPosition(StackPinnedEdge stackPinnedEdge)763 public PointF getStartPosition(StackPinnedEdge stackPinnedEdge) { 764 final RectF allowableStackPositionRegion = getAllowableStackPositionRegion( 765 1 /* default starts with 1 bubble */); 766 if (isLargeScreen()) { 767 // We want the stack to be visually centered on the edge, so we need to base it 768 // of a rect that includes insets. 769 final float desiredY = mScreenRect.height() / 2f - (mBubbleSize / 2f); 770 final float offset = desiredY / mScreenRect.height(); 771 return new BubbleStackView.RelativeStackPosition( 772 stackPinnedEdge == StackPinnedEdge.LEFT, 773 offset) 774 .getAbsolutePositionInRegion(allowableStackPositionRegion); 775 } else { 776 final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset( 777 R.dimen.bubble_stack_starting_offset_y); 778 // TODO: placement bug here because mPositionRect doesn't handle the overhanging edge 779 return new BubbleStackView.RelativeStackPosition( 780 stackPinnedEdge == StackPinnedEdge.LEFT, 781 startingVerticalOffset / mPositionRect.height()) 782 .getAbsolutePositionInRegion(allowableStackPositionRegion); 783 } 784 } 785 786 /** 787 * Returns the region that the stack position must stay within. This goes slightly off the left 788 * and right sides of the screen, below the status bar/cutout and above the navigation bar. 789 * While the stack position is not allowed to rest outside of these bounds, it can temporarily 790 * be animated or dragged beyond them. 791 */ getAllowableStackPositionRegion(int bubbleCount)792 public RectF getAllowableStackPositionRegion(int bubbleCount) { 793 final RectF allowableRegion = new RectF(getAvailableRect()); 794 final int imeHeight = getImeHeight(); 795 final float bottomPadding = bubbleCount > 1 796 ? mBubblePaddingTop + mStackOffset 797 : mBubblePaddingTop; 798 allowableRegion.left -= mBubbleOffscreenAmount; 799 allowableRegion.top += mBubblePaddingTop; 800 allowableRegion.right += mBubbleOffscreenAmount - mBubbleSize; 801 allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize; 802 return allowableRegion; 803 } 804 805 /** 806 * Navigation bar has an area where system gestures can be started from. 807 * 808 * @return {@link Rect} for system navigation bar gesture zone 809 */ getNavBarGestureZone()810 public Rect getNavBarGestureZone() { 811 // Gesture zone height from the bottom 812 int gestureZoneHeight = mContext.getResources().getDimensionPixelSize( 813 com.android.internal.R.dimen.navigation_bar_gesture_height); 814 Rect screen = getScreenRect(); 815 return new Rect( 816 screen.left, 817 screen.bottom - gestureZoneHeight, 818 screen.right, 819 screen.bottom); 820 } 821 822 // 823 // Bubble bar specific sizes below. 824 // 825 826 /** 827 * Sets whether bubbles are showing in the bubble bar from launcher. 828 */ setShowingInBubbleBar(boolean showingInBubbleBar)829 public void setShowingInBubbleBar(boolean showingInBubbleBar) { 830 mShowingInBubbleBar = showingInBubbleBar; 831 } 832 setBubbleBarLocation(BubbleBarLocation location)833 public void setBubbleBarLocation(BubbleBarLocation location) { 834 mBubbleBarLocation = location; 835 } 836 getBubbleBarLocation()837 public BubbleBarLocation getBubbleBarLocation() { 838 return mBubbleBarLocation; 839 } 840 841 /** 842 * @return <code>true</code> when bubble bar is on the left and <code>false</code> when on right 843 */ isBubbleBarOnLeft()844 public boolean isBubbleBarOnLeft() { 845 return mBubbleBarLocation.isOnLeft(mDeviceConfig.isRtl()); 846 } 847 848 /** 849 * Set top coordinate of bubble bar on screen 850 */ setBubbleBarTopOnScreen(int topOnScreen)851 public void setBubbleBarTopOnScreen(int topOnScreen) { 852 mBubbleBarTopOnScreen = topOnScreen; 853 } 854 855 /** 856 * Returns the top coordinate of bubble bar on screen 857 */ getBubbleBarTopOnScreen()858 public int getBubbleBarTopOnScreen() { 859 return mBubbleBarTopOnScreen; 860 } 861 862 /** 863 * How wide the expanded view should be when showing from the bubble bar. 864 */ getExpandedViewWidthForBubbleBar(boolean isOverflow)865 public int getExpandedViewWidthForBubbleBar(boolean isOverflow) { 866 return isOverflow ? mOverflowWidth : mExpandedViewLargeScreenWidth; 867 } 868 869 /** 870 * How tall the expanded view should be when showing from the bubble bar. 871 */ getExpandedViewHeightForBubbleBar(boolean isOverflow)872 public int getExpandedViewHeightForBubbleBar(boolean isOverflow) { 873 if (isOverflow) { 874 return mOverflowHeight; 875 } else { 876 return getBubbleBarExpandedViewHeightForLandscape(); 877 } 878 } 879 880 /** 881 * Calculate the height of expanded view in landscape mode regardless current orientation. 882 * Here is an explanation: 883 * ------------------------ mScreenRect.top 884 * | top inset ↕ | 885 * |----------------------- 886 * | 16dp spacing ↕ | 887 * | --------- | --- expanded view top 888 * | | | | ↑ 889 * | | | | ↓ expanded view height 890 * | --------- | --- expanded view bottom 891 * | 16dp spacing ↕ | ↑ 892 * | @bubble bar@ | | height of the bubble bar container 893 * ------------------------ | already includes bottom inset and spacing 894 * | bottom inset ↕ | ↓ 895 * |----------------------| --- mScreenRect.bottom 896 */ getBubbleBarExpandedViewHeightForLandscape()897 private int getBubbleBarExpandedViewHeightForLandscape() { 898 int heightOfBubbleBarContainer = 899 mScreenRect.height() - getExpandedViewBottomForBubbleBar(); 900 // getting landscape height from screen rect 901 int expandedViewHeight = Math.min(mScreenRect.width(), mScreenRect.height()); 902 expandedViewHeight -= heightOfBubbleBarContainer; /* removing bubble container height */ 903 expandedViewHeight -= mInsets.top; /* removing top inset */ 904 expandedViewHeight -= mExpandedViewPadding; /* removing spacing */ 905 return expandedViewHeight; 906 } 907 908 909 /** The bottom position of the expanded view when showing above the bubble bar. */ getExpandedViewBottomForBubbleBar()910 public int getExpandedViewBottomForBubbleBar() { 911 return mBubbleBarTopOnScreen - mExpandedViewPadding; 912 } 913 914 /** 915 * The amount of padding from the edge of the screen to the expanded view when in bubble bar. 916 */ getBubbleBarExpandedViewPadding()917 public int getBubbleBarExpandedViewPadding() { 918 return mExpandedViewPadding; 919 } 920 921 /** 922 * Get bubble bar expanded view bounds on screen 923 */ getBubbleBarExpandedViewBounds(boolean onLeft, boolean isOverflowExpanded, Rect out)924 public void getBubbleBarExpandedViewBounds(boolean onLeft, boolean isOverflowExpanded, 925 Rect out) { 926 final int padding = getBubbleBarExpandedViewPadding(); 927 final int width = getExpandedViewWidthForBubbleBar(isOverflowExpanded); 928 final int height = getExpandedViewHeightForBubbleBar(isOverflowExpanded); 929 930 out.set(0, 0, width, height); 931 int left; 932 if (onLeft) { 933 left = getInsets().left + padding; 934 } else { 935 left = getAvailableRect().right - width - padding; 936 } 937 int top = getExpandedViewBottomForBubbleBar() - height; 938 out.offsetTo(left, top); 939 } 940 } 941