• 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.res.Resources;
20 import android.graphics.Point;
21 import android.graphics.PointF;
22 import android.view.View;
23 import android.view.WindowInsets;
24 
25 import androidx.annotation.Nullable;
26 import androidx.dynamicanimation.animation.DynamicAnimation;
27 import androidx.dynamicanimation.animation.SpringForce;
28 
29 import com.android.systemui.R;
30 
31 import com.google.android.collect.Sets;
32 
33 import java.util.Set;
34 
35 /**
36  * Animation controller for bubbles when they're in their expanded state, or animating to/from the
37  * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be
38  * dismissed.
39  */
40 public class ExpandedAnimationController
41         extends PhysicsAnimationLayout.PhysicsAnimationController {
42 
43     /**
44      * How much to translate the bubbles when they're animating in/out. This value is multiplied by
45      * the bubble size.
46      */
47     private static final int ANIMATE_TRANSLATION_FACTOR = 4;
48 
49     /** How much to scale down bubbles when they're animating in/out. */
50     private static final float ANIMATE_SCALE_PERCENT = 0.5f;
51 
52     /** The stack position to collapse back to in {@link #collapseBackToStack}. */
53     private PointF mCollapseToPoint;
54 
55     /** Horizontal offset between bubbles, which we need to know to re-stack them. */
56     private float mStackOffsetPx;
57     /** Spacing between bubbles in the expanded state. */
58     private float mBubblePaddingPx;
59     /** Size of each bubble. */
60     private float mBubbleSizePx;
61     /** Height of the status bar. */
62     private float mStatusBarHeight;
63     /** Size of display. */
64     private Point mDisplaySize;
65     /** Size of dismiss target at bottom of screen. */
66     private float mPipDismissHeight;
67 
68     /** Whether the dragged-out bubble is in the dismiss target. */
69     private boolean mIndividualBubbleWithinDismissTarget = false;
70 
71     private boolean mAnimatingExpand = false;
72     private boolean mAnimatingCollapse = false;
73     private Runnable mAfterExpand;
74     private Runnable mAfterCollapse;
75     private PointF mCollapsePoint;
76 
77     /**
78      * Whether the dragged out bubble is springing towards the touch point, rather than using the
79      * default behavior of moving directly to the touch point.
80      *
81      * This happens when the user's finger exits the dismiss area while the bubble is magnetized to
82      * the center. Since the touch point differs from the bubble location, we need to animate the
83      * bubble back to the touch point to avoid a jarring instant location change from the center of
84      * the target to the touch point just outside the target bounds.
85      */
86     private boolean mSpringingBubbleToTouch = false;
87 
88     private int mExpandedViewPadding;
89 
ExpandedAnimationController(Point displaySize, int expandedViewPadding)90     public ExpandedAnimationController(Point displaySize, int expandedViewPadding) {
91         mDisplaySize = displaySize;
92         mExpandedViewPadding = expandedViewPadding;
93     }
94 
95     /**
96      * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
97      * the rest of the bubbles to animate to fill the gap.
98      */
99     private boolean mBubbleDraggedOutEnough = false;
100 
101     /** The bubble currently being dragged out of the row (to potentially be dismissed). */
102     private View mBubbleDraggingOut;
103 
104     /**
105      * Animates expanding the bubbles into a row along the top of the screen.
106      */
expandFromStack(Runnable after)107     public void expandFromStack(Runnable after) {
108         mAnimatingCollapse = false;
109         mAnimatingExpand = true;
110         mAfterExpand = after;
111 
112         startOrUpdateExpandAnimation();
113     }
114 
115     /** Animate collapsing the bubbles back to their stacked position. */
collapseBackToStack(PointF collapsePoint, Runnable after)116     public void collapseBackToStack(PointF collapsePoint, Runnable after) {
117         mAnimatingExpand = false;
118         mAnimatingCollapse = true;
119         mAfterCollapse = after;
120         mCollapsePoint = collapsePoint;
121 
122         startOrUpdateCollapseAnimation();
123     }
124 
startOrUpdateExpandAnimation()125     private void startOrUpdateExpandAnimation() {
126         animationsForChildrenFromIndex(
127                 0, /* startIndex */
128                 (index, animation) -> animation.position(getBubbleLeft(index), getExpandedY()))
129                 .startAll(() -> {
130                     mAnimatingExpand = false;
131 
132                     if (mAfterExpand != null) {
133                         mAfterExpand.run();
134                     }
135 
136                     mAfterExpand = null;
137                 });
138     }
139 
startOrUpdateCollapseAnimation()140     private void startOrUpdateCollapseAnimation() {
141         // Stack to the left if we're going to the left, or right if not.
142         final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
143         animationsForChildrenFromIndex(
144                 0, /* startIndex */
145                 (index, animation) -> {
146                     animation.position(
147                             mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx),
148                             mCollapsePoint.y);
149                 })
150                 .startAll(() -> {
151                     mAnimatingCollapse = false;
152 
153                     if (mAfterCollapse != null) {
154                         mAfterCollapse.run();
155                     }
156 
157                     mAfterCollapse = null;
158                 });
159     }
160 
161     /** Prepares the given bubble to be dragged out. */
prepareForBubbleDrag(View bubble)162     public void prepareForBubbleDrag(View bubble) {
163         mLayout.cancelAnimationsOnView(bubble);
164 
165         mBubbleDraggingOut = bubble;
166         mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE);
167     }
168 
169     /**
170      * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
171      * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
172      * bubble is dragged back into the row.
173      */
dragBubbleOut(View bubbleView, float x, float y)174     public void dragBubbleOut(View bubbleView, float x, float y) {
175         if (mSpringingBubbleToTouch) {
176             if (mLayout.arePropertiesAnimatingOnView(
177                     bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
178                 animationForChild(mBubbleDraggingOut)
179                         .translationX(x)
180                         .translationY(y)
181                         .withStiffness(SpringForce.STIFFNESS_HIGH)
182                         .start();
183             } else {
184                 mSpringingBubbleToTouch = false;
185             }
186         }
187 
188         if (!mSpringingBubbleToTouch && !mIndividualBubbleWithinDismissTarget) {
189             bubbleView.setTranslationX(x);
190             bubbleView.setTranslationY(y);
191         }
192 
193         final boolean draggedOutEnough =
194                 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
195         if (draggedOutEnough != mBubbleDraggedOutEnough) {
196             updateBubblePositions();
197             mBubbleDraggedOutEnough = draggedOutEnough;
198         }
199     }
200 
201     /** Plays a dismiss animation on the dragged out bubble. */
202     public void dismissDraggedOutBubble(View bubble, Runnable after) {
203         mIndividualBubbleWithinDismissTarget = false;
204 
205         animationForChild(bubble)
206                 .withStiffness(SpringForce.STIFFNESS_HIGH)
207                 .scaleX(1.1f)
208                 .scaleY(1.1f)
209                 .alpha(0f, after)
210                 .start();
211 
212         updateBubblePositions();
213     }
214 
215     @Nullable public View getDraggedOutBubble() {
216         return mBubbleDraggingOut;
217     }
218 
219     /** Magnets the given bubble to the dismiss target. */
220     public void magnetBubbleToDismiss(
221             View bubbleView, float velX, float velY, float destY, Runnable after) {
222         mIndividualBubbleWithinDismissTarget = true;
223         mSpringingBubbleToTouch = false;
224         animationForChild(bubbleView)
225                 .withStiffness(SpringForce.STIFFNESS_MEDIUM)
226                 .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
227                 .withPositionStartVelocities(velX, velY)
228                 .translationX(mLayout.getWidth() / 2f - mBubbleSizePx / 2f)
229                 .translationY(destY, after)
230                 .start();
231     }
232 
233     /**
234      * Springs the dragged-out bubble towards the given coordinates and sets flags to have touch
235      * events update the spring's final position until it's settled.
236      */
237     public void demagnetizeBubbleTo(float x, float y, float velX, float velY) {
238         mIndividualBubbleWithinDismissTarget = false;
239         mSpringingBubbleToTouch = true;
240 
241         animationForChild(mBubbleDraggingOut)
242                 .translationX(x)
243                 .translationY(y)
244                 .withPositionStartVelocities(velX, velY)
245                 .withStiffness(SpringForce.STIFFNESS_HIGH)
246                 .start();
247     }
248 
249     /**
250      * Snaps a bubble back to its position within the bubble row, and animates the rest of the
251      * bubbles to accommodate it if it was previously dragged out past the threshold.
252      */
253     public void snapBubbleBack(View bubbleView, float velX, float velY) {
254         final int index = mLayout.indexOfChild(bubbleView);
255 
256         animationForChildAtIndex(index)
257                 .position(getBubbleLeft(index), getExpandedY())
258                 .withPositionStartVelocities(velX, velY)
259                 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
260 
261         updateBubblePositions();
262     }
263 
264     /** Resets bubble drag out gesture flags. */
onGestureFinished()265     public void onGestureFinished() {
266         mBubbleDraggedOutEnough = false;
267         mBubbleDraggingOut = null;
268     }
269 
270     /**
271      * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
272      */
updateYPosition(Runnable after)273     public void updateYPosition(Runnable after) {
274         if (mLayout == null) return;
275         animationsForChildrenFromIndex(
276                 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
277     }
278 
279     /**
280      * Animates the bubbles, starting at the given index, to the left or right by the given number
281      * of bubble widths. Passing zero for numBubbleWidths will animate the bubbles to their normal
282      * positions.
283      */
animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex)284     private void animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex) {
285         animationsForChildrenFromIndex(
286                 startIndex,
287                 (index, animation) ->
288                         animation.translationX(getXForChildAtIndex(index + numBubbleWidths)))
289             .startAll();
290     }
291 
292     /** The Y value of the row of expanded bubbles. */
getExpandedY()293     public float getExpandedY() {
294         if (mLayout == null || mLayout.getRootWindowInsets() == null) {
295             return 0;
296         }
297         final WindowInsets insets = mLayout.getRootWindowInsets();
298         return mBubblePaddingPx + Math.max(
299             mStatusBarHeight,
300             insets.getDisplayCutout() != null
301                 ? insets.getDisplayCutout().getSafeInsetTop()
302                 : 0);
303     }
304 
305     @Override
onActiveControllerForLayout(PhysicsAnimationLayout layout)306     void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
307         final Resources res = layout.getResources();
308         mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
309         mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding);
310         mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
311         mStatusBarHeight =
312                 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
313         mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height);
314 
315         // Ensure that all child views are at 1x scale, and visible, in case they were animating
316         // in.
317         mLayout.setVisibility(View.VISIBLE);
318         animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
319                 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
320     }
321 
322     @Override
getAnimatedProperties()323     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
324         return Sets.newHashSet(
325                 DynamicAnimation.TRANSLATION_X,
326                 DynamicAnimation.TRANSLATION_Y,
327                 DynamicAnimation.SCALE_X,
328                 DynamicAnimation.SCALE_Y,
329                 DynamicAnimation.ALPHA);
330     }
331 
332     @Override
getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)333     int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
334         return NONE;
335     }
336 
337     @Override
getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property)338     float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
339         return 0;
340     }
341 
342     @Override
getSpringForce(DynamicAnimation.ViewProperty property, View view)343     SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
344         return new SpringForce()
345                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
346                 .setStiffness(SpringForce.STIFFNESS_LOW);
347     }
348 
349     @Override
onChildAdded(View child, int index)350     void onChildAdded(View child, int index) {
351         // If a bubble is added while the expand/collapse animations are playing, update the
352         // animation to include the new bubble.
353         if (mAnimatingExpand) {
354             startOrUpdateExpandAnimation();
355         } else if (mAnimatingCollapse) {
356             startOrUpdateCollapseAnimation();
357         } else {
358             child.setTranslationX(getXForChildAtIndex(index));
359             animationForChild(child)
360                     .translationY(
361                             getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
362                             getExpandedY() /* to */)
363                     .start();
364             updateBubblePositions();
365         }
366     }
367 
368     @Override
onChildRemoved(View child, int index, Runnable finishRemoval)369     void onChildRemoved(View child, int index, Runnable finishRemoval) {
370         final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
371 
372         // If we're removing the dragged-out bubble, that means it got dismissed.
373         if (child.equals(mBubbleDraggingOut)) {
374             mBubbleDraggingOut = null;
375             finishRemoval.run();
376         } else {
377             animator.alpha(0f, finishRemoval /* endAction */)
378                     .withStiffness(SpringForce.STIFFNESS_HIGH)
379                     .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
380                     .scaleX(1.1f)
381                     .scaleY(1.1f)
382                     .start();
383         }
384 
385         // Animate all the other bubbles to their new positions sans this bubble.
386         updateBubblePositions();
387     }
388 
389     @Override
onChildReordered(View child, int oldIndex, int newIndex)390     void onChildReordered(View child, int oldIndex, int newIndex) {
391         updateBubblePositions();
392     }
393 
updateBubblePositions()394     private void updateBubblePositions() {
395         if (mAnimatingExpand || mAnimatingCollapse) {
396             return;
397         }
398 
399         for (int i = 0; i < mLayout.getChildCount(); i++) {
400             final View bubble = mLayout.getChildAt(i);
401 
402             // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
403             // will be snapped to the correct X value after the drag (if it's not dismissed).
404             if (bubble.equals(mBubbleDraggingOut)) {
405                 return;
406             }
407 
408             animationForChild(bubble)
409                     .translationX(getBubbleLeft(i))
410                     .start();
411         }
412     }
413 
414     /** Returns the appropriate X translation value for a bubble at the given index. */
getXForChildAtIndex(int index)415     private float getXForChildAtIndex(int index) {
416         return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index;
417     }
418 
419     /**
420      * @param index Bubble index in row.
421      * @return Bubble left x from left edge of screen.
422      */
getBubbleLeft(int index)423     public float getBubbleLeft(int index) {
424         float bubbleLeftFromRowLeft = index * (mBubbleSizePx + mBubblePaddingPx);
425         return getRowLeft() + bubbleLeftFromRowLeft;
426     }
427 
getRowLeft()428     private float getRowLeft() {
429         if (mLayout == null) {
430             return 0;
431         }
432         int bubbleCount = mLayout.getChildCount();
433 
434         // Width calculations.
435         double bubble = bubbleCount * mBubbleSizePx;
436         float gap = (bubbleCount - 1) * mBubblePaddingPx;
437         float row = gap + (float) bubble;
438 
439         float halfRow = row / 2f;
440         float centerScreen = mDisplaySize.x / 2;
441         float rowLeftFromScreenLeft = centerScreen - halfRow;
442 
443         return rowLeftFromScreenLeft;
444     }
445 }
446