• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.bubbles.animation;
18 
19 import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
20 import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE;
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.animation.PhysicsAnimator;
42 import com.android.wm.shell.bubbles.BadgedImageView;
43 import com.android.wm.shell.bubbles.BubblePositioner;
44 import com.android.wm.shell.bubbles.BubbleStackView;
45 import com.android.wm.shell.common.FloatingContentCoordinator;
46 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
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         if (mLayout == null || !isStackPositionSet()) {
302             return true; // Default to left, which is where it starts by default.
303         }
304         return mPositioner.isStackOnLeft(mStackPosition);
305     }
306 
307     /**
308      * Fling stack to given corner, within allowable screen bounds.
309      * Note that we need new SpringForce instances per animation despite identical configs because
310      * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
311      */
springStack( float destinationX, float destinationY, float stiffness)312     public void springStack(
313             float destinationX, float destinationY, float stiffness) {
314         notifyFloatingCoordinatorStackAnimatingTo(destinationX, destinationY);
315 
316         springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
317                 new SpringForce()
318                         .setStiffness(stiffness)
319                         .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
320                 0 /* startXVelocity */,
321                 destinationX);
322 
323         springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y,
324                 new SpringForce()
325                         .setStiffness(stiffness)
326                         .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
327                 0 /* startYVelocity */,
328                 destinationY);
329     }
330 
331     /**
332      * Springs the stack to the specified x/y coordinates, with the stiffness used for springs after
333      * flings.
334      */
springStackAfterFling(float destinationX, float destinationY)335     public void springStackAfterFling(float destinationX, float destinationY) {
336         springStack(destinationX, destinationY, STACK_SPRING_STIFFNESS);
337     }
338 
339     /**
340      * Flings the stack starting with the given velocities, springing it to the nearest edge
341      * afterward.
342      *
343      * @return The X value that the stack will end up at after the fling/spring.
344      */
flingStackThenSpringToEdge(float x, float velX, float velY)345     public float flingStackThenSpringToEdge(float x, float velX, float velY) {
346         final boolean stackOnLeftSide = x - mBubbleSize / 2 < mLayout.getWidth() / 2;
347 
348         final boolean stackShouldFlingLeft = stackOnLeftSide
349                 ? velX < ESCAPE_VELOCITY
350                 : velX < -ESCAPE_VELOCITY;
351 
352         final RectF stackBounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount());
353 
354         // Target X translation (either the left or right side of the screen).
355         final float destinationRelativeX = stackShouldFlingLeft
356                 ? stackBounds.left : stackBounds.right;
357 
358         // If all bubbles were removed during a drag event, just return the X we would have animated
359         // to if there were still bubbles.
360         if (mLayout == null || mLayout.getChildCount() == 0) {
361             return destinationRelativeX;
362         }
363 
364         final ContentResolver contentResolver = mLayout.getContext().getContentResolver();
365         final float stiffness = Settings.Secure.getFloat(contentResolver, "bubble_stiffness",
366                 STACK_SPRING_STIFFNESS /* default */);
367         final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping",
368                 SPRING_AFTER_FLING_DAMPING_RATIO);
369         final float friction = Settings.Secure.getFloat(contentResolver, "bubble_friction",
370                 FLING_FRICTION);
371 
372         // Minimum velocity required for the stack to make it to the targeted side of the screen,
373         // taking friction into account (4.2f is the number that friction scalars are multiplied by
374         // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
375         // but the SpringAnimation at the end will ensure that it reaches the destination X
376         // regardless.
377         final float minimumVelocityToReachEdge =
378                 (destinationRelativeX - x) * (friction * 4.2f);
379 
380         final float estimatedY = PhysicsAnimator.estimateFlingEndValue(
381                 mStackPosition.y, velY,
382                 new PhysicsAnimator.FlingConfig(
383                         friction, stackBounds.top, stackBounds.bottom));
384 
385         notifyFloatingCoordinatorStackAnimatingTo(destinationRelativeX, estimatedY);
386 
387         // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
388         // that it'll make it all the way to the side of the screen.
389         final float startXVelocity = stackShouldFlingLeft
390                 ? Math.min(minimumVelocityToReachEdge, velX)
391                 : Math.max(minimumVelocityToReachEdge, velX);
392 
393 
394 
395         flingThenSpringFirstBubbleWithStackFollowing(
396                 DynamicAnimation.TRANSLATION_X,
397                 startXVelocity,
398                 friction,
399                 new SpringForce()
400                         .setStiffness(stiffness)
401                         .setDampingRatio(dampingRatio),
402                 destinationRelativeX);
403 
404         flingThenSpringFirstBubbleWithStackFollowing(
405                 DynamicAnimation.TRANSLATION_Y,
406                 velY,
407                 friction,
408                 new SpringForce()
409                         .setStiffness(stiffness)
410                         .setDampingRatio(dampingRatio),
411                 /* destination */ null);
412 
413         // If we're flinging now, there's no more touch event to catch up to.
414         mFirstBubbleSpringingToTouch = false;
415         mIsMovingFromFlinging = true;
416         return destinationRelativeX;
417     }
418 
419     /**
420      * Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
421      */
422     public PointF getStackPositionAlongNearestHorizontalEdge() {
423         final PointF stackPos = getStackPosition();
424         final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x);
425         final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount());
426 
427         stackPos.x = onLeft ? bounds.left : bounds.right;
428         return stackPos;
429     }
430 
431     /** Description of current animation controller state. */
432     public void dump(PrintWriter pw) {
433         pw.println("StackAnimationController state:");
434         pw.print("  isActive:             "); pw.println(isActiveController());
435         pw.print("  restingStackPos:      ");
436         pw.println(mPositioner.getRestingPosition().toString());
437         pw.print("  currentStackPos:      "); pw.println(mStackPosition.toString());
438         pw.print("  isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging);
439         pw.print("  withinDismiss:        "); pw.println(isStackStuckToTarget());
440         pw.print("  firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch);
441     }
442 
443     /**
444      * Flings the first bubble along the given property's axis, using the provided configuration
445      * values. When the animation ends - either by hitting the min/max, or by friction sufficiently
446      * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final
447      * position.
448      */
449     protected void flingThenSpringFirstBubbleWithStackFollowing(
450             DynamicAnimation.ViewProperty property,
451             float vel,
452             float friction,
453             SpringForce spring,
454             Float finalPosition) {
455         if (!isActiveController()) {
456             return;
457         }
458 
459         Log.d(TAG, String.format("Flinging %s.",
460                 PhysicsAnimationLayout.getReadablePropertyName(property)));
461 
462         StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
463         final float currentValue = firstBubbleProperty.getValue(this);
464         final RectF bounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount());
465         final float min =
466                 property.equals(DynamicAnimation.TRANSLATION_X)
467                         ? bounds.left
468                         : bounds.top;
469         final float max =
470                 property.equals(DynamicAnimation.TRANSLATION_X)
471                         ? bounds.right
472                         : bounds.bottom;
473 
474         FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty);
475         flingAnimation.setFriction(friction)
476                 .setStartVelocity(vel)
477 
478                 // If the bubble's property value starts beyond the desired min/max, use that value
479                 // instead so that the animation won't immediately end. If, for example, the user
480                 // drags the bubbles into the navigation bar, but then flings them upward, we want
481                 // the fling to occur despite temporarily having a value outside of the min/max. If
482                 // the bubbles are out of bounds and flung even farther out of bounds, the fling
483                 // animation will halt immediately and the SpringAnimation will take over, springing
484                 // it in reverse to the (legal) final position.
485                 .setMinValue(Math.min(currentValue, min))
486                 .setMaxValue(Math.max(currentValue, max))
487 
488                 .addEndListener((animation, canceled, endValue, endVelocity) -> {
489                     if (!canceled) {
490                         mPositioner.setRestingPosition(mStackPosition);
491 
492                         springFirstBubbleWithStackFollowing(property, spring, endVelocity,
493                                 finalPosition != null
494                                         ? finalPosition
495                                         : Math.max(min, Math.min(max, endValue)));
496                     }
497                 });
498 
499         cancelStackPositionAnimation(property);
500         mStackPositionAnimations.put(property, flingAnimation);
501         flingAnimation.start();
502     }
503 
504     /**
505      * Cancel any stack position animations that were started by calling
506      * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end
507      * listeners.
508      */
509     public void cancelStackPositionAnimations() {
510         cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
511         cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
512 
513         removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
514         removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
515     }
516 
517     /**
518      * Animates the stack either away from the newly visible IME, or back to its original position
519      * due to the IME going away.
520      *
521      * @return The destination Y value of the stack due to the IME movement (or the current position
522      * of the stack if it's not moving).
523      */
524     public float animateForImeVisibility(boolean imeVisible) {
525         final float maxBubbleY = mPositioner.getAllowableStackPositionRegion(
526                 getBubbleCount()).bottom;
527         float destinationY = UNSET;
528 
529         if (imeVisible) {
530             // Stack is lower than it should be and overlaps the now-visible IME.
531             if (mStackPosition.y > maxBubbleY && mPreImeY == UNSET) {
532                 mPreImeY = mStackPosition.y;
533                 destinationY = maxBubbleY;
534             }
535         } else {
536             if (mPreImeY != UNSET) {
537                 destinationY = mPreImeY;
538                 mPreImeY = UNSET;
539             }
540         }
541 
542         if (destinationY != UNSET) {
543             springFirstBubbleWithStackFollowing(
544                     DynamicAnimation.TRANSLATION_Y,
545                     getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
546                             .setStiffness(IME_ANIMATION_STIFFNESS),
547                     /* startVel */ 0f,
548                     destinationY);
549 
550             notifyFloatingCoordinatorStackAnimatingTo(mStackPosition.x, destinationY);
551         }
552 
553         return destinationY != UNSET ? destinationY : mStackPosition.y;
554     }
555 
556     /**
557      * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so
558      * we return these bounds from
559      * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
560      */
561     private void notifyFloatingCoordinatorStackAnimatingTo(float x, float y) {
562         final Rect floatingBounds = mStackFloatingContent.getFloatingBoundsOnScreen();
563         floatingBounds.offsetTo((int) x, (int) y);
564         mAnimatingToBounds = floatingBounds;
565         mFloatingContentCoordinator.onContentMoved(mStackFloatingContent);
566     }
567 
568     /** Moves the stack in response to a touch event. */
569     public void moveStackFromTouch(float x, float y) {
570         // Begin the spring-to-touch catch up animation if needed.
571         if (mSpringToTouchOnNextMotionEvent) {
572             springStack(x, y, SPRING_TO_TOUCH_STIFFNESS);
573             mSpringToTouchOnNextMotionEvent = false;
574             mFirstBubbleSpringingToTouch = true;
575         } else if (mFirstBubbleSpringingToTouch) {
576             final SpringAnimation springToTouchX =
577                     (SpringAnimation) mStackPositionAnimations.get(
578                             DynamicAnimation.TRANSLATION_X);
579             final SpringAnimation springToTouchY =
580                     (SpringAnimation) mStackPositionAnimations.get(
581                             DynamicAnimation.TRANSLATION_Y);
582 
583             // If either animation is still running, we haven't caught up. Update the animations.
584             if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
585                 springToTouchX.animateToFinalPosition(x);
586                 springToTouchY.animateToFinalPosition(y);
587             } else {
588                 // If the animations have finished, the stack is now at the touch point. We can
589                 // resume moving the bubble directly.
590                 mFirstBubbleSpringingToTouch = false;
591             }
592         }
593 
594         if (!mFirstBubbleSpringingToTouch && !isStackStuckToTarget()) {
595             moveFirstBubbleWithStackFollowing(x, y);
596         }
597     }
598 
599     /** Notify the controller that the stack has been unstuck from the dismiss target. */
600     public void onUnstuckFromTarget() {
601         mSpringToTouchOnNextMotionEvent = true;
602     }
603 
604     /**
605      * 'Implode' the stack by shrinking the bubbles, fading them out, and translating them down.
606      */
607     public void animateStackDismissal(float translationYBy, Runnable after) {
608         animationsForChildrenFromIndex(0, (index, animation) ->
609                 animation
610                         .scaleX(0f)
611                         .scaleY(0f)
612                         .alpha(0f)
613                         .translationY(
614                                 mLayout.getChildAt(index).getTranslationY() + translationYBy)
615                         .withStiffness(SpringForce.STIFFNESS_HIGH))
616                 .startAll(after);
617     }
618 
619     /**
620      * Springs the first bubble to the given final position, with the rest of the stack 'following'.
621      */
springFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, SpringForce spring, float vel, float finalPosition, @Nullable Runnable... after)622     protected void springFirstBubbleWithStackFollowing(
623             DynamicAnimation.ViewProperty property, SpringForce spring,
624             float vel, float finalPosition, @Nullable Runnable... after) {
625 
626         if (mLayout.getChildCount() == 0 || !isActiveController()) {
627             return;
628         }
629 
630         Log.d(TAG, String.format("Springing %s to final position %f.",
631                 PhysicsAnimationLayout.getReadablePropertyName(property),
632                 finalPosition));
633 
634         // Whether we're springing towards the touch location, rather than to a position on the
635         // sides of the screen.
636         final boolean isSpringingTowardsTouch = mSpringToTouchOnNextMotionEvent;
637 
638         StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
639         SpringAnimation springAnimation =
640                 new SpringAnimation(this, firstBubbleProperty)
641                         .setSpring(spring)
642                         .addEndListener((dynamicAnimation, b, v, v1) -> {
643                             if (!isSpringingTowardsTouch) {
644                                 // If we're springing towards the touch position, don't save the
645                                 // resting position - the touch location is not a valid resting
646                                 // position. We'll set this when the stack springs to the left or
647                                 // right side of the screen after the touch gesture ends.
648                                 mPositioner.setRestingPosition(mStackPosition);
649                             }
650 
651                             if (mOnStackAnimationFinished != null) {
652                                 mOnStackAnimationFinished.run();
653                             }
654 
655                             if (after != null) {
656                                 for (Runnable callback : after) {
657                                     callback.run();
658                                 }
659                             }
660                         })
661                         .setStartVelocity(vel);
662 
663         cancelStackPositionAnimation(property);
664         mStackPositionAnimations.put(property, springAnimation);
665         springAnimation.animateToFinalPosition(finalPosition);
666     }
667 
668     @Override
getAnimatedProperties()669     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
670         return Sets.newHashSet(
671                 DynamicAnimation.TRANSLATION_X, // For positioning.
672                 DynamicAnimation.TRANSLATION_Y,
673                 DynamicAnimation.ALPHA,         // For fading in new bubbles.
674                 DynamicAnimation.SCALE_X,       // For 'popping in' new bubbles.
675                 DynamicAnimation.SCALE_Y);
676     }
677 
678     @Override
getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)679     int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
680         if (property.equals(DynamicAnimation.TRANSLATION_X)
681                 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
682             return index + 1;
683         } else {
684             return NONE;
685         }
686     }
687 
688 
689     @Override
getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index)690     float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index) {
691         if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
692             // If we're in the dismiss target, have the bubbles pile on top of each other with no
693             // offset.
694             if (isStackStuckToTarget()) {
695                 return 0f;
696             } else {
697                 // We only show the first two bubbles in the stack & the rest hide behind them
698                 // so they don't need an offset.
699                 return index > (NUM_VISIBLE_WHEN_RESTING - 1) ? 0f : mStackOffset;
700             }
701         } else {
702             return 0f;
703         }
704     }
705 
706     @Override
getSpringForce(DynamicAnimation.ViewProperty property, View view)707     SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
708         final ContentResolver contentResolver = mLayout.getContext().getContentResolver();
709         final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping",
710                 DEFAULT_BOUNCINESS);
711 
712         return new SpringForce()
713                 .setDampingRatio(dampingRatio)
714                 .setStiffness(CHAIN_STIFFNESS);
715     }
716 
717     @Override
onChildAdded(View child, int index)718     void onChildAdded(View child, int index) {
719         // Don't animate additions within the dismiss target.
720         if (isStackStuckToTarget()) {
721             return;
722         }
723 
724         if (getBubbleCount() == 1) {
725             // If this is the first child added, position the stack in its starting position.
726             moveStackToStartPosition();
727         } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) {
728             // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
729             // to the back of the stack, it'll be largely invisible so don't bother animating it in.
730             animateInBubble(child, index);
731         } else {
732             // We are not animating the bubble in. Make sure it has the right alpha and scale values
733             // in case this view was previously removed and is being re-added.
734             child.setAlpha(1f);
735             child.setScaleX(1f);
736             child.setScaleY(1f);
737         }
738     }
739 
740     @Override
onChildRemoved(View child, int index, Runnable finishRemoval)741     void onChildRemoved(View child, int index, Runnable finishRemoval) {
742         PhysicsAnimator.getInstance(child)
743                 .spring(DynamicAnimation.ALPHA, 0f)
744                 .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig)
745                 .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig)
746                 .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction)
747                 .start();
748 
749         // If there are other bubbles, pull them into the correct position.
750         if (getBubbleCount() > 0) {
751             animationForChildAtIndex(0).translationX(mStackPosition.x).start();
752         } else {
753             // When all children are removed ensure stack position is sane
754             mPositioner.setRestingPosition(mPositioner.getRestingPosition());
755 
756             // Remove the stack from the coordinator since we don't have any bubbles and aren't
757             // visible.
758             mFloatingContentCoordinator.onContentRemoved(mStackFloatingContent);
759         }
760     }
761 
animateReorder(List<View> bubbleViews, Runnable after)762     public void animateReorder(List<View> bubbleViews, Runnable after) {
763         // After the bubble going to index 0 springs above stack, update all icons
764         // at the same time, to avoid visibly changing bubble order before the animation completes.
765         Runnable updateAllIcons = () -> {
766             for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) {
767                 View view = bubbleViews.get(newIndex);
768                 updateBadgesAndZOrder(view, newIndex);
769             }
770         };
771 
772         boolean swapped = false;
773         for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) {
774             View view = bubbleViews.get(newIndex);
775             final int oldIndex = mLayout.indexOfChild(view);
776             swapped |= animateSwap(view, oldIndex, newIndex, updateAllIcons, after);
777         }
778         if (!swapped) {
779             // All bubbles were at the right position. Make sure badges and z order is correct.
780             updateAllIcons.run();
781         }
782     }
783 
animateSwap(View view, int oldIndex, int newIndex, Runnable updateAllIcons, Runnable finishReorder)784     private boolean animateSwap(View view, int oldIndex, int newIndex,
785             Runnable updateAllIcons, Runnable finishReorder) {
786         if (newIndex == oldIndex) {
787             // View order did not change. Make sure position is correct.
788             moveToFinalIndex(view, newIndex, finishReorder);
789             return false;
790         } else {
791             // Reorder existing bubbles
792             if (newIndex == 0) {
793                 animateToFrontThenUpdateIcons(view, updateAllIcons, finishReorder);
794             } else {
795                 moveToFinalIndex(view, newIndex, finishReorder);
796             }
797             return true;
798         }
799     }
800 
animateToFrontThenUpdateIcons(View v, Runnable updateAllIcons, Runnable finishReorder)801     private void animateToFrontThenUpdateIcons(View v, Runnable updateAllIcons,
802             Runnable finishReorder) {
803         final ViewPropertyAnimator animator = v.animate()
804                 .translationY(getStackPosition().y - mSwapAnimationOffset)
805                 .setDuration(BUBBLE_SWAP_DURATION)
806                 .withEndAction(() -> {
807                     updateAllIcons.run();
808                     moveToFinalIndex(v, 0 /* index */, finishReorder);
809                 });
810         v.setTag(R.id.reorder_animator_tag, animator);
811     }
812 
moveToFinalIndex(View view, int newIndex, Runnable finishReorder)813     private void moveToFinalIndex(View view, int newIndex,
814             Runnable finishReorder) {
815         final ViewPropertyAnimator animator = view.animate()
816                 .translationY(getStackPosition().y
817                         + Math.min(newIndex, NUM_VISIBLE_WHEN_RESTING - 1) * mStackOffset)
818                 .setDuration(BUBBLE_SWAP_DURATION)
819                 .withEndAction(() -> {
820                     view.setTag(R.id.reorder_animator_tag, null);
821                     finishReorder.run();
822                 });
823         view.setTag(R.id.reorder_animator_tag, animator);
824     }
825 
826     // TODO: do we need this & BubbleStackView#updateBadgesAndZOrder?
updateBadgesAndZOrder(View v, int index)827     private void updateBadgesAndZOrder(View v, int index) {
828         v.setZ(index < NUM_VISIBLE_WHEN_RESTING ? (mMaxBubbles * mElevation) - index : 0f);
829         BadgedImageView bv = (BadgedImageView) v;
830         if (index == 0) {
831             bv.showDotAndBadge(!isStackOnLeftSide());
832         } else {
833             bv.hideDotAndBadge(!isStackOnLeftSide());
834         }
835     }
836 
837     @Override
838     void onChildReordered(View child, int oldIndex, int newIndex) {}
839 
840     @Override
841     void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
842         Resources res = layout.getResources();
843         mStackOffset = mPositioner.getStackOffset();
844         mSwapAnimationOffset = res.getDimensionPixelSize(R.dimen.bubble_swap_animation_offset);
845         mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
846         mElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
847         mBubbleSize = mPositioner.getBubbleSize();
848         mBubblePaddingTop = mPositioner.getBubblePaddingTop();
849     }
850 
851     /**
852      * Update resources.
853      */
854     public void updateResources() {
855         if (mLayout != null) {
856             Resources res = mLayout.getContext().getResources();
857             mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
858         }
859     }
860 
861     private boolean isStackStuckToTarget() {
862         return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget();
863     }
864 
865     /** Moves the stack, without any animation, to the starting position. */
866     private void moveStackToStartPosition() {
867         // Post to ensure that the layout's width and height have been calculated.
868         mLayout.setVisibility(View.INVISIBLE);
869         mLayout.post(() -> {
870             setStackPosition(mPositioner.getRestingPosition());
871 
872             mStackMovedToStartPosition = true;
873             mLayout.setVisibility(View.VISIBLE);
874 
875             // Animate in the top bubble now that we're visible.
876             if (mLayout.getChildCount() > 0) {
877                 // Add the stack to the floating content coordinator now that we have a bubble and
878                 // are visible.
879                 mFloatingContentCoordinator.onContentAdded(mStackFloatingContent);
880 
881                 animateInBubble(mLayout.getChildAt(0), 0 /* index */);
882             }
883         });
884     }
885 
886     /**
887      * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
888      * bubbles to animate 'following' to the new location.
889      */
moveFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, float value)890     private void moveFirstBubbleWithStackFollowing(
891             DynamicAnimation.ViewProperty property, float value) {
892 
893         // Update the canonical stack position.
894         if (property.equals(DynamicAnimation.TRANSLATION_X)) {
895             mStackPosition.x = value;
896         } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
897             mStackPosition.y = value;
898         }
899 
900         if (mLayout.getChildCount() > 0) {
901             property.setValue(mLayout.getChildAt(0), value);
902             if (mLayout.getChildCount() > 1) {
903                 float newValue = value + getOffsetForChainedPropertyAnimation(property, 0);
904                 animationForChildAtIndex(1)
905                         .property(property, newValue)
906                         .start();
907             }
908         }
909     }
910 
911     /** Moves the stack to a position instantly, with no animation. */
setStackPosition(PointF pos)912     public void setStackPosition(PointF pos) {
913         Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
914         mStackPosition.set(pos.x, pos.y);
915 
916         mPositioner.setRestingPosition(mStackPosition);
917 
918         // If we're not the active controller, we don't want to physically move the bubble views.
919         if (isActiveController()) {
920             // Cancel animations that could be moving the views.
921             mLayout.cancelAllAnimationsOfProperties(
922                     DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
923             cancelStackPositionAnimations();
924 
925             // Since we're not using the chained animations, apply the offsets manually.
926             final float xOffset = getOffsetForChainedPropertyAnimation(
927                     DynamicAnimation.TRANSLATION_X, 0);
928             final float yOffset = getOffsetForChainedPropertyAnimation(
929                     DynamicAnimation.TRANSLATION_Y, 0);
930             for (int i = 0; i < mLayout.getChildCount(); i++) {
931                 float index = Math.min(i, NUM_VISIBLE_WHEN_RESTING - 1);
932                 mLayout.getChildAt(i).setTranslationX(pos.x + (index * xOffset));
933                 mLayout.getChildAt(i).setTranslationY(pos.y + (index * yOffset));
934             }
935         }
936     }
937 
setStackPosition(BubbleStackView.RelativeStackPosition position)938     public void setStackPosition(BubbleStackView.RelativeStackPosition position) {
939         setStackPosition(position.getAbsolutePositionInRegion(
940                 mPositioner.getAllowableStackPositionRegion(getBubbleCount())));
941     }
942 
isStackPositionSet()943     private boolean isStackPositionSet() {
944         return mStackMovedToStartPosition;
945     }
946 
947     /** Animates in the given bubble. */
animateInBubble(View v, int index)948     private void animateInBubble(View v, int index) {
949         if (!isActiveController()) {
950             return;
951         }
952 
953         final float yOffset =
954                 getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y, 0);
955         float endY = mStackPosition.y + yOffset * index;
956         float endX = mStackPosition.x;
957         if (mPositioner.showBubblesVertically()) {
958             v.setTranslationY(endY);
959             final float startX = isStackOnLeftSide()
960                     ? endX - NEW_BUBBLE_START_Y
961                     : endX + NEW_BUBBLE_START_Y;
962             v.setTranslationX(startX);
963         } else {
964             v.setTranslationX(mStackPosition.x);
965             final float startY = endY + NEW_BUBBLE_START_Y;
966             v.setTranslationY(startY);
967         }
968         v.setScaleX(NEW_BUBBLE_START_SCALE);
969         v.setScaleY(NEW_BUBBLE_START_SCALE);
970         v.setAlpha(0f);
971         final ViewPropertyAnimator animator = v.animate()
972                 .scaleX(1f)
973                 .scaleY(1f)
974                 .alpha(1f)
975                 .setDuration(BUBBLE_SWAP_DURATION)
976                 .withEndAction(() -> {
977                     v.setTag(R.id.reorder_animator_tag, null);
978                 });
979         v.setTag(R.id.reorder_animator_tag, animator);
980         if (mPositioner.showBubblesVertically()) {
981             animator.translationX(endX);
982         } else {
983             animator.translationY(endY);
984         }
985     }
986 
987     /**
988      * Cancels any outstanding first bubble property animations that are running. This does not
989      * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
990      * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
991      * {@link #flingThenSpringFirstBubbleWithStackFollowing}.
992      */
cancelStackPositionAnimation(DynamicAnimation.ViewProperty property)993     private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
994         if (mStackPositionAnimations.containsKey(property)) {
995             mStackPositionAnimations.get(property).cancel();
996         }
997     }
998 
999     /**
1000      * Returns the {@link MagnetizedObject} instance for the bubble stack.
1001      */
getMagnetizedStack()1002     public MagnetizedObject<StackAnimationController> getMagnetizedStack() {
1003         if (mMagnetizedStack == null) {
1004             mMagnetizedStack = new MagnetizedObject<StackAnimationController>(
1005                     mLayout.getContext(),
1006                     this,
1007                     new StackPositionProperty(DynamicAnimation.TRANSLATION_X),
1008                     new StackPositionProperty(DynamicAnimation.TRANSLATION_Y)
1009             ) {
1010                 @Override
1011                 public float getWidth(@NonNull StackAnimationController underlyingObject) {
1012                     return mBubbleSize;
1013                 }
1014 
1015                 @Override
1016                 public float getHeight(@NonNull StackAnimationController underlyingObject) {
1017                     return mBubbleSize;
1018                 }
1019 
1020                 @Override
1021                 public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject,
1022                         @NonNull int[] loc) {
1023                     loc[0] = (int) mStackPosition.x;
1024                     loc[1] = (int) mStackPosition.y;
1025                 }
1026             };
1027             mMagnetizedStack.setHapticsEnabled(true);
1028             mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
1029             mMagnetizedStack.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
1030         }
1031 
1032         final ContentResolver contentResolver = mLayout.getContext().getContentResolver();
1033         final float minVelocity = Settings.Secure.getFloat(contentResolver,
1034                 "bubble_dismiss_fling_min_velocity",
1035                 mMagnetizedStack.getFlingToTargetMinVelocity() /* default */);
1036         final float maxVelocity = Settings.Secure.getFloat(contentResolver,
1037                 "bubble_dismiss_stick_max_velocity",
1038                 mMagnetizedStack.getStickToTargetMaxXVelocity() /* default */);
1039         final float targetWidth = Settings.Secure.getFloat(contentResolver,
1040                 "bubble_dismiss_target_width_percent",
1041                 mMagnetizedStack.getFlingToTargetWidthPercent() /* default */);
1042 
1043         mMagnetizedStack.setFlingToTargetMinVelocity(minVelocity);
1044         mMagnetizedStack.setStickToTargetMaxXVelocity(maxVelocity);
1045         mMagnetizedStack.setFlingToTargetWidthPercent(targetWidth);
1046 
1047         return mMagnetizedStack;
1048     }
1049 
1050     /** Returns the number of 'real' bubbles (excluding overflow). */
getBubbleCount()1051     private int getBubbleCount() {
1052         return mBubbleCountSupplier.getAsInt();
1053     }
1054 
1055     /**
1056      * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
1057      * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
1058      * property directly to move the first bubble and cause the stack to 'follow' to the new
1059      * location.
1060      *
1061      * This could also be achieved by simply animating the first bubble view and adding an update
1062      * listener to dispatch movement to the rest of the stack. However, this would require
1063      * duplication of logic in that update handler - it's simpler to keep all logic contained in the
1064      * {@link #moveFirstBubbleWithStackFollowing} method.
1065      */
1066     private class StackPositionProperty
1067             extends FloatPropertyCompat<StackAnimationController> {
1068         private final DynamicAnimation.ViewProperty mProperty;
1069 
StackPositionProperty(DynamicAnimation.ViewProperty property)1070         private StackPositionProperty(DynamicAnimation.ViewProperty property) {
1071             super(property.toString());
1072             mProperty = property;
1073         }
1074 
1075         @Override
getValue(StackAnimationController controller)1076         public float getValue(StackAnimationController controller) {
1077             return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
1078         }
1079 
1080         @Override
setValue(StackAnimationController controller, float value)1081         public void setValue(StackAnimationController controller, float value) {
1082             moveFirstBubbleWithStackFollowing(mProperty, value);
1083         }
1084     }
1085 }
1086 
1087