• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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