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