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