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