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