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