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