• 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.systemui.bubbles.animation;
18 
19 import android.content.res.Resources;
20 import android.graphics.PointF;
21 import android.graphics.RectF;
22 import android.util.Log;
23 import android.view.View;
24 import android.view.WindowInsets;
25 
26 import androidx.dynamicanimation.animation.DynamicAnimation;
27 import androidx.dynamicanimation.animation.FlingAnimation;
28 import androidx.dynamicanimation.animation.FloatPropertyCompat;
29 import androidx.dynamicanimation.animation.SpringAnimation;
30 import androidx.dynamicanimation.animation.SpringForce;
31 
32 import com.android.systemui.R;
33 
34 import com.google.android.collect.Sets;
35 
36 import java.util.HashMap;
37 import java.util.Set;
38 
39 /**
40  * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop
41  * each other with a slight offset to the left or right (depending on which side of the screen they
42  * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of
43  * the screen.
44  */
45 public class StackAnimationController extends
46         PhysicsAnimationLayout.PhysicsAnimationController {
47 
48     private static final String TAG = "Bubbs.StackCtrl";
49 
50     /** Scale factor to use initially for new bubbles being animated in. */
51     private static final float ANIMATE_IN_STARTING_SCALE = 1.15f;
52 
53     /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */
54     private static final int ANIMATE_TRANSLATION_FACTOR = 4;
55 
56     /**
57      * Values to use for the default {@link SpringForce} provided to the physics animation layout.
58      */
59     private static final int DEFAULT_STIFFNESS = 12000;
60     private static final int FLING_FOLLOW_STIFFNESS = 20000;
61     private static final float DEFAULT_BOUNCINESS = 0.9f;
62 
63     /**
64      * Friction applied to fling animations. Since the stack must land on one of the sides of the
65      * screen, we want less friction horizontally so that the stack has a better chance of making it
66      * to the side without needing a spring.
67      */
68     private static final float FLING_FRICTION_X = 2.2f;
69     private static final float FLING_FRICTION_Y = 2.2f;
70 
71     /**
72      * Values to use for the stack spring animation used to spring the stack to its final position
73      * after a fling.
74      */
75     private static final int SPRING_AFTER_FLING_STIFFNESS = 750;
76     private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
77 
78     /**
79      * Minimum fling velocity required to trigger moving the stack from one side of the screen to
80      * the other.
81      */
82     private static final float ESCAPE_VELOCITY = 750f;
83 
84     /**
85      * The canonical position of the stack. This is typically the position of the first bubble, but
86      * we need to keep track of it separately from the first bubble's translation in case there are
87      * no bubbles, or the first bubble was just added and being animated to its new position.
88      */
89     private PointF mStackPosition = new PointF(-1, -1);
90 
91     /** Whether or not the stack's start position has been set. */
92     private boolean mStackMovedToStartPosition = false;
93 
94     /** The most recent position in which the stack was resting on the edge of the screen. */
95     private PointF mRestingStackPosition;
96 
97     /** The height of the most recently visible IME. */
98     private float mImeHeight = 0f;
99 
100     /**
101      * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
102      * IME is not visible or the user moved the stack since the IME became visible.
103      */
104     private float mPreImeY = Float.MIN_VALUE;
105 
106     /**
107      * Animations on the stack position itself, which would have been started in
108      * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to
109      * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect)
110      * to a legal position on the side of the screen.
111      */
112     private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations =
113             new HashMap<>();
114 
115     /**
116      * Whether the current motion of the stack is due to a fling animation (vs. being dragged
117      * manually).
118      */
119     private boolean mIsMovingFromFlinging = false;
120 
121     /**
122      * Whether the stack is within the dismiss target (either by being dragged, magnet'd, or flung).
123      */
124     private boolean mWithinDismissTarget = false;
125 
126     /**
127      * Whether the first bubble is springing towards the touch point, rather than using the default
128      * behavior of moving directly to the touch point with the rest of the stack following it.
129      *
130      * This happens when the user's finger exits the dismiss area while the stack is magnetized to
131      * the center. Since the touch point differs from the stack location, we need to animate the
132      * stack back to the touch point to avoid a jarring instant location change from the center of
133      * the target to the touch point just outside the target bounds.
134      *
135      * This is reset once the spring animations end, since that means the first bubble has
136      * successfully 'caught up' to the touch.
137      */
138     private boolean mFirstBubbleSpringingToTouch = false;
139 
140     /** Horizontal offset of bubbles in the stack. */
141     private float mStackOffset;
142     /** Diameter of the bubbles themselves. */
143     private int mIndividualBubbleSize;
144     /**
145      * The amount of space to add between the bubbles and certain UI elements, such as the top of
146      * the screen or the IME. This does not apply to the left/right sides of the screen since the
147      * stack goes offscreen intentionally.
148      */
149     private int mBubblePadding;
150     /** How far offscreen the stack rests. */
151     private int mBubbleOffscreen;
152     /** How far down the screen the stack starts, when there is no pre-existing location. */
153     private int mStackStartingVerticalOffset;
154     /** Height of the status bar. */
155     private float mStatusBarHeight;
156 
157     /**
158      * Instantly move the first bubble to the given point, and animate the rest of the stack behind
159      * it with the 'following' effect.
160      */
moveFirstBubbleWithStackFollowing(float x, float y)161     public void moveFirstBubbleWithStackFollowing(float x, float y) {
162         // If we manually move the bubbles with the IME open, clear the return point since we don't
163         // want the stack to snap away from the new position.
164         mPreImeY = Float.MIN_VALUE;
165 
166         moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x);
167         moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y);
168 
169         // This method is called when the stack is being dragged manually, so we're clearly no
170         // longer flinging.
171         mIsMovingFromFlinging = false;
172     }
173 
174     /**
175      * The position of the stack - typically the position of the first bubble; if no bubbles have
176      * been added yet, it will be where the first bubble will go when added.
177      */
getStackPosition()178     public PointF getStackPosition() {
179         return mStackPosition;
180     }
181 
182     /** Whether the stack is on the left side of the screen. */
isStackOnLeftSide()183     public boolean isStackOnLeftSide() {
184         if (mLayout == null || !isStackPositionSet()) {
185             return false;
186         }
187 
188         float stackCenter = mStackPosition.x + mIndividualBubbleSize / 2;
189         float screenCenter = mLayout.getWidth() / 2;
190         return stackCenter < screenCenter;
191     }
192 
193     /**
194      * Fling stack to given corner, within allowable screen bounds.
195      * Note that we need new SpringForce instances per animation despite identical configs because
196      * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
197      */
springStack(float destinationX, float destinationY)198     public void springStack(float destinationX, float destinationY) {
199         springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
200                     new SpringForce()
201                         .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
202                         .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
203                     0 /* startXVelocity */,
204                     destinationX);
205 
206         springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y,
207                     new SpringForce()
208                         .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
209                         .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
210                     0 /* startYVelocity */,
211                     destinationY);
212     }
213 
214     /**
215      * Flings the stack starting with the given velocities, springing it to the nearest edge
216      * afterward.
217      *
218      * @return The X value that the stack will end up at after the fling/spring.
219      */
flingStackThenSpringToEdge(float x, float velX, float velY)220     public float flingStackThenSpringToEdge(float x, float velX, float velY) {
221         final boolean stackOnLeftSide = x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2;
222 
223         final boolean stackShouldFlingLeft = stackOnLeftSide
224                 ? velX < ESCAPE_VELOCITY
225                 : velX < -ESCAPE_VELOCITY;
226 
227         final RectF stackBounds = getAllowableStackPositionRegion();
228 
229         // Target X translation (either the left or right side of the screen).
230         final float destinationRelativeX = stackShouldFlingLeft
231                 ? stackBounds.left : stackBounds.right;
232 
233         // Minimum velocity required for the stack to make it to the targeted side of the screen,
234         // taking friction into account (4.2f is the number that friction scalars are multiplied by
235         // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
236         // but the SpringAnimation at the end will ensure that it reaches the destination X
237         // regardless.
238         final float minimumVelocityToReachEdge =
239                 (destinationRelativeX - x) * (FLING_FRICTION_X * 4.2f);
240 
241         // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
242         // that it'll make it all the way to the side of the screen.
243         final float startXVelocity = stackShouldFlingLeft
244                 ? Math.min(minimumVelocityToReachEdge, velX)
245                 : Math.max(minimumVelocityToReachEdge, velX);
246 
247         flingThenSpringFirstBubbleWithStackFollowing(
248                 DynamicAnimation.TRANSLATION_X,
249                 startXVelocity,
250                 FLING_FRICTION_X,
251                 new SpringForce()
252                         .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
253                         .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
254                 destinationRelativeX);
255 
256         flingThenSpringFirstBubbleWithStackFollowing(
257                 DynamicAnimation.TRANSLATION_Y,
258                 velY,
259                 FLING_FRICTION_Y,
260                 new SpringForce()
261                         .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
262                         .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
263                 /* destination */ null);
264 
265         mLayout.setEndActionForMultipleProperties(
266                 () -> {
267                     mRestingStackPosition = new PointF();
268                     mRestingStackPosition.set(mStackPosition);
269                     mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
270                     mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
271                 },
272                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
273 
274         // If we're flinging now, there's no more touch event to catch up to.
275         mFirstBubbleSpringingToTouch = false;
276         mIsMovingFromFlinging = true;
277         return destinationRelativeX;
278     }
279 
280     /**
281      * Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
282      */
283     public PointF getStackPositionAlongNearestHorizontalEdge() {
284         final PointF stackPos = getStackPosition();
285         final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x);
286         final RectF bounds = getAllowableStackPositionRegion();
287 
288         stackPos.x = onLeft ? bounds.left : bounds.right;
289         return stackPos;
290     }
291 
292     /**
293      * Moves the stack in response to rotation. We keep it in the most similar position by keeping
294      * it on the same side, and positioning it the same percentage of the way down the screen
295      * (taking status bar/nav bar into account by using the allowable region's height).
296      */
297     public void moveStackToSimilarPositionAfterRotation(boolean wasOnLeft, float verticalPercent) {
298         final RectF allowablePos = getAllowableStackPositionRegion();
299         final float allowableRegionHeight = allowablePos.bottom - allowablePos.top;
300 
301         final float x = wasOnLeft ? allowablePos.left : allowablePos.right;
302         final float y = (allowableRegionHeight * verticalPercent) + allowablePos.top;
303 
304         setStackPosition(new PointF(x, y));
305     }
306 
307     /**
308      * Flings the first bubble along the given property's axis, using the provided configuration
309      * values. When the animation ends - either by hitting the min/max, or by friction sufficiently
310      * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final
311      * position.
312      */
313     protected void flingThenSpringFirstBubbleWithStackFollowing(
314             DynamicAnimation.ViewProperty property,
315             float vel,
316             float friction,
317             SpringForce spring,
318             Float finalPosition) {
319         Log.d(TAG, String.format("Flinging %s.",
320                         PhysicsAnimationLayout.getReadablePropertyName(property)));
321 
322         StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
323         final float currentValue = firstBubbleProperty.getValue(this);
324         final RectF bounds = getAllowableStackPositionRegion();
325         final float min =
326                 property.equals(DynamicAnimation.TRANSLATION_X)
327                         ? bounds.left
328                         : bounds.top;
329         final float max =
330                 property.equals(DynamicAnimation.TRANSLATION_X)
331                         ? bounds.right
332                         : bounds.bottom;
333 
334         FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty);
335         flingAnimation.setFriction(friction)
336                 .setStartVelocity(vel)
337 
338                 // If the bubble's property value starts beyond the desired min/max, use that value
339                 // instead so that the animation won't immediately end. If, for example, the user
340                 // drags the bubbles into the navigation bar, but then flings them upward, we want
341                 // the fling to occur despite temporarily having a value outside of the min/max. If
342                 // the bubbles are out of bounds and flung even farther out of bounds, the fling
343                 // animation will halt immediately and the SpringAnimation will take over, springing
344                 // it in reverse to the (legal) final position.
345                 .setMinValue(Math.min(currentValue, min))
346                 .setMaxValue(Math.max(currentValue, max))
347 
348                 .addEndListener((animation, canceled, endValue, endVelocity) -> {
349                     if (!canceled) {
350                         springFirstBubbleWithStackFollowing(property, spring, endVelocity,
351                                 finalPosition != null
352                                         ? finalPosition
353                                         : Math.max(min, Math.min(max, endValue)));
354                     }
355                 });
356 
357         cancelStackPositionAnimation(property);
358         mStackPositionAnimations.put(property, flingAnimation);
359         flingAnimation.start();
360     }
361 
362     /**
363      * Cancel any stack position animations that were started by calling
364      * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end
365      * listeners.
366      */
367     public void cancelStackPositionAnimations() {
368         cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
369         cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
370 
371         mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
372         mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
373     }
374 
375     /** Save the current IME height so that we know where the stack bounds should be. */
376     public void setImeHeight(int imeHeight) {
377         mImeHeight = imeHeight;
378     }
379 
380     /**
381      * Animates the stack either away from the newly visible IME, or back to its original position
382      * due to the IME going away.
383      */
384     public void animateForImeVisibility(boolean imeVisible) {
385         final float maxBubbleY = getAllowableStackPositionRegion().bottom;
386         float destinationY = Float.MIN_VALUE;
387 
388         if (imeVisible) {
389             // Stack is lower than it should be and overlaps the now-visible IME.
390             if (mStackPosition.y > maxBubbleY && mPreImeY == Float.MIN_VALUE) {
391                 mPreImeY = mStackPosition.y;
392                 destinationY = maxBubbleY;
393             }
394         } else {
395             if (mPreImeY > Float.MIN_VALUE) {
396                 destinationY = mPreImeY;
397                 mPreImeY = Float.MIN_VALUE;
398             }
399         }
400 
401         if (destinationY > Float.MIN_VALUE) {
402             springFirstBubbleWithStackFollowing(
403                     DynamicAnimation.TRANSLATION_Y,
404                     getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
405                             .setStiffness(SpringForce.STIFFNESS_LOW),
406                     /* startVel */ 0f,
407                     destinationY);
408         }
409     }
410 
411     /**
412      * Returns the region within which the stack is allowed to rest. This goes slightly off the left
413      * and right sides of the screen, below the status bar/cutout and above the navigation bar.
414      * While the stack is not allowed to rest outside of these bounds, it can temporarily be
415      * animated or dragged beyond them.
416      */
417     public RectF getAllowableStackPositionRegion() {
418         final WindowInsets insets = mLayout.getRootWindowInsets();
419         final RectF allowableRegion = new RectF();
420         if (insets != null) {
421             allowableRegion.left =
422                     -mBubbleOffscreen
423                             + Math.max(
424                             insets.getSystemWindowInsetLeft(),
425                             insets.getDisplayCutout() != null
426                                     ? insets.getDisplayCutout().getSafeInsetLeft()
427                                     : 0);
428             allowableRegion.right =
429                     mLayout.getWidth()
430                             - mIndividualBubbleSize
431                             + mBubbleOffscreen
432                             - Math.max(
433                             insets.getSystemWindowInsetRight(),
434                             insets.getDisplayCutout() != null
435                                     ? insets.getDisplayCutout().getSafeInsetRight()
436                                     : 0);
437 
438             allowableRegion.top =
439                     mBubblePadding
440                             + Math.max(
441                             mStatusBarHeight,
442                             insets.getDisplayCutout() != null
443                                     ? insets.getDisplayCutout().getSafeInsetTop()
444                                     : 0);
445             allowableRegion.bottom =
446                     mLayout.getHeight()
447                             - mIndividualBubbleSize
448                             - mBubblePadding
449                             - (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePadding : 0f)
450                             - Math.max(
451                             insets.getSystemWindowInsetBottom(),
452                             insets.getDisplayCutout() != null
453                                     ? insets.getDisplayCutout().getSafeInsetBottom()
454                                     : 0);
455         }
456 
457         return allowableRegion;
458     }
459 
460     /** Moves the stack in response to a touch event. */
461     public void moveStackFromTouch(float x, float y) {
462 
463         // If we're springing to the touch point to 'catch up' after dragging out of the dismiss
464         // target, then update the stack position animations instead of moving the bubble directly.
465         if (mFirstBubbleSpringingToTouch) {
466             final SpringAnimation springToTouchX =
467                     (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X);
468             final SpringAnimation springToTouchY =
469                     (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y);
470 
471             // If either animation is still running, we haven't caught up. Update the animations.
472             if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
473                 springToTouchX.animateToFinalPosition(x);
474                 springToTouchY.animateToFinalPosition(y);
475             } else {
476                 // If the animations have finished, the stack is now at the touch point. We can
477                 // resume moving the bubble directly.
478                 mFirstBubbleSpringingToTouch = false;
479             }
480         }
481 
482         if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) {
483             moveFirstBubbleWithStackFollowing(x, y);
484         }
485     }
486 
487     /**
488      * Demagnetizes the stack, springing it towards the given point. This also sets flags so that
489      * subsequent touch events will update the final position of the demagnetization spring instead
490      * of directly moving the bubbles, until demagnetization is complete.
491      */
492     public void demagnetizeFromDismissToPoint(float x, float y, float velX, float velY) {
493         mWithinDismissTarget = false;
494         mFirstBubbleSpringingToTouch = true;
495 
496         springFirstBubbleWithStackFollowing(
497                 DynamicAnimation.TRANSLATION_X,
498                 new SpringForce()
499                         .setDampingRatio(DEFAULT_BOUNCINESS)
500                         .setStiffness(DEFAULT_STIFFNESS),
501                 velX, x);
502 
503         springFirstBubbleWithStackFollowing(
504                 DynamicAnimation.TRANSLATION_Y,
505                 new SpringForce()
506                         .setDampingRatio(DEFAULT_BOUNCINESS)
507                         .setStiffness(DEFAULT_STIFFNESS),
508                 velY, y);
509     }
510 
511     /**
512      * Spring the stack towards the dismiss target, respecting existing velocity. This also sets
513      * flags so that subsequent touch events will not move the stack until it's demagnetized.
514      */
515     public void magnetToDismiss(float velX, float velY, float destY, Runnable after) {
516         mWithinDismissTarget = true;
517         mFirstBubbleSpringingToTouch = false;
518 
519         animationForChildAtIndex(0)
520                 .translationX(mLayout.getWidth() / 2f - mIndividualBubbleSize / 2f)
521                 .translationY(destY, after)
522                 .withPositionStartVelocities(velX, velY)
523                 .withStiffness(SpringForce.STIFFNESS_MEDIUM)
524                 .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
525                 .start();
526     }
527 
528     /**
529      * 'Implode' the stack by shrinking the bubbles via chained animations and fading them out.
530      */
531     public void implodeStack(Runnable after) {
532         // Pop and fade the bubbles sequentially.
533         animationForChildAtIndex(0)
534                 .scaleX(0.5f)
535                 .scaleY(0.5f)
536                 .alpha(0f)
537                 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
538                 .withStiffness(SpringForce.STIFFNESS_HIGH)
539                 .start(() -> {
540                     // Run the callback and reset flags. The child translation animations might
541                     // still be running, but that's fine. Once the alpha is at 0f they're no longer
542                     // visible anyway.
543                     after.run();
544                     mWithinDismissTarget = false;
545                 });
546     }
547 
548     /**
549      * Springs the first bubble to the given final position, with the rest of the stack 'following'.
550      */
551     protected void springFirstBubbleWithStackFollowing(
552             DynamicAnimation.ViewProperty property, SpringForce spring,
553             float vel, float finalPosition) {
554 
555         if (mLayout.getChildCount() == 0) {
556             return;
557         }
558 
559         Log.d(TAG, String.format("Springing %s to final position %f.",
560                 PhysicsAnimationLayout.getReadablePropertyName(property),
561                 finalPosition));
562 
563         StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
564         SpringAnimation springAnimation =
565                 new SpringAnimation(this, firstBubbleProperty)
566                         .setSpring(spring)
567                         .setStartVelocity(vel);
568 
569         cancelStackPositionAnimation(property);
570         mStackPositionAnimations.put(property, springAnimation);
571         springAnimation.animateToFinalPosition(finalPosition);
572     }
573 
574     @Override
575     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
576         return Sets.newHashSet(
577                 DynamicAnimation.TRANSLATION_X, // For positioning.
578                 DynamicAnimation.TRANSLATION_Y,
579                 DynamicAnimation.ALPHA,         // For fading in new bubbles.
580                 DynamicAnimation.SCALE_X,       // For 'popping in' new bubbles.
581                 DynamicAnimation.SCALE_Y);
582     }
583 
584     @Override
585     int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
586         if (property.equals(DynamicAnimation.TRANSLATION_X)
587                 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
588             return index + 1;
589         } else if (mWithinDismissTarget) {
590             return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
591         } else {
592             return NONE;
593         }
594     }
595 
596 
597     @Override
598     float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
599         if (property.equals(DynamicAnimation.TRANSLATION_X)) {
600             // If we're in the dismiss target, have the bubbles pile on top of each other with no
601             // offset.
602             if (mWithinDismissTarget) {
603                 return 0f;
604             } else {
605                 // Offset to the left if we're on the left, or the right otherwise.
606                 return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
607                         ? -mStackOffset : mStackOffset;
608             }
609         } else {
610             return 0f;
611         }
612     }
613 
614     @Override
615     SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
616         return new SpringForce()
617                 .setDampingRatio(DEFAULT_BOUNCINESS)
618                 .setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS);
619     }
620 
621     @Override
622     void onChildAdded(View child, int index) {
623         if (mLayout.getChildCount() == 1) {
624             // If this is the first child added, position the stack in its starting position.
625             moveStackToStartPosition();
626         } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) {
627             // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
628             // to the back of the stack, it'll be largely invisible so don't bother animating it in.
629             animateInBubble(child);
630         }
631     }
632 
633     @Override
634     void onChildRemoved(View child, int index, Runnable finishRemoval) {
635         // Animate the removing view in the opposite direction of the stack.
636         final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
637         animationForChild(child)
638                 .alpha(0f, finishRemoval /* after */)
639                 .scaleX(ANIMATE_IN_STARTING_SCALE)
640                 .scaleY(ANIMATE_IN_STARTING_SCALE)
641                 .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR))
642                 .start();
643 
644         if (mLayout.getChildCount() > 0) {
645             animationForChildAtIndex(0).translationX(mStackPosition.x).start();
646         } else {
647             // Set the start position back to the default since we're out of bubbles. New bubbles
648             // will then animate in from the start position.
649             mStackPosition = getDefaultStartPosition();
650         }
651     }
652 
653     @Override
654     void onChildReordered(View child, int oldIndex, int newIndex) {}
655 
656     @Override
657     void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
658         Resources res = layout.getResources();
659         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
660         mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
661         mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
662         mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
663         mStackStartingVerticalOffset =
664                 res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
665         mStatusBarHeight =
666                 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
667     }
668 
669     /** Moves the stack, without any animation, to the starting position. */
670     private void moveStackToStartPosition() {
671         // Post to ensure that the layout's width and height have been calculated.
672         mLayout.setVisibility(View.INVISIBLE);
673         mLayout.post(() -> {
674             setStackPosition(mRestingStackPosition == null
675                     ? getDefaultStartPosition()
676                     : mRestingStackPosition);
677             mStackMovedToStartPosition = true;
678             mLayout.setVisibility(View.VISIBLE);
679 
680             // Animate in the top bubble now that we're visible.
681             if (mLayout.getChildCount() > 0) {
682                 animateInBubble(mLayout.getChildAt(0));
683             }
684         });
685     }
686 
687     /**
688      * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
689      * bubbles to animate 'following' to the new location.
690      */
691     private void moveFirstBubbleWithStackFollowing(
692             DynamicAnimation.ViewProperty property, float value) {
693 
694         // Update the canonical stack position.
695         if (property.equals(DynamicAnimation.TRANSLATION_X)) {
696             mStackPosition.x = value;
697         } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
698             mStackPosition.y = value;
699         }
700 
701         if (mLayout.getChildCount() > 0) {
702             property.setValue(mLayout.getChildAt(0), value);
703             if (mLayout.getChildCount() > 1) {
704                 animationForChildAtIndex(1)
705                         .property(property, value + getOffsetForChainedPropertyAnimation(property))
706                         .start();
707             }
708         }
709     }
710 
711     /** Moves the stack to a position instantly, with no animation. */
712     private void setStackPosition(PointF pos) {
713         Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
714         mStackPosition.set(pos.x, pos.y);
715 
716         // If we're not the active controller, we don't want to physically move the bubble views.
717         if (isActiveController()) {
718             mLayout.cancelAllAnimations();
719             cancelStackPositionAnimations();
720 
721             // Since we're not using the chained animations, apply the offsets manually.
722             final float xOffset = getOffsetForChainedPropertyAnimation(
723                     DynamicAnimation.TRANSLATION_X);
724             final float yOffset = getOffsetForChainedPropertyAnimation(
725                     DynamicAnimation.TRANSLATION_Y);
726             for (int i = 0; i < mLayout.getChildCount(); i++) {
727                 mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
728                 mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
729             }
730         }
731     }
732 
733     /** Returns the default stack position, which is on the top right. */
734     private PointF getDefaultStartPosition() {
735         return new PointF(
736                 getAllowableStackPositionRegion().right,
737                 getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
738     }
739 
740     private boolean isStackPositionSet() {
741         return mStackMovedToStartPosition;
742     }
743 
744     /** Animates in the given bubble. */
745     private void animateInBubble(View child) {
746         if (!isActiveController()) {
747             return;
748         }
749 
750         child.setTranslationY(mStackPosition.y);
751 
752         float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
753         animationForChild(child)
754                 .scaleX(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
755                 .scaleY(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
756                 .alpha(0f /* from */, 1f /* to */)
757                 .translationX(
758                         mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset /* from */,
759                         mStackPosition.x /* to */)
760                 .start();
761     }
762 
763     /**
764      * Cancels any outstanding first bubble property animations that are running. This does not
765      * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
766      * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
767      * {@link #flingThenSpringFirstBubbleWithStackFollowing}.
768      */
769     private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
770         if (mStackPositionAnimations.containsKey(property)) {
771             mStackPositionAnimations.get(property).cancel();
772         }
773     }
774 
775     /**
776      * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
777      * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
778      * property directly to move the first bubble and cause the stack to 'follow' to the new
779      * location.
780      *
781      * This could also be achieved by simply animating the first bubble view and adding an update
782      * listener to dispatch movement to the rest of the stack. However, this would require
783      * duplication of logic in that update handler - it's simpler to keep all logic contained in the
784      * {@link #moveFirstBubbleWithStackFollowing} method.
785      */
786     private class StackPositionProperty
787             extends FloatPropertyCompat<StackAnimationController> {
788         private final DynamicAnimation.ViewProperty mProperty;
789 
790         private StackPositionProperty(DynamicAnimation.ViewProperty property) {
791             super(property.toString());
792             mProperty = property;
793         }
794 
795         @Override
796         public float getValue(StackAnimationController controller) {
797             return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
798         }
799 
800         @Override
801         public void setValue(StackAnimationController controller, float value) {
802             moveFirstBubbleWithStackFollowing(mProperty, value);
803         }
804     }
805 }
806 
807