• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.wm.shell.bubbles.animation;
18 
19 import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
20 
21 import android.content.res.Resources;
22 import android.graphics.Path;
23 import android.graphics.PointF;
24 import android.graphics.Rect;
25 import android.view.View;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 import androidx.dynamicanimation.animation.DynamicAnimation;
30 import androidx.dynamicanimation.animation.SpringForce;
31 
32 import com.android.wm.shell.R;
33 import com.android.wm.shell.animation.Interpolators;
34 import com.android.wm.shell.animation.PhysicsAnimator;
35 import com.android.wm.shell.bubbles.BubblePositioner;
36 import com.android.wm.shell.common.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     /** Damping ratio for expand/collapse spring. */
62     private static final float DAMPING_RATIO_MEDIUM_LOW_BOUNCY = 0.65f;
63 
64     /** Stiffness for the expand/collapse path-following animation. */
65     private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
66 
67     /** What percentage of the screen to use when centering the bubbles in landscape. */
68     private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
69 
70     /**
71      * Velocity required to dismiss an individual bubble without dragging it into the dismiss
72      * target.
73      */
74     private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
75 
76     private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig =
77             new PhysicsAnimator.SpringConfig(
78                     EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY);
79 
80     /** Horizontal offset between bubbles, which we need to know to re-stack them. */
81     private float mStackOffsetPx;
82     /** Space between status bar and bubbles in the expanded state. */
83     private float mBubblePaddingTop;
84     /** Size of each bubble. */
85     private float mBubbleSizePx;
86     /** Max number of bubbles shown in row above expanded view. */
87     private int mBubblesMaxRendered;
88     /** Max amount of space to have between bubbles when expanded. */
89     private int mBubblesMaxSpace;
90     /** Amount of space between the bubbles when expanded. */
91     private float mSpaceBetweenBubbles;
92     /** Whether the expand / collapse animation is running. */
93     private boolean mAnimatingExpand = false;
94 
95     /**
96      * Whether we are animating other Bubbles UI elements out in preparation for a call to
97      * {@link #collapseBackToStack}. If true, we won't animate bubbles in response to adds or
98      * reorders.
99      */
100     private boolean mPreparingToCollapse = false;
101 
102     private boolean mAnimatingCollapse = false;
103     @Nullable
104     private 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 
138     private BubblePositioner mPositioner;
139 
ExpandedAnimationController(BubblePositioner positioner, int expandedViewPadding, Runnable onBubbleAnimatedOutAction)140     public ExpandedAnimationController(BubblePositioner positioner, int expandedViewPadding,
141             Runnable onBubbleAnimatedOutAction) {
142         mPositioner = positioner;
143         updateResources();
144         mExpandedViewPadding = expandedViewPadding;
145         mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction;
146         mCollapsePoint = mPositioner.getDefaultStartPosition();
147     }
148 
149     /**
150      * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
151      * the rest of the bubbles to animate to fill the gap.
152      */
153     private boolean mBubbleDraggedOutEnough = false;
154 
155     /** End action to run when the lead bubble's expansion animation completes. */
156     @Nullable
157     private Runnable mLeadBubbleEndAction;
158 
159     /**
160      * Animates expanding the bubbles into a row along the top of the screen, optionally running an
161      * end action when the entire animation completes, and an end action when the lead bubble's
162      * animation ends.
163      */
expandFromStack( @ullable Runnable after, @Nullable Runnable leadBubbleEndAction)164     public void expandFromStack(
165             @Nullable Runnable after, @Nullable Runnable leadBubbleEndAction) {
166         mPreparingToCollapse = false;
167         mAnimatingCollapse = false;
168         mAnimatingExpand = true;
169         mAfterExpand = after;
170         mLeadBubbleEndAction = leadBubbleEndAction;
171 
172         startOrUpdatePathAnimation(true /* expanding */);
173     }
174 
175     /**
176      * Animates expanding the bubbles into a row along the top of the screen.
177      */
expandFromStack(@ullable Runnable after)178     public void expandFromStack(@Nullable Runnable after) {
179         expandFromStack(after, null /* leadBubbleEndAction */);
180     }
181 
182     /**
183      * Sets that we're animating the stack collapsed, but haven't yet called
184      * {@link #collapseBackToStack}. This will temporarily suspend animations for bubbles that are
185      * added or re-ordered, since the upcoming collapse animation will handle positioning those
186      * bubbles in the collapsed stack.
187      */
notifyPreparingToCollapse()188     public void notifyPreparingToCollapse() {
189         mPreparingToCollapse = true;
190     }
191 
192     /** Animate collapsing the bubbles back to their stacked position. */
collapseBackToStack(PointF collapsePoint, Runnable after)193     public void collapseBackToStack(PointF collapsePoint, Runnable after) {
194         mAnimatingExpand = false;
195         mPreparingToCollapse = false;
196         mAnimatingCollapse = true;
197         mAfterCollapse = after;
198         mCollapsePoint = collapsePoint;
199 
200         startOrUpdatePathAnimation(false /* expanding */);
201     }
202 
203     /**
204      * Update effective screen width based on current orientation.
205      */
updateResources()206     public void updateResources() {
207         if (mLayout == null) {
208             return;
209         }
210         Resources res = mLayout.getContext().getResources();
211         mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
212         mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
213         mBubbleSizePx = mPositioner.getBubbleSize();
214         mBubblesMaxRendered = mPositioner.getMaxBubbles();
215         mSpaceBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
216     }
217 
218     /**
219      * Animates the bubbles along a curved path, either to expand them along the top or collapse
220      * them back into a stack.
221      */
startOrUpdatePathAnimation(boolean expanding)222     private void startOrUpdatePathAnimation(boolean expanding) {
223         Runnable after;
224 
225         if (expanding) {
226             after = () -> {
227                 mAnimatingExpand = false;
228 
229                 if (mAfterExpand != null) {
230                     mAfterExpand.run();
231                 }
232 
233                 mAfterExpand = null;
234 
235                 // Update bubble positions in case any bubbles were added or removed during the
236                 // expansion animation.
237                 updateBubblePositions();
238             };
239         } else {
240             after = () -> {
241                 mAnimatingCollapse = false;
242 
243                 if (mAfterCollapse != null) {
244                     mAfterCollapse.run();
245                 }
246 
247                 mAfterCollapse = null;
248             };
249         }
250 
251         // Animate each bubble individually, since each path will end in a different spot.
252         animationsForChildrenFromIndex(0, (index, animation) -> {
253             final View bubble = mLayout.getChildAt(index);
254 
255             // Start a path at the bubble's current position.
256             final Path path = new Path();
257             path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
258 
259             final float expandedY = mPositioner.showBubblesVertically()
260                     ? getBubbleXOrYForOrientation(index)
261                     : getExpandedY();
262             if (expanding) {
263                 // If we're expanding, first draw a line from the bubble's current position to the
264                 // top of the screen.
265                 path.lineTo(bubble.getTranslationX(), expandedY);
266                 // Then, draw a line across the screen to the bubble's resting position.
267                 if (mPositioner.showBubblesVertically()) {
268                     Rect availableRect = mPositioner.getAvailableRect();
269                     boolean onLeft = mCollapsePoint != null
270                             && mCollapsePoint.x < (availableRect.width() / 2f);
271                     float translationX = onLeft
272                             ? availableRect.left
273                             : availableRect.right - mBubbleSizePx;
274                     path.lineTo(translationX, getBubbleXOrYForOrientation(index));
275                 } else {
276                     path.lineTo(getBubbleXOrYForOrientation(index), expandedY);
277                 }
278             } else {
279                 final float stackedX = mCollapsePoint.x;
280 
281                 // If we're collapsing, draw a line from the bubble's current position to the side
282                 // of the screen where the bubble will be stacked.
283                 path.lineTo(stackedX, expandedY);
284 
285                 // Then, draw a line down to the stack position.
286                 path.lineTo(stackedX, mCollapsePoint.y
287                         + Math.min(index, NUM_VISIBLE_WHEN_RESTING - 1) * mStackOffsetPx);
288             }
289 
290             // The lead bubble should be the bubble with the longest distance to travel when we're
291             // expanding, and the bubble with the shortest distance to travel when we're collapsing.
292             // During expansion from the left side, the last bubble has to travel to the far right
293             // side, so we have it lead and 'pull' the rest of the bubbles into place. From the
294             // right side, the first bubble is traveling to the top left, so it leads. During
295             // collapse to the left, the first bubble has the shortest travel time back to the stack
296             // position, so it leads (and vice versa).
297             final boolean firstBubbleLeads =
298                     (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
299                             || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
300             final int startDelay = firstBubbleLeads
301                     ? (index * 10)
302                     : ((mLayout.getChildCount() - index) * 10);
303 
304             final boolean isLeadBubble =
305                     (firstBubbleLeads && index == 0)
306                             || (!firstBubbleLeads && index == mLayout.getChildCount() - 1);
307 
308             animation
309                     .followAnimatedTargetAlongPath(
310                             path,
311                             EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
312                             Interpolators.LINEAR /* targetAnimInterpolator */,
313                             isLeadBubble ? mLeadBubbleEndAction : null /* endAction */,
314                             () -> mLeadBubbleEndAction = null /* endAction */)
315                     .withStartDelay(startDelay)
316                     .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS);
317         }).startAll(after);
318     }
319 
320     /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */
onUnstuckFromTarget()321     public void onUnstuckFromTarget() {
322         mSpringToTouchOnNextMotionEvent = true;
323     }
324 
325     /**
326      * Prepares the given bubble view to be dragged out, using the provided magnetic target and
327      * listener.
328      */
prepareForBubbleDrag( View bubble, MagnetizedObject.MagneticTarget target, MagnetizedObject.MagnetListener listener)329     public void prepareForBubbleDrag(
330             View bubble,
331             MagnetizedObject.MagneticTarget target,
332             MagnetizedObject.MagnetListener listener) {
333         mLayout.cancelAnimationsOnView(bubble);
334 
335         bubble.setTranslationZ(Short.MAX_VALUE);
336         mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>(
337                 mLayout.getContext(), bubble,
338                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
339             @Override
340             public float getWidth(@NonNull View underlyingObject) {
341                 return mBubbleSizePx;
342             }
343 
344             @Override
345             public float getHeight(@NonNull View underlyingObject) {
346                 return mBubbleSizePx;
347             }
348 
349             @Override
350             public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
351                 loc[0] = (int) bubble.getTranslationX();
352                 loc[1] = (int) bubble.getTranslationY();
353             }
354         };
355         mMagnetizedBubbleDraggingOut.addTarget(target);
356         mMagnetizedBubbleDraggingOut.setMagnetListener(listener);
357         mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
358         mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
359     }
360 
springBubbleTo(View bubble, float x, float y)361     private void springBubbleTo(View bubble, float x, float y) {
362         animationForChild(bubble)
363                 .translationX(x)
364                 .translationY(y)
365                 .withStiffness(SpringForce.STIFFNESS_HIGH)
366                 .start();
367     }
368 
369     /**
370      * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
371      * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
372      * bubble is dragged back into the row.
373      */
dragBubbleOut(View bubbleView, float x, float y)374     public void dragBubbleOut(View bubbleView, float x, float y) {
375         if (mSpringToTouchOnNextMotionEvent) {
376             springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
377             mSpringToTouchOnNextMotionEvent = false;
378             mSpringingBubbleToTouch = true;
379         } else if (mSpringingBubbleToTouch) {
380             if (mLayout.arePropertiesAnimatingOnView(
381                     bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
382                 springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
383             } else {
384                 mSpringingBubbleToTouch = false;
385             }
386         }
387 
388         if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) {
389             bubbleView.setTranslationX(x);
390             bubbleView.setTranslationY(y);
391         }
392 
393         final boolean draggedOutEnough =
394                 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
395         if (draggedOutEnough != mBubbleDraggedOutEnough) {
396             updateBubblePositions();
397             mBubbleDraggedOutEnough = draggedOutEnough;
398         }
399     }
400 
401     /** Plays a dismiss animation on the dragged out bubble. */
402     public void dismissDraggedOutBubble(View bubble, float translationYBy, Runnable after) {
403         if (bubble == null) {
404             return;
405         }
406         animationForChild(bubble)
407                 .withStiffness(SpringForce.STIFFNESS_HIGH)
408                 .scaleX(0f)
409                 .scaleY(0f)
410                 .translationY(bubble.getTranslationY() + translationYBy)
411                 .alpha(0f, after)
412                 .start();
413 
414         updateBubblePositions();
415     }
416 
417     @Nullable
418     public View getDraggedOutBubble() {
419         return mMagnetizedBubbleDraggingOut == null
420                 ? null
421                 : mMagnetizedBubbleDraggingOut.getUnderlyingObject();
422     }
423 
424     /** Returns the MagnetizedObject instance for the dragging-out bubble. */
425     public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() {
426         return mMagnetizedBubbleDraggingOut;
427     }
428 
429     /**
430      * Snaps a bubble back to its position within the bubble row, and animates the rest of the
431      * bubbles to accommodate it if it was previously dragged out past the threshold.
432      */
433     public void snapBubbleBack(View bubbleView, float velX, float velY) {
434         if (mLayout == null) {
435             return;
436         }
437         final int index = mLayout.indexOfChild(bubbleView);
438 
439         animationForChildAtIndex(index)
440                 .position(getBubbleXOrYForOrientation(index), getExpandedY())
441                 .withPositionStartVelocities(velX, velY)
442                 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
443 
444         mMagnetizedBubbleDraggingOut = null;
445 
446         updateBubblePositions();
447     }
448 
449     /** Resets bubble drag out gesture flags. */
onGestureFinished()450     public void onGestureFinished() {
451         mBubbleDraggedOutEnough = false;
452         mMagnetizedBubbleDraggingOut = null;
453         updateBubblePositions();
454     }
455 
456     /**
457      * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
458      */
updateYPosition(Runnable after)459     public void updateYPosition(Runnable after) {
460         if (mLayout == null) return;
461         animationsForChildrenFromIndex(
462                 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
463     }
464 
465     /** The Y value of the row of expanded bubbles. */
getExpandedY()466     public float getExpandedY() {
467         return mPositioner.getAvailableRect().top + mBubblePaddingTop;
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();
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, int index)506     float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index) {
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(DAMPING_RATIO_MEDIUM_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 if (mPositioner.showBubblesVertically()) {
526             child.setTranslationY(getBubbleXOrYForOrientation(index));
527             if (!mPreparingToCollapse) {
528                 // Only animate if we're not collapsing as that animation will handle placing the
529                 // new bubble in the stacked position.
530                 Rect availableRect = mPositioner.getAvailableRect();
531                 boolean onLeft = mCollapsePoint != null
532                         && mCollapsePoint.x < (availableRect.width() / 2f);
533                 float fromX = onLeft
534                         ? -mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR
535                         : availableRect.right + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
536                 float toX = onLeft
537                         ? availableRect.left + mExpandedViewPadding
538                         : availableRect.right - mBubbleSizePx - mExpandedViewPadding;
539                 animationForChild(child)
540                         .translationX(fromX, toX)
541                         .start();
542                 updateBubblePositions();
543             }
544         } else {
545             child.setTranslationX(getBubbleXOrYForOrientation(index));
546             if (!mPreparingToCollapse) {
547                 // Only animate if we're not collapsing as that animation will handle placing the
548                 // new bubble in the stacked position.
549                 float toY = getExpandedY();
550                 float fromY = getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
551                 animationForChild(child)
552                         .translationY(fromY, toY)
553                         .start();
554                 updateBubblePositions();
555             }
556         }
557     }
558 
559     @Override
560     void onChildRemoved(View child, int index, Runnable finishRemoval) {
561         // If we're removing the dragged-out bubble, that means it got dismissed.
562         if (child.equals(getDraggedOutBubble())) {
563             mMagnetizedBubbleDraggingOut = null;
564             finishRemoval.run();
565             mOnBubbleAnimatedOutAction.run();
566         } else {
567             PhysicsAnimator.getInstance(child)
568                     .spring(DynamicAnimation.ALPHA, 0f)
569                     .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig)
570                     .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig)
571                     .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction)
572                     .start();
573         }
574 
575         // Animate all the other bubbles to their new positions sans this bubble.
576         updateBubblePositions();
577     }
578 
579     @Override
580     void onChildReordered(View child, int oldIndex, int newIndex) {
581         if (mPreparingToCollapse) {
582             // If a re-order is received while we're preparing to collapse, ignore it. Once started,
583             // the collapse animation will animate all of the bubbles to their correct (stacked)
584             // position.
585             return;
586         }
587 
588         if (mAnimatingCollapse) {
589             // If a re-order is received during collapse, update the animation so that the bubbles
590             // end up in the correct (stacked) position.
591             startOrUpdatePathAnimation(false /* expanding */);
592         } else {
593             // Otherwise, animate the bubbles around to reflect their new order.
594             updateBubblePositions();
595         }
596     }
597 
598     private void updateBubblePositions() {
599         if (mAnimatingExpand || mAnimatingCollapse) {
600             return;
601         }
602 
603         for (int i = 0; i < mLayout.getChildCount(); i++) {
604             final View bubble = mLayout.getChildAt(i);
605 
606             // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
607             // will be snapped to the correct X value after the drag (if it's not dismissed).
608             if (bubble.equals(getDraggedOutBubble())) {
609                 return;
610             }
611 
612             if (mPositioner.showBubblesVertically()) {
613                 Rect availableRect = mPositioner.getAvailableRect();
614                 boolean onLeft = mCollapsePoint != null
615                         && mCollapsePoint.x < (availableRect.width() / 2f);
616                 animationForChild(bubble)
617                         .translationX(onLeft
618                                 ? availableRect.left
619                                 : availableRect.right - mBubbleSizePx)
620                         .translationY(getBubbleXOrYForOrientation(i))
621                         .start();
622             } else {
623                 animationForChild(bubble)
624                         .translationX(getBubbleXOrYForOrientation(i))
625                         .translationY(getExpandedY())
626                         .start();
627             }
628         }
629     }
630 
631     // TODO - could move to method on bubblePositioner if mSpaceBetweenBubbles gets moved
632     /**
633      * When bubbles are expanded in portrait, they display at the top of the screen in a horizontal
634      * row. When in landscape or on a large screen, they show at the left or right side in a
635      * vertical row. This method accounts for screen orientation and will return an x or y value
636      * for the position of the bubble in the row.
637      *
638      * @param index Bubble index in row.
639      * @return the y position of the bubble if showing vertically and the x position if showing
640      * horizontally.
641      */
642     public float getBubbleXOrYForOrientation(int index) {
643         if (mLayout == null) {
644             return 0;
645         }
646         final float positionInBar = index * (mBubbleSizePx + mSpaceBetweenBubbles);
647         Rect availableRect = mPositioner.getAvailableRect();
648         final boolean isLandscape = mPositioner.showBubblesVertically();
649         final float expandedStackSize = (mLayout.getChildCount() * mBubbleSizePx)
650                 + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles);
651         final float centerPosition = isLandscape
652                 ? availableRect.centerY()
653                 : availableRect.centerX();
654         final float rowStart = centerPosition - (expandedStackSize / 2f);
655         return rowStart + positionInBar;
656     }
657 }
658