1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.bubbles.animation;
18 
19 import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
20 import static com.android.wm.shell.bubbles.animation.FlingToDismissUtils.getFlingToDismissTargetWidth;
21 
22 import android.content.ContentResolver;
23 import android.content.res.Resources;
24 import android.graphics.PointF;
25 import android.graphics.Rect;
26 import android.graphics.RectF;
27 import android.provider.Settings;
28 import android.util.Log;
29 import android.view.View;
30 import android.view.ViewPropertyAnimator;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.dynamicanimation.animation.DynamicAnimation;
35 import androidx.dynamicanimation.animation.FlingAnimation;
36 import androidx.dynamicanimation.animation.FloatPropertyCompat;
37 import androidx.dynamicanimation.animation.SpringAnimation;
38 import androidx.dynamicanimation.animation.SpringForce;
39 
40 import com.android.wm.shell.R;
41 import com.android.wm.shell.bubbles.BadgedImageView;
42 import com.android.wm.shell.bubbles.BubblePositioner;
43 import com.android.wm.shell.bubbles.BubbleStackView;
44 import com.android.wm.shell.common.FloatingContentCoordinator;
45 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
46 import com.android.wm.shell.shared.animation.PhysicsAnimator;
47 
48 import com.google.android.collect.Sets;
49 
50 import java.io.PrintWriter;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Set;
54 import java.util.function.IntSupplier;
55 
56 /**
57  * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop
58  * each other with a slight offset to the left or right (depending on which side of the screen they
59  * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of
60  * the screen.
61  */
62 public class StackAnimationController extends
63         PhysicsAnimationLayout.PhysicsAnimationController {
64 
65     private static final String TAG = "Bubbs.StackCtrl";
66 
67     /** Value to use for animating bubbles in and springing stack after fling. */
68     private static final float STACK_SPRING_STIFFNESS = 700f;
69 
70     /** Values to use for animating updated bubble to top of stack. */
71     private static final float NEW_BUBBLE_START_SCALE = 0.5f;
72     private static final float NEW_BUBBLE_START_Y = 100f;
73     private static final long BUBBLE_SWAP_DURATION = 300L;
74 
75     /**
76      * Values to use for the default {@link SpringForce} provided to the physics animation layout.
77      */
78     public static final int SPRING_TO_TOUCH_STIFFNESS = 12000;
79     public static final float IME_ANIMATION_STIFFNESS = SpringForce.STIFFNESS_LOW;
80     private static final int CHAIN_STIFFNESS = 800;
81     public static final float DEFAULT_BOUNCINESS = 0.9f;
82 
83     private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig =
84             new PhysicsAnimator.SpringConfig(
85                     STACK_SPRING_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY);
86 
87     /**
88      * Friction applied to fling animations. Since the stack must land on one of the sides of the
89      * screen, we want less friction horizontally so that the stack has a better chance of making it
90      * to the side without needing a spring.
91      */
92     private static final float FLING_FRICTION = 1.9f;
93 
94     private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
95 
96     /** Sentinel value for unset position value. */
97     private static final float UNSET = -Float.MIN_VALUE;
98 
99     /**
100      * Minimum fling velocity required to trigger moving the stack from one side of the screen to
101      * the other.
102      */
103     private static final float ESCAPE_VELOCITY = 750f;
104 
105     /** Velocity required to dismiss the stack without dragging it into the dismiss target. */
106     private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f;
107 
108     /**
109      * The canonical position of the stack. This is typically the position of the first bubble, but
110      * we need to keep track of it separately from the first bubble's translation in case there are
111      * no bubbles, or the first bubble was just added and being animated to its new position.
112      */
113     private PointF mStackPosition = new PointF(-1, -1);
114 
115     /**
116      * MagnetizedObject instance for the stack, which is used by the touch handler for the magnetic
117      * dismiss target.
118      */
119     private MagnetizedObject<StackAnimationController> mMagnetizedStack;
120 
121     /**
122      * The area that Bubbles will occupy after all animations end. This is used to move other
123      * floating content out of the way proactively.
124      */
125     private Rect mAnimatingToBounds = new Rect();
126 
127     /** Whether or not the stack's start position has been set. */
128     private boolean mStackMovedToStartPosition = false;
129 
130     /**
131      * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
132      * IME is not visible or the user moved the stack since the IME became visible.
133      */
134     private float mPreImeY = UNSET;
135 
136     /**
137      * Animations on the stack position itself, which would have been started in
138      * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to
139      * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect)
140      * to a legal position on the side of the screen.
141      */
142     private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations =
143             new HashMap<>();
144 
145     /**
146      * Whether the current motion of the stack is due to a fling animation (vs. being dragged
147      * manually).
148      */
149     private boolean mIsMovingFromFlinging = false;
150 
151     /**
152      * Whether the first bubble is springing towards the touch point, rather than using the default
153      * behavior of moving directly to the touch point with the rest of the stack following it.
154      *
155      * This happens when the user's finger exits the dismiss area while the stack is magnetized to
156      * the center. Since the touch point differs from the stack location, we need to animate the
157      * stack back to the touch point to avoid a jarring instant location change from the center of
158      * the target to the touch point just outside the target bounds.
159      *
160      * This is reset once the spring animations end, since that means the first bubble has
161      * successfully 'caught up' to the touch.
162      */
163     private boolean mFirstBubbleSpringingToTouch = false;
164 
165     /**
166      * Whether to spring the stack to the next touch event coordinates. This is used to animate the
167      * stack (including the first bubble) out of the magnetic dismiss target to the touch location.
168      * Once it 'catches up' and the animation ends, we'll revert to moving the first bubble directly
169      * and only animating the following bubbles.
170      */
171     private boolean mSpringToTouchOnNextMotionEvent = false;
172 
173     /** Offset of bubbles in the stack (i.e. how much they overlap). */
174     private float mStackOffset;
175     /** Offset between stack y and animation y for bubble swap. */
176     private float mSwapAnimationOffset;
177     /** Max number of bubbles to show in the expanded bubble row. */
178     private int mMaxBubbles;
179     /** Default bubble elevation. */
180     private int mElevation;
181     /** Diameter of the bubble. */
182     private int mBubbleSize;
183     /**
184      * The amount of space to add between the bubbles and certain UI elements, such as the top of
185      * the screen or the IME. This does not apply to the left/right sides of the screen since the
186      * stack goes offscreen intentionally.
187      */
188     private int mBubblePaddingTop;
189     /** Contains display size, orientation, and inset information. */
190     private BubblePositioner mPositioner;
191 
192     /** FloatingContentCoordinator instance for resolving floating content conflicts. */
193     private FloatingContentCoordinator mFloatingContentCoordinator;
194 
195     /**
196      * FloatingContent instance that returns the stack's location on the screen, and moves it when
197      * requested.
198      */
199     private final FloatingContentCoordinator.FloatingContent mStackFloatingContent =
200             new FloatingContentCoordinator.FloatingContent() {
201 
202         private final Rect mFloatingBoundsOnScreen = new Rect();
203 
204         @Override
205         public void moveToBounds(@NonNull Rect bounds) {
206             springStack(bounds.left, bounds.top, STACK_SPRING_STIFFNESS);
207         }
208 
209         @NonNull
210         @Override
211         public Rect getAllowedFloatingBoundsRegion() {
212             final Rect floatingBounds = getFloatingBoundsOnScreen();
213             final Rect allowableStackArea = new Rect();
214             mPositioner.getAllowableStackPositionRegion(getBubbleCount())
215                     .roundOut(allowableStackArea);
216             allowableStackArea.right += floatingBounds.width();
217             allowableStackArea.bottom += floatingBounds.height();
218             return allowableStackArea;
219         }
220 
221         @NonNull
222         @Override
223         public Rect getFloatingBoundsOnScreen() {
224             if (!mAnimatingToBounds.isEmpty()) {
225                 return mAnimatingToBounds;
226             }
227 
228             if (mLayout.getChildCount() > 0) {
229                 // Calculate the bounds using stack position + bubble size so that we don't need to
230                 // wait for the bubble views to lay out.
231                 mFloatingBoundsOnScreen.set(
232                         (int) mStackPosition.x,
233                         (int) mStackPosition.y,
234                         (int) mStackPosition.x + mBubbleSize,
235                         (int) mStackPosition.y + mBubbleSize + mBubblePaddingTop);
236             } else {
237                 mFloatingBoundsOnScreen.setEmpty();
238             }
239 
240             return mFloatingBoundsOnScreen;
241         }
242     };
243 
244     /** Returns the number of 'real' bubbles (excluding the overflow bubble). */
245     private IntSupplier mBubbleCountSupplier;
246 
247     /**
248      * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the
249      * end of this animation means we have no bubbles left, and notify the BubbleController.
250      */
251     private Runnable mOnBubbleAnimatedOutAction;
252 
253     /**
254      * Callback to run whenever the stack is finished being flung somewhere.
255      */
256     private Runnable mOnStackAnimationFinished;
257 
StackAnimationController( FloatingContentCoordinator floatingContentCoordinator, IntSupplier bubbleCountSupplier, Runnable onBubbleAnimatedOutAction, Runnable onStackAnimationFinished, BubblePositioner positioner)258     public StackAnimationController(
259             FloatingContentCoordinator floatingContentCoordinator,
260             IntSupplier bubbleCountSupplier,
261             Runnable onBubbleAnimatedOutAction,
262             Runnable onStackAnimationFinished,
263             BubblePositioner positioner) {
264         mFloatingContentCoordinator = floatingContentCoordinator;
265         mBubbleCountSupplier = bubbleCountSupplier;
266         mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction;
267         mOnStackAnimationFinished = onStackAnimationFinished;
268         mPositioner = positioner;
269     }
270 
271     /**
272      * Instantly move the first bubble to the given point, and animate the rest of the stack behind
273      * it with the 'following' effect.
274      */
moveFirstBubbleWithStackFollowing(float x, float y)275     public void moveFirstBubbleWithStackFollowing(float x, float y) {
276         // If we're moving the bubble around, we're not animating to any bounds.
277         mAnimatingToBounds.setEmpty();
278 
279         // If we manually move the bubbles with the IME open, clear the return point since we don't
280         // want the stack to snap away from the new position.
281         mPreImeY = UNSET;
282 
283         moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x);
284         moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y);
285 
286         // This method is called when the stack is being dragged manually, so we're clearly no
287         // longer flinging.
288         mIsMovingFromFlinging = false;
289     }
290 
291     /**
292      * The position of the stack - typically the position of the first bubble; if no bubbles have
293      * been added yet, it will be where the first bubble will go when added.
294      */
getStackPosition()295     public PointF getStackPosition() {
296         return mStackPosition;
297     }
298 
299     /** Whether the stack is on the left side of the screen. */
isStackOnLeftSide()300     public boolean isStackOnLeftSide() {
301         return mPositioner.isStackOnLeft(mStackPosition);
302     }
303 
304     /**
305      * Fling stack to given corner, within allowable screen bounds.
306      * Note that we need new SpringForce instances per animation despite identical configs because
307      * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
308      */
springStack( float destinationX, float destinationY, float stiffness)309     public void springStack(
310             float destinationX, float destinationY, float stiffness) {
311         notifyFloatingCoordinatorStackAnimatingTo(destinationX, destinationY);
312 
313         springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
314                 new SpringForce()
315                         .setStiffness(stiffness)
316                         .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
317                 0 /* startXVelocity */,
318                 destinationX);
319 
320         springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y,
321                 new SpringForce()
322                         .setStiffness(stiffness)
323                         .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
324                 0 /* startYVelocity */,
325                 destinationY);
326     }
327 
328     /**
329      * Springs the stack to the specified x/y coordinates, with the stiffness used for springs after
330      * flings.
331      */
springStackAfterFling(float destinationX, float destinationY)332     public void springStackAfterFling(float destinationX, float destinationY) {
333         springStack(destinationX, destinationY, STACK_SPRING_STIFFNESS);
334     }
335 
336     /**
337      * Flings the stack starting with the given velocities, springing it to the nearest edge
338      * afterward.
339      *
340      * @return The X value that the stack will end up at after the fling/spring.
341      */
flingStackThenSpringToEdge(float x, float velX, float velY)342     public float flingStackThenSpringToEdge(float x, float velX, float velY) {
343         final boolean stackOnLeftSide = x - mBubbleSize / 2 < mLayout.getWidth() / 2;
344 
345         final boolean stackShouldFlingLeft = stackOnLeftSide
346                 ? velX < ESCAPE_VELOCITY
347                 : velX < -ESCAPE_VELOCITY;
348 
349         final RectF stackBounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount());
350 
351         // Target X translation (either the left or right side of the screen).
352         final float destinationRelativeX = stackShouldFlingLeft
353                 ? stackBounds.left : stackBounds.right;
354 
355         // If all bubbles were removed during a drag event, just return the X we would have animated
356         // to if there were still bubbles.
357         if (mLayout == null || mLayout.getChildCount() == 0) {
358             return destinationRelativeX;
359         }
360 
361         final ContentResolver contentResolver = mLayout.getContext().getContentResolver();
362         final float stiffness = Settings.Secure.getFloat(contentResolver, "bubble_stiffness",
363                 STACK_SPRING_STIFFNESS /* default */);
364         final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping",
365                 SPRING_AFTER_FLING_DAMPING_RATIO);
366         final float friction = Settings.Secure.getFloat(contentResolver, "bubble_friction",
367                 FLING_FRICTION);
368 
369         // Minimum velocity required for the stack to make it to the targeted side of the screen,
370         // taking friction into account (4.2f is the number that friction scalars are multiplied by
371         // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
372         // but the SpringAnimation at the end will ensure that it reaches the destination X
373         // regardless.
374         final float minimumVelocityToReachEdge =
375                 (destinationRelativeX - x) * (friction * 4.2f);
376 
377         final float estimatedY = PhysicsAnimator.estimateFlingEndValue(
378                 mStackPosition.y, velY,
379                 new PhysicsAnimator.FlingConfig(
380                         friction, stackBounds.top, stackBounds.bottom));
381 
382         notifyFloatingCoordinatorStackAnimatingTo(destinationRelativeX, estimatedY);
383 
384         // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
385         // that it'll make it all the way to the side of the screen.
386         final float startXVelocity = stackShouldFlingLeft
387                 ? Math.min(minimumVelocityToReachEdge, velX)
388                 : Math.max(minimumVelocityToReachEdge, velX);
389 
390 
391 
392         flingThenSpringFirstBubbleWithStackFollowing(
393                 DynamicAnimation.TRANSLATION_X,
394                 startXVelocity,
395                 friction,
396                 new SpringForce()
397                         .setStiffness(stiffness)
398                         .setDampingRatio(dampingRatio),
399                 destinationRelativeX);
400 
401         flingThenSpringFirstBubbleWithStackFollowing(
402                 DynamicAnimation.TRANSLATION_Y,
403                 velY,
404                 friction,
405                 new SpringForce()
406                         .setStiffness(stiffness)
407                         .setDampingRatio(dampingRatio),
408                 /* destination */ null);
409 
410         // If we're flinging now, there's no more touch event to catch up to.
411         mFirstBubbleSpringingToTouch = false;
412         mIsMovingFromFlinging = true;
413         return destinationRelativeX;
414     }
415 
416     /**
417      * Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
418      */
419     public PointF getStackPositionAlongNearestHorizontalEdge() {
420         final PointF stackPos = getStackPosition();
421         final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x);
422         final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount());
423 
424         stackPos.x = onLeft ? bounds.left : bounds.right;
425         return stackPos;
426     }
427 
428     /** Description of current animation controller state. */
429     public void dump(PrintWriter pw) {
430         pw.println("StackAnimationController state:");
431         pw.print("  isActive:             "); pw.println(isActiveController());
432         pw.print("  restingStackPos:      ");
433         pw.println(mPositioner.getRestingPosition().toString());
434         pw.print("  currentStackPos:      "); pw.println(mStackPosition.toString());
435         pw.print("  isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging);
436         pw.print("  withinDismiss:        "); pw.println(isStackStuckToTarget());
437         pw.print("  firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch);
438     }
439 
440     /**
441      * Flings the first bubble along the given property's axis, using the provided configuration
442      * values. When the animation ends - either by hitting the min/max, or by friction sufficiently
443      * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final
444      * position.
445      */
446     protected void flingThenSpringFirstBubbleWithStackFollowing(
447             DynamicAnimation.ViewProperty property,
448             float vel,
449             float friction,
450             SpringForce spring,
451             Float finalPosition) {
452         if (!isActiveController()) {
453             return;
454         }
455 
456         Log.d(TAG, String.format("Flinging %s.",
457                 PhysicsAnimationLayout.getReadablePropertyName(property)));
458 
459         StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
460         final float currentValue = firstBubbleProperty.getValue(this);
461         final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount());
462         final float min =
463                 property.equals(DynamicAnimation.TRANSLATION_X)
464                         ? bounds.left
465                         : bounds.top;
466         final float max =
467                 property.equals(DynamicAnimation.TRANSLATION_X)
468                         ? bounds.right
469                         : bounds.bottom;
470 
471         FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty);
472         flingAnimation.setFriction(friction)
473                 .setStartVelocity(vel)
474 
475                 // If the bubble's property value starts beyond the desired min/max, use that value
476                 // instead so that the animation won't immediately end. If, for example, the user
477                 // drags the bubbles into the navigation bar, but then flings them upward, we want
478                 // the fling to occur despite temporarily having a value outside of the min/max. If
479                 // the bubbles are out of bounds and flung even farther out of bounds, the fling
480                 // animation will halt immediately and the SpringAnimation will take over, springing
481                 // it in reverse to the (legal) final position.
482                 .setMinValue(Math.min(currentValue, min))
483                 .setMaxValue(Math.max(currentValue, max))
484 
485                 .addEndListener((animation, canceled, endValue, endVelocity) -> {
486                     if (!canceled) {
487                         mPositioner.setRestingPosition(mStackPosition);
488 
489                         springFirstBubbleWithStackFollowing(property, spring, endVelocity,
490                                 finalPosition != null
491                                         ? finalPosition
492                                         : Math.max(min, Math.min(max, endValue)));
493                     }
494                 });
495 
496         cancelStackPositionAnimation(property);
497         mStackPositionAnimations.put(property, flingAnimation);
498         flingAnimation.start();
499     }
500 
501     /**
502      * Cancel any stack position animations that were started by calling
503      * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end
504      * listeners.
505      */
506     public void cancelStackPositionAnimations() {
507         cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
508         cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
509 
510         removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
511         removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
512     }
513 
514     /**
515      * Animates the stack either away from the newly visible IME, or back to its original position
516      * due to the IME going away.
517      *
518      * @return The destination Y value of the stack due to the IME movement (or the current position
519      * of the stack if it's not moving).
520      */
521     public float animateForImeVisibility(boolean imeVisible) {
522         final float maxBubbleY = mPositioner.getAllowableStackPositionRegion(
523                 getBubbleCount()).bottom;
524         float destinationY = UNSET;
525 
526         if (imeVisible) {
527             // Stack is lower than it should be and overlaps the now-visible IME.
528             if (mStackPosition.y > maxBubbleY && mPreImeY == UNSET) {
529                 mPreImeY = mStackPosition.y;
530                 destinationY = maxBubbleY;
531             }
532         } else {
533             if (mPreImeY != UNSET) {
534                 destinationY = mPreImeY;
535                 mPreImeY = UNSET;
536             }
537         }
538 
539         if (destinationY != UNSET) {
540             springFirstBubbleWithStackFollowing(
541                     DynamicAnimation.TRANSLATION_Y,
542                     getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
543                             .setStiffness(IME_ANIMATION_STIFFNESS),
544                     /* startVel */ 0f,
545                     destinationY);
546 
547             notifyFloatingCoordinatorStackAnimatingTo(mStackPosition.x, destinationY);
548         }
549 
550         return destinationY != UNSET ? destinationY : mStackPosition.y;
551     }
552 
553     /**
554      * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so
555      * we return these bounds from
556      * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
557      */
558     private void notifyFloatingCoordinatorStackAnimatingTo(float x, float y) {
559         final Rect floatingBounds = mStackFloatingContent.getFloatingBoundsOnScreen();
560         floatingBounds.offsetTo((int) x, (int) y);
561         mAnimatingToBounds = floatingBounds;
562         mFloatingContentCoordinator.onContentMoved(mStackFloatingContent);
563     }
564 
565     /** Moves the stack in response to a touch event. */
566     public void moveStackFromTouch(float x, float y) {
567         // Begin the spring-to-touch catch up animation if needed.
568         if (mSpringToTouchOnNextMotionEvent) {
569             springStack(x, y, SPRING_TO_TOUCH_STIFFNESS);
570             mSpringToTouchOnNextMotionEvent = false;
571             mFirstBubbleSpringingToTouch = true;
572         } else if (mFirstBubbleSpringingToTouch) {
573             final SpringAnimation springToTouchX =
574                     (SpringAnimation) mStackPositionAnimations.get(
575                             DynamicAnimation.TRANSLATION_X);
576             final SpringAnimation springToTouchY =
577                     (SpringAnimation) mStackPositionAnimations.get(
578                             DynamicAnimation.TRANSLATION_Y);
579 
580             // If either animation is still running, we haven't caught up. Update the animations.
581             if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
582                 springToTouchX.animateToFinalPosition(x);
583                 springToTouchY.animateToFinalPosition(y);
584             } else {
585                 // If the animations have finished, the stack is now at the touch point. We can
586                 // resume moving the bubble directly.
587                 mFirstBubbleSpringingToTouch = false;
588             }
589         }
590 
591         if (!mFirstBubbleSpringingToTouch && !isStackStuckToTarget()) {
592             moveFirstBubbleWithStackFollowing(x, y);
593         }
594     }
595 
596     /** Notify the controller that the stack has been unstuck from the dismiss target. */
597     public void onUnstuckFromTarget() {
598         mSpringToTouchOnNextMotionEvent = true;
599     }
600 
601     /**
602      * 'Implode' the stack by shrinking the bubbles, fading them out, and translating them down.
603      */
604     public void animateStackDismissal(float translationYBy, Runnable after) {
605         animationsForChildrenFromIndex(0, (index, animation) ->
606                 animation
607                         .scaleX(0f)
608                         .scaleY(0f)
609                         .alpha(0f)
610                         .translationY(
611                                 mLayout.getChildAt(index).getTranslationY() + translationYBy)
612                         .withStiffness(SpringForce.STIFFNESS_HIGH))
613                 .startAll(after);
614     }
615 
616     /**
617      * Springs the first bubble to the given final position, with the rest of the stack 'following'.
618      */
springFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, SpringForce spring, float vel, float finalPosition, @Nullable Runnable... after)619     protected void springFirstBubbleWithStackFollowing(
620             DynamicAnimation.ViewProperty property, SpringForce spring,
621             float vel, float finalPosition, @Nullable Runnable... after) {
622 
623         if (mLayout.getChildCount() == 0 || !isActiveController()) {
624             return;
625         }
626 
627         Log.d(TAG, String.format("Springing %s to final position %f.",
628                 PhysicsAnimationLayout.getReadablePropertyName(property),
629                 finalPosition));
630 
631         // Whether we're springing towards the touch location, rather than to a position on the
632         // sides of the screen.
633         final boolean isSpringingTowardsTouch = mSpringToTouchOnNextMotionEvent;
634 
635         StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
636         SpringAnimation springAnimation =
637                 new SpringAnimation(this, firstBubbleProperty)
638                         .setSpring(spring)
639                         .addEndListener((dynamicAnimation, b, v, v1) -> {
640                             if (!isSpringingTowardsTouch) {
641                                 // If we're springing towards the touch position, don't save the
642                                 // resting position - the touch location is not a valid resting
643                                 // position. We'll set this when the stack springs to the left or
644                                 // right side of the screen after the touch gesture ends.
645                                 mPositioner.setRestingPosition(mStackPosition);
646                             }
647 
648                             if (mOnStackAnimationFinished != null) {
649                                 mOnStackAnimationFinished.run();
650                             }
651 
652                             if (after != null) {
653                                 for (Runnable callback : after) {
654                                     callback.run();
655                                 }
656                             }
657                         })
658                         .setStartVelocity(vel);
659 
660         cancelStackPositionAnimation(property);
661         mStackPositionAnimations.put(property, springAnimation);
662         springAnimation.animateToFinalPosition(finalPosition);
663     }
664 
665     @Override
getAnimatedProperties()666     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
667         return Sets.newHashSet(
668                 DynamicAnimation.TRANSLATION_X, // For positioning.
669                 DynamicAnimation.TRANSLATION_Y,
670                 DynamicAnimation.ALPHA,         // For fading in new bubbles.
671                 DynamicAnimation.SCALE_X,       // For 'popping in' new bubbles.
672                 DynamicAnimation.SCALE_Y);
673     }
674 
675     @Override
getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)676     int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
677         if (property.equals(DynamicAnimation.TRANSLATION_X)
678                 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
679             return index + 1;
680         } else {
681             return NONE;
682         }
683     }
684 
685 
686     @Override
getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index)687     float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index) {
688         if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
689             // If we're in the dismiss target, have the bubbles pile on top of each other with no
690             // offset.
691             if (isStackStuckToTarget()) {
692                 return 0f;
693             } else {
694                 // We only show the first two bubbles in the stack & the rest hide behind them
695                 // so they don't need an offset.
696                 return index > (NUM_VISIBLE_WHEN_RESTING - 1) ? 0f : mStackOffset;
697             }
698         } else {
699             return 0f;
700         }
701     }
702 
703     @Override
getSpringForce(DynamicAnimation.ViewProperty property, View view)704     SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
705         final ContentResolver contentResolver = mLayout.getContext().getContentResolver();
706         final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping",
707                 DEFAULT_BOUNCINESS);
708 
709         return new SpringForce()
710                 .setDampingRatio(dampingRatio)
711                 .setStiffness(CHAIN_STIFFNESS);
712     }
713 
714     @Override
onChildAdded(View child, int index)715     void onChildAdded(View child, int index) {
716         // Don't animate additions within the dismiss target.
717         if (isStackStuckToTarget()) {
718             return;
719         }
720 
721         if (getBubbleCount() == 1) {
722             // If this is the first child added, position the stack in its starting position.
723             moveStackToStartPosition();
724         } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) {
725             // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
726             // to the back of the stack, it'll be largely invisible so don't bother animating it in.
727             animateInBubble(child, index);
728         } else {
729             // We are not animating the bubble in. Make sure it has the right alpha and scale values
730             // in case this view was previously removed and is being re-added.
731             child.setAlpha(1f);
732             child.setScaleX(1f);
733             child.setScaleY(1f);
734         }
735     }
736 
737     @Override
onChildRemoved(View child, int index, Runnable finishRemoval)738     void onChildRemoved(View child, int index, Runnable finishRemoval) {
739         PhysicsAnimator.getInstance(child)
740                 .spring(DynamicAnimation.ALPHA, 0f)
741                 .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig)
742                 .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig)
743                 .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction)
744                 .start();
745 
746         // If there are other bubbles, pull them into the correct position.
747         if (getBubbleCount() > 0) {
748             animationForChildAtIndex(0).translationX(mStackPosition.x).start();
749         } else {
750             // When all children are removed ensure stack position is sane
751             mPositioner.setRestingPosition(mPositioner.getRestingPosition());
752 
753             // Remove the stack from the coordinator since we don't have any bubbles and aren't
754             // visible.
755             mFloatingContentCoordinator.onContentRemoved(mStackFloatingContent);
756         }
757     }
758 
animateReorder(List<View> bubbleViews, Runnable after)759     public void animateReorder(List<View> bubbleViews, Runnable after) {
760         // After the bubble going to index 0 springs above stack, update all icons
761         // at the same time, to avoid visibly changing bubble order before the animation completes.
762         Runnable updateAllIcons = () -> {
763             for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) {
764                 View view = bubbleViews.get(newIndex);
765                 updateBadgesAndZOrder(view, newIndex);
766             }
767         };
768 
769         boolean swapped = false;
770         for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) {
771             View view = bubbleViews.get(newIndex);
772             if (view != null) {
773                 final int oldIndex = mLayout.indexOfChild(view);
774                 swapped |= animateSwap(view, oldIndex, newIndex, updateAllIcons, after);
775             }
776         }
777         if (!swapped) {
778             // All bubbles were at the right position. Make sure badges and z order is correct.
779             updateAllIcons.run();
780         }
781     }
782 
animateSwap(View view, int oldIndex, int newIndex, Runnable updateAllIcons, Runnable finishReorder)783     private boolean animateSwap(View view, int oldIndex, int newIndex,
784             Runnable updateAllIcons, Runnable finishReorder) {
785         if (newIndex == oldIndex) {
786             // View order did not change. Make sure position is correct.
787             moveToFinalIndex(view, newIndex, finishReorder);
788             return false;
789         } else {
790             // Reorder existing bubbles
791             if (newIndex == 0) {
792                 animateToFrontThenUpdateIcons(view, updateAllIcons, finishReorder);
793             } else {
794                 moveToFinalIndex(view, newIndex, finishReorder);
795             }
796             return true;
797         }
798     }
799 
animateToFrontThenUpdateIcons(View v, Runnable updateAllIcons, Runnable finishReorder)800     private void animateToFrontThenUpdateIcons(View v, Runnable updateAllIcons,
801             Runnable finishReorder) {
802         final ViewPropertyAnimator animator = v.animate()
803                 .translationY(getStackPosition().y - mSwapAnimationOffset)
804                 .setDuration(BUBBLE_SWAP_DURATION)
805                 .withEndAction(() -> {
806                     updateAllIcons.run();
807                     moveToFinalIndex(v, 0 /* index */, finishReorder);
808                 });
809         v.setTag(R.id.reorder_animator_tag, animator);
810     }
811 
moveToFinalIndex(View view, int newIndex, Runnable finishReorder)812     private void moveToFinalIndex(View view, int newIndex,
813             Runnable finishReorder) {
814         final ViewPropertyAnimator animator = view.animate()
815                 .translationY(getStackPosition().y
816                         + Math.min(newIndex, NUM_VISIBLE_WHEN_RESTING - 1) * mStackOffset)
817                 .setDuration(BUBBLE_SWAP_DURATION)
818                 .withEndAction(() -> {
819                     view.setTag(R.id.reorder_animator_tag, null);
820                     finishReorder.run();
821                 });
822         view.setTag(R.id.reorder_animator_tag, animator);
823     }
824 
825     // TODO: do we need this & BubbleStackView#updateBadgesAndZOrder?
updateBadgesAndZOrder(View v, int index)826     private void updateBadgesAndZOrder(View v, int index) {
827         v.setZ(index < NUM_VISIBLE_WHEN_RESTING ? (mMaxBubbles * mElevation) - index : 0f);
828         BadgedImageView bv = (BadgedImageView) v;
829         if (index == 0) {
830             bv.showDotAndBadge(!isStackOnLeftSide());
831         } else {
832             bv.hideDotAndBadge(!isStackOnLeftSide());
833         }
834     }
835 
836     @Override
837     void onChildReordered(View child, int oldIndex, int newIndex) {}
838 
839     @Override
840     void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
841         Resources res = layout.getResources();
842         mStackOffset = mPositioner.getStackOffset();
843         mSwapAnimationOffset = res.getDimensionPixelSize(R.dimen.bubble_swap_animation_offset);
844         mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
845         mElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
846         mBubbleSize = mPositioner.getBubbleSize();
847         mBubblePaddingTop = mPositioner.getBubblePaddingTop();
848     }
849 
850     /**
851      * Update resources.
852      */
853     public void updateResources() {
854         if (mLayout != null) {
855             Resources res = mLayout.getContext().getResources();
856             mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
857             updateFlingToDismissTargetWidth();
858         }
859     }
860 
861     private void updateFlingToDismissTargetWidth() {
862         if (mLayout != null && mMagnetizedStack != null) {
863             int screenWidthPx = mLayout.getResources().getDisplayMetrics().widthPixels;
864             mMagnetizedStack.setFlingToTargetWidthPercent(
865                     getFlingToDismissTargetWidth(screenWidthPx));
866         }
867     }
868 
869     private boolean isStackStuckToTarget() {
870         return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget();
871     }
872 
873     /** Moves the stack, without any animation, to the starting position. */
874     private void moveStackToStartPosition() {
875         // Post to ensure that the layout's width and height have been calculated.
876         mLayout.setVisibility(View.INVISIBLE);
877         mLayout.post(() -> {
878             setStackPosition(mPositioner.getRestingPosition());
879 
880             mStackMovedToStartPosition = true;
881             mLayout.setVisibility(View.VISIBLE);
882 
883             // Animate in the top bubble now that we're visible.
884             if (mLayout.getChildCount() > 0) {
885                 // Add the stack to the floating content coordinator now that we have a bubble and
886                 // are visible.
887                 mFloatingContentCoordinator.onContentAdded(mStackFloatingContent);
888 
889                 animateInBubble(mLayout.getChildAt(0), 0 /* index */);
890             }
891         });
892     }
893 
894     /**
895      * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
896      * bubbles to animate 'following' to the new location.
897      */
moveFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, float value)898     private void moveFirstBubbleWithStackFollowing(
899             DynamicAnimation.ViewProperty property, float value) {
900 
901         // Update the canonical stack position.
902         if (property.equals(DynamicAnimation.TRANSLATION_X)) {
903             mStackPosition.x = value;
904         } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
905             mStackPosition.y = value;
906         }
907 
908         if (mLayout.getChildCount() > 0) {
909             property.setValue(mLayout.getChildAt(0), value);
910             if (mLayout.getChildCount() > 1) {
911                 float newValue = value + getOffsetForChainedPropertyAnimation(property, 0);
912                 animationForChildAtIndex(1)
913                         .property(property, newValue)
914                         .start();
915             }
916         }
917     }
918 
919     /** Moves the stack to a position instantly, with no animation. */
setStackPosition(PointF pos)920     public void setStackPosition(PointF pos) {
921         Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
922         mStackPosition.set(pos.x, pos.y);
923 
924         mPositioner.setRestingPosition(mStackPosition);
925 
926         // If we're not the active controller, we don't want to physically move the bubble views.
927         if (isActiveController()) {
928             // Cancel animations that could be moving the views.
929             mLayout.cancelAllAnimationsOfProperties(
930                     DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
931             cancelStackPositionAnimations();
932 
933             // Since we're not using the chained animations, apply the offsets manually.
934             final float xOffset = getOffsetForChainedPropertyAnimation(
935                     DynamicAnimation.TRANSLATION_X, 0);
936             final float yOffset = getOffsetForChainedPropertyAnimation(
937                     DynamicAnimation.TRANSLATION_Y, 0);
938             for (int i = 0; i < mLayout.getChildCount(); i++) {
939                 float index = Math.min(i, NUM_VISIBLE_WHEN_RESTING - 1);
940                 mLayout.getChildAt(i).setTranslationX(pos.x + (index * xOffset));
941                 mLayout.getChildAt(i).setTranslationY(pos.y + (index * yOffset));
942             }
943         }
944     }
945 
setStackPosition(BubbleStackView.RelativeStackPosition position)946     public void setStackPosition(BubbleStackView.RelativeStackPosition position) {
947         setStackPosition(position.getAbsolutePositionInRegion(
948                 mPositioner.getAllowableStackPositionRegion(getBubbleCount())));
949     }
950 
isStackPositionSet()951     private boolean isStackPositionSet() {
952         return mStackMovedToStartPosition;
953     }
954 
955     /** Animates in the given bubble. */
animateInBubble(View v, int index)956     private void animateInBubble(View v, int index) {
957         if (!isActiveController()) {
958             return;
959         }
960 
961         final float yOffset =
962                 getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y, 0);
963         float endY = mStackPosition.y + yOffset * index;
964         float endX = mStackPosition.x;
965         if (mPositioner.showBubblesVertically()) {
966             v.setTranslationY(endY);
967             final float startX = isStackOnLeftSide()
968                     ? endX - NEW_BUBBLE_START_Y
969                     : endX + NEW_BUBBLE_START_Y;
970             v.setTranslationX(startX);
971         } else {
972             v.setTranslationX(mStackPosition.x);
973             final float startY = endY + NEW_BUBBLE_START_Y;
974             v.setTranslationY(startY);
975         }
976         v.setScaleX(NEW_BUBBLE_START_SCALE);
977         v.setScaleY(NEW_BUBBLE_START_SCALE);
978         v.setAlpha(0f);
979         final ViewPropertyAnimator animator = v.animate()
980                 .scaleX(1f)
981                 .scaleY(1f)
982                 .alpha(1f)
983                 .setDuration(BUBBLE_SWAP_DURATION)
984                 .withEndAction(() -> {
985                     v.setTag(R.id.reorder_animator_tag, null);
986                 });
987         v.setTag(R.id.reorder_animator_tag, animator);
988         if (mPositioner.showBubblesVertically()) {
989             animator.translationX(endX);
990         } else {
991             animator.translationY(endY);
992         }
993     }
994 
995     /**
996      * Cancels any outstanding first bubble property animations that are running. This does not
997      * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
998      * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
999      * {@link #flingThenSpringFirstBubbleWithStackFollowing}.
1000      */
cancelStackPositionAnimation(DynamicAnimation.ViewProperty property)1001     private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
1002         if (mStackPositionAnimations.containsKey(property)) {
1003             mStackPositionAnimations.get(property).cancel();
1004         }
1005     }
1006 
1007     /**
1008      * Returns the {@link MagnetizedObject} instance for the bubble stack.
1009      */
getMagnetizedStack()1010     public MagnetizedObject<StackAnimationController> getMagnetizedStack() {
1011         if (mMagnetizedStack == null) {
1012             mMagnetizedStack = new MagnetizedObject<StackAnimationController>(
1013                     mLayout.getContext(),
1014                     this,
1015                     new StackPositionProperty(DynamicAnimation.TRANSLATION_X),
1016                     new StackPositionProperty(DynamicAnimation.TRANSLATION_Y)
1017             ) {
1018                 @Override
1019                 public float getWidth(@NonNull StackAnimationController underlyingObject) {
1020                     return mBubbleSize;
1021                 }
1022 
1023                 @Override
1024                 public float getHeight(@NonNull StackAnimationController underlyingObject) {
1025                     return mBubbleSize;
1026                 }
1027 
1028                 @Override
1029                 public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject,
1030                         @NonNull int[] loc) {
1031                     loc[0] = (int) mStackPosition.x;
1032                     loc[1] = (int) mStackPosition.y;
1033                 }
1034             };
1035             mMagnetizedStack.setHapticsEnabled(true);
1036             mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
1037             updateFlingToDismissTargetWidth();
1038         }
1039         return mMagnetizedStack;
1040     }
1041 
1042     /** Returns the number of 'real' bubbles (excluding overflow). */
getBubbleCount()1043     private int getBubbleCount() {
1044         return mBubbleCountSupplier.getAsInt();
1045     }
1046 
1047     /**
1048      * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
1049      * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
1050      * property directly to move the first bubble and cause the stack to 'follow' to the new
1051      * location.
1052      *
1053      * <p>This could also be achieved by simply animating the first bubble view and adding an update
1054      * listener to dispatch movement to the rest of the stack. However, this would require
1055      * duplication of logic in that update handler - it's simpler to keep all logic contained in the
1056      * {@link #moveFirstBubbleWithStackFollowing} method.
1057      */
1058     private class StackPositionProperty
1059             extends FloatPropertyCompat<StackAnimationController> {
1060         private final DynamicAnimation.ViewProperty mProperty;
1061 
StackPositionProperty(DynamicAnimation.ViewProperty property)1062         private StackPositionProperty(DynamicAnimation.ViewProperty property) {
1063             super(property.toString());
1064             mProperty = property;
1065         }
1066 
1067         @Override
getValue(StackAnimationController controller)1068         public float getValue(StackAnimationController controller) {
1069             return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
1070         }
1071 
1072         @Override
setValue(StackAnimationController controller, float value)1073         public void setValue(StackAnimationController controller, float value) {
1074             moveFirstBubbleWithStackFollowing(mProperty, value);
1075         }
1076     }
1077 }
1078 
1079