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