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