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