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