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