1 /*
2  * Copyright (C) 2019 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.systemui.bubbles.animation;
18 
19 import android.content.res.Configuration;
20 import android.content.res.Resources;
21 import android.graphics.Path;
22 import android.graphics.Point;
23 import android.graphics.PointF;
24 import android.view.DisplayCutout;
25 import android.view.View;
26 import android.view.WindowInsets;
27 
28 import androidx.annotation.NonNull;
29 import androidx.annotation.Nullable;
30 import androidx.dynamicanimation.animation.DynamicAnimation;
31 import androidx.dynamicanimation.animation.SpringForce;
32 
33 import com.android.systemui.Interpolators;
34 import com.android.systemui.R;
35 import com.android.systemui.util.animation.PhysicsAnimator;
36 import com.android.systemui.util.magnetictarget.MagnetizedObject;
37 
38 import com.google.android.collect.Sets;
39 
40 import java.io.FileDescriptor;
41 import java.io.PrintWriter;
42 import java.util.Set;
43 
44 /**
45  * Animation controller for bubbles when they're in their expanded state, or animating to/from the
46  * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be
47  * dismissed.
48  */
49 public class ExpandedAnimationController
50         extends PhysicsAnimationLayout.PhysicsAnimationController {
51 
52     /**
53      * How much to translate the bubbles when they're animating in/out. This value is multiplied by
54      * the bubble size.
55      */
56     private static final int ANIMATE_TRANSLATION_FACTOR = 4;
57 
58     /** Duration of the expand/collapse target path animation. */
59     public static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175;
60 
61     /** Stiffness for the expand/collapse path-following animation. */
62     private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
63 
64     /** What percentage of the screen to use when centering the bubbles in landscape. */
65     private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
66 
67     /**
68      * Velocity required to dismiss an individual bubble without dragging it into the dismiss
69      * target.
70      */
71     private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
72 
73     private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig =
74             new PhysicsAnimator.SpringConfig(
75                     EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY);
76 
77     /** Horizontal offset between bubbles, which we need to know to re-stack them. */
78     private float mStackOffsetPx;
79     /** Space between status bar and bubbles in the expanded state. */
80     private float mBubblePaddingTop;
81     /** Size of each bubble. */
82     private float mBubbleSizePx;
83     /** Space between bubbles in row above expanded view. */
84     private float mSpaceBetweenBubbles;
85     /** Height of the status bar. */
86     private float mStatusBarHeight;
87     /** Size of display. */
88     private Point mDisplaySize;
89     /** Max number of bubbles shown in row above expanded view. */
90     private int mBubblesMaxRendered;
91     /** What the current screen orientation is. */
92     private int mScreenOrientation;
93 
94     private boolean mAnimatingExpand = false;
95 
96     /**
97      * Whether we are animating other Bubbles UI elements out in preparation for a call to
98      * {@link #collapseBackToStack}. If true, we won't animate bubbles in response to adds or
99      * reorders.
100      */
101     private boolean mPreparingToCollapse = false;
102 
103     private boolean mAnimatingCollapse = false;
104     private @Nullable Runnable mAfterExpand;
105     private Runnable mAfterCollapse;
106     private PointF mCollapsePoint;
107 
108     /**
109      * Whether the dragged out bubble is springing towards the touch point, rather than using the
110      * default behavior of moving directly to the touch point.
111      *
112      * This happens when the user's finger exits the dismiss area while the bubble is magnetized to
113      * the center. Since the touch point differs from the bubble location, we need to animate the
114      * bubble back to the touch point to avoid a jarring instant location change from the center of
115      * the target to the touch point just outside the target bounds.
116      */
117     private boolean mSpringingBubbleToTouch = false;
118 
119     /**
120      * Whether to spring the bubble to the next touch event coordinates. This is used to animate the
121      * bubble out of the magnetic dismiss target to the touch location.
122      *
123      * Once it 'catches up' and the animation ends, we'll revert to moving it directly.
124      */
125     private boolean mSpringToTouchOnNextMotionEvent = false;
126 
127     /** The bubble currently being dragged out of the row (to potentially be dismissed). */
128     private MagnetizedObject<View> mMagnetizedBubbleDraggingOut;
129 
130     private int mExpandedViewPadding;
131 
132     /**
133      * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the
134      * end of this animation means we have no bubbles left, and notify the BubbleController.
135      */
136     private Runnable mOnBubbleAnimatedOutAction;
137 
ExpandedAnimationController(Point displaySize, int expandedViewPadding, int orientation, Runnable onBubbleAnimatedOutAction)138     public ExpandedAnimationController(Point displaySize, int expandedViewPadding,
139             int orientation, Runnable onBubbleAnimatedOutAction) {
140         updateResources(orientation, displaySize);
141         mExpandedViewPadding = expandedViewPadding;
142         mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction;
143     }
144 
145     /**
146      * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
147      * the rest of the bubbles to animate to fill the gap.
148      */
149     private boolean mBubbleDraggedOutEnough = false;
150 
151     /** End action to run when the lead bubble's expansion animation completes. */
152     @Nullable private Runnable mLeadBubbleEndAction;
153 
154     /**
155      * Animates expanding the bubbles into a row along the top of the screen, optionally running an
156      * end action when the entire animation completes, and an end action when the lead bubble's
157      * animation ends.
158      */
expandFromStack( @ullable Runnable after, @Nullable Runnable leadBubbleEndAction)159     public void expandFromStack(
160             @Nullable Runnable after, @Nullable Runnable leadBubbleEndAction) {
161         mPreparingToCollapse = false;
162         mAnimatingCollapse = false;
163         mAnimatingExpand = true;
164         mAfterExpand = after;
165         mLeadBubbleEndAction = leadBubbleEndAction;
166 
167         startOrUpdatePathAnimation(true /* expanding */);
168     }
169 
170     /**
171      * Animates expanding the bubbles into a row along the top of the screen.
172      */
expandFromStack(@ullable Runnable after)173     public void expandFromStack(@Nullable Runnable after) {
174         expandFromStack(after, null /* leadBubbleEndAction */);
175     }
176 
177     /**
178      * Sets that we're animating the stack collapsed, but haven't yet called
179      * {@link #collapseBackToStack}. This will temporarily suspend animations for bubbles that are
180      * added or re-ordered, since the upcoming collapse animation will handle positioning those
181      * bubbles in the collapsed stack.
182      */
notifyPreparingToCollapse()183     public void notifyPreparingToCollapse() {
184         mPreparingToCollapse = true;
185     }
186 
187     /** Animate collapsing the bubbles back to their stacked position. */
collapseBackToStack(PointF collapsePoint, Runnable after)188     public void collapseBackToStack(PointF collapsePoint, Runnable after) {
189         mAnimatingExpand = false;
190         mPreparingToCollapse = false;
191         mAnimatingCollapse = true;
192         mAfterCollapse = after;
193         mCollapsePoint = collapsePoint;
194 
195         startOrUpdatePathAnimation(false /* expanding */);
196     }
197 
198     /**
199      * Update effective screen width based on current orientation.
200      * @param orientation Landscape or portrait.
201      * @param displaySize Updated display size.
202      */
updateResources(int orientation, Point displaySize)203     public void updateResources(int orientation, Point displaySize) {
204         mScreenOrientation = orientation;
205         mDisplaySize = displaySize;
206         if (mLayout == null) {
207             return;
208         }
209         Resources res = mLayout.getContext().getResources();
210         mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
211         mStatusBarHeight = res.getDimensionPixelSize(
212                 com.android.internal.R.dimen.status_bar_height);
213         mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
214         mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
215         mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
216         mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
217 
218         // Includes overflow button.
219         float totalGapWidth = getWidthForDisplayingBubbles() - (mExpandedViewPadding * 2)
220                 - (mBubblesMaxRendered + 1) * mBubbleSizePx;
221         mSpaceBetweenBubbles = totalGapWidth / mBubblesMaxRendered;
222     }
223 
224     /**
225      * Animates the bubbles along a curved path, either to expand them along the top or collapse
226      * them back into a stack.
227      */
startOrUpdatePathAnimation(boolean expanding)228     private void startOrUpdatePathAnimation(boolean expanding) {
229         Runnable after;
230 
231         if (expanding) {
232             after = () -> {
233                 mAnimatingExpand = false;
234 
235                 if (mAfterExpand != null) {
236                     mAfterExpand.run();
237                 }
238 
239                 mAfterExpand = null;
240 
241                 // Update bubble positions in case any bubbles were added or removed during the
242                 // expansion animation.
243                 updateBubblePositions();
244             };
245         } else {
246             after = () -> {
247                 mAnimatingCollapse = false;
248 
249                 if (mAfterCollapse != null) {
250                     mAfterCollapse.run();
251                 }
252 
253                 mAfterCollapse = null;
254             };
255         }
256 
257         // Animate each bubble individually, since each path will end in a different spot.
258         animationsForChildrenFromIndex(0, (index, animation) -> {
259             final View bubble = mLayout.getChildAt(index);
260 
261             // Start a path at the bubble's current position.
262             final Path path = new Path();
263             path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
264 
265             final float expandedY = getExpandedY();
266             if (expanding) {
267                 // If we're expanding, first draw a line from the bubble's current position to the
268                 // top of the screen.
269                 path.lineTo(bubble.getTranslationX(), expandedY);
270 
271                 // Then, draw a line across the screen to the bubble's resting position.
272                 path.lineTo(getBubbleLeft(index), expandedY);
273             } else {
274                 final float sideMultiplier =
275                         mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
276                 final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx);
277 
278                 // If we're collapsing, draw a line from the bubble's current position to the side
279                 // of the screen where the bubble will be stacked.
280                 path.lineTo(stackedX, expandedY);
281 
282                 // Then, draw a line down to the stack position.
283                 path.lineTo(stackedX, mCollapsePoint.y);
284             }
285 
286             // The lead bubble should be the bubble with the longest distance to travel when we're
287             // expanding, and the bubble with the shortest distance to travel when we're collapsing.
288             // During expansion from the left side, the last bubble has to travel to the far right
289             // side, so we have it lead and 'pull' the rest of the bubbles into place. From the
290             // right side, the first bubble is traveling to the top left, so it leads. During
291             // collapse to the left, the first bubble has the shortest travel time back to the stack
292             // position, so it leads (and vice versa).
293             final boolean firstBubbleLeads =
294                     (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
295                             || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
296             final int startDelay = firstBubbleLeads
297                     ? (index * 10)
298                     : ((mLayout.getChildCount() - index) * 10);
299 
300             final boolean isLeadBubble =
301                     (firstBubbleLeads && index == 0)
302                             || (!firstBubbleLeads && index == mLayout.getChildCount() - 1);
303 
304             animation
305                     .followAnimatedTargetAlongPath(
306                             path,
307                             EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
308                             Interpolators.LINEAR /* targetAnimInterpolator */,
309                             isLeadBubble ? mLeadBubbleEndAction : null /* endAction */,
310                             () -> mLeadBubbleEndAction = null /* endAction */)
311                     .withStartDelay(startDelay)
312                     .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS);
313         }).startAll(after);
314     }
315 
316     /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */
onUnstuckFromTarget()317     public void onUnstuckFromTarget() {
318         mSpringToTouchOnNextMotionEvent = true;
319     }
320 
321     /**
322      * Prepares the given bubble view to be dragged out, using the provided magnetic target and
323      * listener.
324      */
prepareForBubbleDrag( View bubble, MagnetizedObject.MagneticTarget target, MagnetizedObject.MagnetListener listener)325     public void prepareForBubbleDrag(
326             View bubble,
327             MagnetizedObject.MagneticTarget target,
328             MagnetizedObject.MagnetListener listener) {
329         mLayout.cancelAnimationsOnView(bubble);
330 
331         bubble.setTranslationZ(Short.MAX_VALUE);
332         mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>(
333                 mLayout.getContext(), bubble,
334                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
335             @Override
336             public float getWidth(@NonNull View underlyingObject) {
337                 return mBubbleSizePx;
338             }
339 
340             @Override
341             public float getHeight(@NonNull View underlyingObject) {
342                 return mBubbleSizePx;
343             }
344 
345             @Override
346             public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
347                 loc[0] = (int) bubble.getTranslationX();
348                 loc[1] = (int) bubble.getTranslationY();
349             }
350         };
351         mMagnetizedBubbleDraggingOut.addTarget(target);
352         mMagnetizedBubbleDraggingOut.setMagnetListener(listener);
353         mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
354         mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
355     }
356 
springBubbleTo(View bubble, float x, float y)357     private void springBubbleTo(View bubble, float x, float y) {
358         animationForChild(bubble)
359                 .translationX(x)
360                 .translationY(y)
361                 .withStiffness(SpringForce.STIFFNESS_HIGH)
362                 .start();
363     }
364 
365     /**
366      * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
367      * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
368      * bubble is dragged back into the row.
369      */
dragBubbleOut(View bubbleView, float x, float y)370     public void dragBubbleOut(View bubbleView, float x, float y) {
371         if (mSpringToTouchOnNextMotionEvent) {
372             springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
373             mSpringToTouchOnNextMotionEvent = false;
374             mSpringingBubbleToTouch = true;
375         } else if (mSpringingBubbleToTouch) {
376             if (mLayout.arePropertiesAnimatingOnView(
377                     bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
378                 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
379             } else {
380                 mSpringingBubbleToTouch = false;
381             }
382         }
383 
384         if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) {
385             bubbleView.setTranslationX(x);
386             bubbleView.setTranslationY(y);
387         }
388 
389         final boolean draggedOutEnough =
390                 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
391         if (draggedOutEnough != mBubbleDraggedOutEnough) {
392             updateBubblePositions();
393             mBubbleDraggedOutEnough = draggedOutEnough;
394         }
395     }
396 
397     /** Plays a dismiss animation on the dragged out bubble. */
398     public void dismissDraggedOutBubble(View bubble, float translationYBy, Runnable after) {
399         if (bubble == null) {
400             return;
401         }
402         animationForChild(bubble)
403                 .withStiffness(SpringForce.STIFFNESS_HIGH)
404                 .scaleX(0f)
405                 .scaleY(0f)
406                 .translationY(bubble.getTranslationY() + translationYBy)
407                 .alpha(0f, after)
408                 .start();
409 
410         updateBubblePositions();
411     }
412 
413     @Nullable public View getDraggedOutBubble() {
414         return mMagnetizedBubbleDraggingOut == null
415                 ? null
416                 : mMagnetizedBubbleDraggingOut.getUnderlyingObject();
417     }
418 
419     /** Returns the MagnetizedObject instance for the dragging-out bubble. */
420     public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() {
421         return mMagnetizedBubbleDraggingOut;
422     }
423 
424     /**
425      * Snaps a bubble back to its position within the bubble row, and animates the rest of the
426      * bubbles to accommodate it if it was previously dragged out past the threshold.
427      */
428     public void snapBubbleBack(View bubbleView, float velX, float velY) {
429         final int index = mLayout.indexOfChild(bubbleView);
430 
431         animationForChildAtIndex(index)
432                 .position(getBubbleLeft(index), getExpandedY())
433                 .withPositionStartVelocities(velX, velY)
434                 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
435 
436         mMagnetizedBubbleDraggingOut = null;
437 
438         updateBubblePositions();
439     }
440 
441     /** Resets bubble drag out gesture flags. */
onGestureFinished()442     public void onGestureFinished() {
443         mBubbleDraggedOutEnough = false;
444         mMagnetizedBubbleDraggingOut = null;
445         updateBubblePositions();
446     }
447 
448     /**
449      * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
450      */
updateYPosition(Runnable after)451     public void updateYPosition(Runnable after) {
452         if (mLayout == null) return;
453         animationsForChildrenFromIndex(
454                 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
455     }
456 
457     /** The Y value of the row of expanded bubbles. */
getExpandedY()458     public float getExpandedY() {
459         if (mLayout == null || mLayout.getRootWindowInsets() == null) {
460             return 0;
461         }
462         final WindowInsets insets = mLayout.getRootWindowInsets();
463         return mBubblePaddingTop + Math.max(
464                 mStatusBarHeight,
465                 insets.getDisplayCutout() != null
466                         ? insets.getDisplayCutout().getSafeInsetTop()
467                         : 0);
468     }
469 
470     /** Description of current animation controller state. */
dump(FileDescriptor fd, PrintWriter pw, String[] args)471     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
472         pw.println("ExpandedAnimationController state:");
473         pw.print("  isActive:          "); pw.println(isActiveController());
474         pw.print("  animatingExpand:   "); pw.println(mAnimatingExpand);
475         pw.print("  animatingCollapse: "); pw.println(mAnimatingCollapse);
476         pw.print("  springingBubble:   "); pw.println(mSpringingBubbleToTouch);
477     }
478 
479     @Override
onActiveControllerForLayout(PhysicsAnimationLayout layout)480     void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
481         updateResources(mScreenOrientation, mDisplaySize);
482 
483         // Ensure that all child views are at 1x scale, and visible, in case they were animating
484         // in.
485         mLayout.setVisibility(View.VISIBLE);
486         animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
487                 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
488     }
489 
490     @Override
getAnimatedProperties()491     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
492         return Sets.newHashSet(
493                 DynamicAnimation.TRANSLATION_X,
494                 DynamicAnimation.TRANSLATION_Y,
495                 DynamicAnimation.SCALE_X,
496                 DynamicAnimation.SCALE_Y,
497                 DynamicAnimation.ALPHA);
498     }
499 
500     @Override
getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)501     int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
502         return NONE;
503     }
504 
505     @Override
getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property)506     float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
507         return 0;
508     }
509 
510     @Override
getSpringForce(DynamicAnimation.ViewProperty property, View view)511     SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
512         return new SpringForce()
513                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
514                 .setStiffness(SpringForce.STIFFNESS_LOW);
515     }
516 
517     @Override
onChildAdded(View child, int index)518     void onChildAdded(View child, int index) {
519         // If a bubble is added while the expand/collapse animations are playing, update the
520         // animation to include the new bubble.
521         if (mAnimatingExpand) {
522             startOrUpdatePathAnimation(true /* expanding */);
523         } else if (mAnimatingCollapse) {
524             startOrUpdatePathAnimation(false /* expanding */);
525         } else {
526             child.setTranslationX(getBubbleLeft(index));
527 
528             // If we're preparing to collapse, don't start animations since the collapse animation
529             // will take over and animate the new bubble into the correct (stacked) position.
530             if (!mPreparingToCollapse) {
531                 animationForChild(child)
532                         .translationY(
533                                 getExpandedY()
534                                         - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
535                                 getExpandedY() /* to */)
536                         .start();
537                 updateBubblePositions();
538             }
539         }
540     }
541 
542     @Override
onChildRemoved(View child, int index, Runnable finishRemoval)543     void onChildRemoved(View child, int index, Runnable finishRemoval) {
544         // If we're removing the dragged-out bubble, that means it got dismissed.
545         if (child.equals(getDraggedOutBubble())) {
546             mMagnetizedBubbleDraggingOut = null;
547             finishRemoval.run();
548             mOnBubbleAnimatedOutAction.run();
549         } else {
550             PhysicsAnimator.getInstance(child)
551                     .spring(DynamicAnimation.ALPHA, 0f)
552                     .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig)
553                     .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig)
554                     .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction)
555                     .start();
556         }
557 
558         // Animate all the other bubbles to their new positions sans this bubble.
559         updateBubblePositions();
560     }
561 
562     @Override
onChildReordered(View child, int oldIndex, int newIndex)563     void onChildReordered(View child, int oldIndex, int newIndex) {
564         if (mPreparingToCollapse) {
565             // If a re-order is received while we're preparing to collapse, ignore it. Once started,
566             // the collapse animation will animate all of the bubbles to their correct (stacked)
567             // position.
568             return;
569         }
570 
571         if (mAnimatingCollapse) {
572             // If a re-order is received during collapse, update the animation so that the bubbles
573             // end up in the correct (stacked) position.
574             startOrUpdatePathAnimation(false /* expanding */);
575         } else {
576             // Otherwise, animate the bubbles around to reflect their new order.
577             updateBubblePositions();
578         }
579     }
580 
updateBubblePositions()581     private void updateBubblePositions() {
582         if (mAnimatingExpand || mAnimatingCollapse) {
583             return;
584         }
585 
586         for (int i = 0; i < mLayout.getChildCount(); i++) {
587             final View bubble = mLayout.getChildAt(i);
588 
589             // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
590             // will be snapped to the correct X value after the drag (if it's not dismissed).
591             if (bubble.equals(getDraggedOutBubble())) {
592                 return;
593             }
594 
595             animationForChild(bubble)
596                     .translationX(getBubbleLeft(i))
597                     .start();
598         }
599     }
600 
601     /**
602      * @param index Bubble index in row.
603      * @return Bubble left x from left edge of screen.
604      */
getBubbleLeft(int index)605     public float getBubbleLeft(int index) {
606         final float bubbleFromRowLeft = index * (mBubbleSizePx + mSpaceBetweenBubbles);
607         return getRowLeft() + bubbleFromRowLeft;
608     }
609 
610     /**
611      * When expanded, the bubbles are centered in the screen. In portrait, all available space is
612      * used. In landscape we have too much space so the value is restricted. This method accounts
613      * for window decorations (nav bar, cutouts).
614      *
615      * @return the desired width to display the expanded bubbles in.
616      */
getWidthForDisplayingBubbles()617     public float getWidthForDisplayingBubbles() {
618         final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */);
619         if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
620             // display size y in landscape will be the smaller dimension of the screen
621             return Math.max(mDisplaySize.y, availableWidth * CENTER_BUBBLES_LANDSCAPE_PERCENT);
622         } else {
623             return availableWidth;
624         }
625     }
626 
627     /**
628      * Determines the available screen width without the cutout.
629      *
630      * @param subtractStableInsets Whether or not stable insets should also be removed from the
631      *                             returned width.
632      * @return the total screen width available accounting for cutouts and insets,
633      * iff {@param includeStableInsets} is true.
634      */
getAvailableScreenWidth(boolean subtractStableInsets)635     private float getAvailableScreenWidth(boolean subtractStableInsets) {
636         float availableSize = mDisplaySize.x;
637         WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null;
638         if (insets != null) {
639             int cutoutLeft = 0;
640             int cutoutRight = 0;
641             DisplayCutout cutout = insets.getDisplayCutout();
642             if (cutout != null) {
643                 cutoutLeft = cutout.getSafeInsetLeft();
644                 cutoutRight = cutout.getSafeInsetRight();
645             }
646             final int stableLeft = subtractStableInsets ? insets.getStableInsetLeft() : 0;
647             final int stableRight = subtractStableInsets ? insets.getStableInsetRight() : 0;
648             availableSize -= Math.max(stableLeft, cutoutLeft);
649             availableSize -= Math.max(stableRight, cutoutRight);
650         }
651         return availableSize;
652     }
653 
getRowLeft()654     private float getRowLeft() {
655         if (mLayout == null) {
656             return 0;
657         }
658         float rowWidth = (mLayout.getChildCount() * mBubbleSizePx)
659                 + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles);
660 
661         // This display size we're using includes the size of the insets, we want the true
662         // center of the display minus the notch here, which means we should include the
663         // stable insets (e.g. status bar, nav bar) in this calculation.
664         final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f;
665         return trueCenter - (rowWidth / 2f);
666     }
667 }
668