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.Configuration;
20 import android.content.res.Resources;
21 import android.graphics.Path;
22 import android.graphics.Point;
23 import android.graphics.PointF;
24 import android.view.DisplayCutout;
25 import android.view.View;
26 import android.view.WindowInsets;
27 
28 import androidx.annotation.Nullable;
29 import androidx.dynamicanimation.animation.DynamicAnimation;
30 import androidx.dynamicanimation.animation.SpringForce;
31 
32 import com.android.systemui.Interpolators;
33 import com.android.systemui.R;
34 
35 import com.google.android.collect.Sets;
36 
37 import java.io.FileDescriptor;
38 import java.io.PrintWriter;
39 import java.util.Set;
40 
41 /**
42  * Animation controller for bubbles when they're in their expanded state, or animating to/from the
43  * expanded state. This controls the expansion animation as well as bubbles 'dragging out' to be
44  * dismissed.
45  */
46 public class ExpandedAnimationController
47         extends PhysicsAnimationLayout.PhysicsAnimationController {
48 
49     /**
50      * How much to translate the bubbles when they're animating in/out. This value is multiplied by
51      * the bubble size.
52      */
53     private static final int ANIMATE_TRANSLATION_FACTOR = 4;
54 
55     /** Duration of the expand/collapse target path animation. */
56     private static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175;
57 
58     /** Stiffness for the expand/collapse path-following animation. */
59     private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;
60 
61     /** What percentage of the screen to use when centering the bubbles in landscape. */
62     private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
63 
64     /** Horizontal offset between bubbles, which we need to know to re-stack them. */
65     private float mStackOffsetPx;
66     /** Space between status bar and bubbles in the expanded state. */
67     private float mBubblePaddingTop;
68     /** Size of each bubble. */
69     private float mBubbleSizePx;
70     /** Height of the status bar. */
71     private float mStatusBarHeight;
72     /** Size of display. */
73     private Point mDisplaySize;
74     /** Max number of bubbles shown in row above expanded view.*/
75     private int mBubblesMaxRendered;
76     /** What the current screen orientation is. */
77     private int mScreenOrientation;
78 
79     /** Whether the dragged-out bubble is in the dismiss target. */
80     private boolean mIndividualBubbleWithinDismissTarget = false;
81 
82     private boolean mAnimatingExpand = false;
83     private boolean mAnimatingCollapse = false;
84     private Runnable mAfterExpand;
85     private Runnable mAfterCollapse;
86     private PointF mCollapsePoint;
87 
88     /**
89      * Whether the dragged out bubble is springing towards the touch point, rather than using the
90      * default behavior of moving directly to the touch point.
91      *
92      * This happens when the user's finger exits the dismiss area while the bubble is magnetized to
93      * the center. Since the touch point differs from the bubble location, we need to animate the
94      * bubble back to the touch point to avoid a jarring instant location change from the center of
95      * the target to the touch point just outside the target bounds.
96      */
97     private boolean mSpringingBubbleToTouch = false;
98 
99     private int mExpandedViewPadding;
100     private float mLauncherGridDiff;
101 
ExpandedAnimationController(Point displaySize, int expandedViewPadding, int orientation)102     public ExpandedAnimationController(Point displaySize, int expandedViewPadding,
103             int orientation) {
104         updateOrientation(orientation, displaySize);
105         mExpandedViewPadding = expandedViewPadding;
106         mLauncherGridDiff = 30f;
107     }
108 
109     /**
110      * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
111      * the rest of the bubbles to animate to fill the gap.
112      */
113     private boolean mBubbleDraggedOutEnough = false;
114 
115     /** The bubble currently being dragged out of the row (to potentially be dismissed). */
116     private View mBubbleDraggingOut;
117 
118     /**
119      * Animates expanding the bubbles into a row along the top of the screen.
120      */
expandFromStack(Runnable after)121     public void expandFromStack(Runnable after) {
122         mAnimatingCollapse = false;
123         mAnimatingExpand = true;
124         mAfterExpand = after;
125 
126         startOrUpdatePathAnimation(true /* expanding */);
127     }
128 
129     /** Animate collapsing the bubbles back to their stacked position. */
collapseBackToStack(PointF collapsePoint, Runnable after)130     public void collapseBackToStack(PointF collapsePoint, Runnable after) {
131         mAnimatingExpand = false;
132         mAnimatingCollapse = true;
133         mAfterCollapse = after;
134         mCollapsePoint = collapsePoint;
135 
136         startOrUpdatePathAnimation(false /* expanding */);
137     }
138 
139     /**
140      * Update effective screen width based on current orientation.
141      * @param orientation Landscape or portrait.
142      * @param displaySize Updated display size.
143      */
updateOrientation(int orientation, Point displaySize)144     public void updateOrientation(int orientation, Point displaySize) {
145         mScreenOrientation = orientation;
146         mDisplaySize = displaySize;
147         if (mLayout != null) {
148             Resources res = mLayout.getContext().getResources();
149             mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
150             mStatusBarHeight = res.getDimensionPixelSize(
151                     com.android.internal.R.dimen.status_bar_height);
152         }
153     }
154 
155     /**
156      * Animates the bubbles along a curved path, either to expand them along the top or collapse
157      * them back into a stack.
158      */
startOrUpdatePathAnimation(boolean expanding)159     private void startOrUpdatePathAnimation(boolean expanding) {
160         Runnable after;
161 
162         if (expanding) {
163             after = () -> {
164                 mAnimatingExpand = false;
165 
166                 if (mAfterExpand != null) {
167                     mAfterExpand.run();
168                 }
169 
170                 mAfterExpand = null;
171             };
172         } else {
173             after = () -> {
174                 mAnimatingCollapse = false;
175 
176                 if (mAfterCollapse != null) {
177                     mAfterCollapse.run();
178                 }
179 
180                 mAfterCollapse = null;
181             };
182         }
183 
184         // Animate each bubble individually, since each path will end in a different spot.
185         animationsForChildrenFromIndex(0, (index, animation) -> {
186             final View bubble = mLayout.getChildAt(index);
187 
188             // Start a path at the bubble's current position.
189             final Path path = new Path();
190             path.moveTo(bubble.getTranslationX(), bubble.getTranslationY());
191 
192             final float expandedY = getExpandedY();
193             if (expanding) {
194                 // If we're expanding, first draw a line from the bubble's current position to the
195                 // top of the screen.
196                 path.lineTo(bubble.getTranslationX(), expandedY);
197 
198                 // Then, draw a line across the screen to the bubble's resting position.
199                 path.lineTo(getBubbleLeft(index), expandedY);
200             } else {
201                 final float sideMultiplier =
202                         mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
203                 final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx);
204 
205                 // If we're collapsing, draw a line from the bubble's current position to the side
206                 // of the screen where the bubble will be stacked.
207                 path.lineTo(stackedX, expandedY);
208 
209                 // Then, draw a line down to the stack position.
210                 path.lineTo(stackedX, mCollapsePoint.y);
211             }
212 
213             // The lead bubble should be the bubble with the longest distance to travel when we're
214             // expanding, and the bubble with the shortest distance to travel when we're collapsing.
215             // During expansion from the left side, the last bubble has to travel to the far right
216             // side, so we have it lead and 'pull' the rest of the bubbles into place. From the
217             // right side, the first bubble is traveling to the top left, so it leads. During
218             // collapse to the left, the first bubble has the shortest travel time back to the stack
219             // position, so it leads (and vice versa).
220             final boolean firstBubbleLeads =
221                     (expanding && !mLayout.isFirstChildXLeftOfCenter(bubble.getTranslationX()))
222                             || (!expanding && mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x));
223             final int startDelay = firstBubbleLeads
224                     ? (index * 10)
225                     : ((mLayout.getChildCount() - index) * 10);
226 
227             animation
228                     .followAnimatedTargetAlongPath(
229                             path,
230                             EXPAND_COLLAPSE_TARGET_ANIM_DURATION /* targetAnimDuration */,
231                             Interpolators.LINEAR /* targetAnimInterpolator */)
232                     .withStartDelay(startDelay)
233                     .withStiffness(EXPAND_COLLAPSE_ANIM_STIFFNESS);
234         }).startAll(after);
235     }
236 
237     /** Prepares the given bubble to be dragged out. */
prepareForBubbleDrag(View bubble)238     public void prepareForBubbleDrag(View bubble) {
239         mLayout.cancelAnimationsOnView(bubble);
240 
241         mBubbleDraggingOut = bubble;
242         mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE);
243     }
244 
245     /**
246      * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
247      * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
248      * bubble is dragged back into the row.
249      */
dragBubbleOut(View bubbleView, float x, float y)250     public void dragBubbleOut(View bubbleView, float x, float y) {
251         if (mSpringingBubbleToTouch) {
252             if (mLayout.arePropertiesAnimatingOnView(
253                     bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
254                 animationForChild(mBubbleDraggingOut)
255                         .translationX(x)
256                         .translationY(y)
257                         .withStiffness(SpringForce.STIFFNESS_HIGH)
258                         .start();
259             } else {
260                 mSpringingBubbleToTouch = false;
261             }
262         }
263 
264         if (!mSpringingBubbleToTouch && !mIndividualBubbleWithinDismissTarget) {
265             bubbleView.setTranslationX(x);
266             bubbleView.setTranslationY(y);
267         }
268 
269         final boolean draggedOutEnough =
270                 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
271         if (draggedOutEnough != mBubbleDraggedOutEnough) {
272             updateBubblePositions();
273             mBubbleDraggedOutEnough = draggedOutEnough;
274         }
275     }
276 
277     /** Plays a dismiss animation on the dragged out bubble. */
278     public void dismissDraggedOutBubble(View bubble, Runnable after) {
279         mIndividualBubbleWithinDismissTarget = false;
280 
281         animationForChild(bubble)
282                 .withStiffness(SpringForce.STIFFNESS_HIGH)
283                 .scaleX(1.1f)
284                 .scaleY(1.1f)
285                 .alpha(0f, after)
286                 .start();
287 
288         updateBubblePositions();
289     }
290 
291     @Nullable public View getDraggedOutBubble() {
292         return mBubbleDraggingOut;
293     }
294 
295     /** Magnets the given bubble to the dismiss target. */
296     public void magnetBubbleToDismiss(
297             View bubbleView, float velX, float velY, float destY, Runnable after) {
298         mIndividualBubbleWithinDismissTarget = true;
299         mSpringingBubbleToTouch = false;
300         animationForChild(bubbleView)
301                 .withStiffness(SpringForce.STIFFNESS_MEDIUM)
302                 .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
303                 .withPositionStartVelocities(velX, velY)
304                 .translationX(mLayout.getWidth() / 2f - mBubbleSizePx / 2f)
305                 .translationY(destY, after)
306                 .start();
307     }
308 
309     /**
310      * Springs the dragged-out bubble towards the given coordinates and sets flags to have touch
311      * events update the spring's final position until it's settled.
312      */
313     public void demagnetizeBubbleTo(float x, float y, float velX, float velY) {
314         mIndividualBubbleWithinDismissTarget = false;
315         mSpringingBubbleToTouch = true;
316 
317         animationForChild(mBubbleDraggingOut)
318                 .translationX(x)
319                 .translationY(y)
320                 .withPositionStartVelocities(velX, velY)
321                 .withStiffness(SpringForce.STIFFNESS_HIGH)
322                 .start();
323     }
324 
325     /**
326      * Snaps a bubble back to its position within the bubble row, and animates the rest of the
327      * bubbles to accommodate it if it was previously dragged out past the threshold.
328      */
329     public void snapBubbleBack(View bubbleView, float velX, float velY) {
330         final int index = mLayout.indexOfChild(bubbleView);
331 
332         animationForChildAtIndex(index)
333                 .position(getBubbleLeft(index), getExpandedY())
334                 .withPositionStartVelocities(velX, velY)
335                 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
336 
337         updateBubblePositions();
338     }
339 
340     /** Resets bubble drag out gesture flags. */
onGestureFinished()341     public void onGestureFinished() {
342         mBubbleDraggedOutEnough = false;
343         mBubbleDraggingOut = null;
344         updateBubblePositions();
345     }
346 
347     /**
348      * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing.
349      */
updateYPosition(Runnable after)350     public void updateYPosition(Runnable after) {
351         if (mLayout == null) return;
352         animationsForChildrenFromIndex(
353                 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after);
354     }
355 
356     /** The Y value of the row of expanded bubbles. */
getExpandedY()357     public float getExpandedY() {
358         if (mLayout == null || mLayout.getRootWindowInsets() == null) {
359             return 0;
360         }
361         final WindowInsets insets = mLayout.getRootWindowInsets();
362         return mBubblePaddingTop + Math.max(
363             mStatusBarHeight,
364             insets.getDisplayCutout() != null
365                 ? insets.getDisplayCutout().getSafeInsetTop()
366                 : 0);
367     }
368 
369     /** Description of current animation controller state. */
dump(FileDescriptor fd, PrintWriter pw, String[] args)370     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
371         pw.println("ExpandedAnimationController state:");
372         pw.print("  isActive:          "); pw.println(isActiveController());
373         pw.print("  animatingExpand:   "); pw.println(mAnimatingExpand);
374         pw.print("  animatingCollapse: "); pw.println(mAnimatingCollapse);
375         pw.print("  bubbleInDismiss:   "); pw.println(mIndividualBubbleWithinDismissTarget);
376         pw.print("  springingBubble:   "); pw.println(mSpringingBubbleToTouch);
377     }
378 
379     @Override
onActiveControllerForLayout(PhysicsAnimationLayout layout)380     void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
381         final Resources res = layout.getResources();
382         mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
383         mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
384         mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
385         mStatusBarHeight =
386                 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
387         mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
388 
389         // Ensure that all child views are at 1x scale, and visible, in case they were animating
390         // in.
391         mLayout.setVisibility(View.VISIBLE);
392         animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) ->
393                 animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll();
394     }
395 
396     @Override
getAnimatedProperties()397     Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
398         return Sets.newHashSet(
399                 DynamicAnimation.TRANSLATION_X,
400                 DynamicAnimation.TRANSLATION_Y,
401                 DynamicAnimation.SCALE_X,
402                 DynamicAnimation.SCALE_Y,
403                 DynamicAnimation.ALPHA);
404     }
405 
406     @Override
getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)407     int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
408         return NONE;
409     }
410 
411     @Override
getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property)412     float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
413         return 0;
414     }
415 
416     @Override
getSpringForce(DynamicAnimation.ViewProperty property, View view)417     SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
418         return new SpringForce()
419                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
420                 .setStiffness(SpringForce.STIFFNESS_LOW);
421     }
422 
423     @Override
onChildAdded(View child, int index)424     void onChildAdded(View child, int index) {
425         // If a bubble is added while the expand/collapse animations are playing, update the
426         // animation to include the new bubble.
427         if (mAnimatingExpand) {
428             startOrUpdatePathAnimation(true /* expanding */);
429         } else if (mAnimatingCollapse) {
430             startOrUpdatePathAnimation(false /* expanding */);
431         } else {
432             child.setTranslationX(getBubbleLeft(index));
433             animationForChild(child)
434                     .translationY(
435                             getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */
436                             getExpandedY() /* to */)
437                     .start();
438             updateBubblePositions();
439         }
440     }
441 
442     @Override
onChildRemoved(View child, int index, Runnable finishRemoval)443     void onChildRemoved(View child, int index, Runnable finishRemoval) {
444         final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
445 
446         // If we're removing the dragged-out bubble, that means it got dismissed.
447         if (child.equals(mBubbleDraggingOut)) {
448             mBubbleDraggingOut = null;
449             finishRemoval.run();
450         } else {
451             animator.alpha(0f, finishRemoval /* endAction */)
452                     .withStiffness(SpringForce.STIFFNESS_HIGH)
453                     .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
454                     .scaleX(1.1f)
455                     .scaleY(1.1f)
456                     .start();
457         }
458 
459         // Animate all the other bubbles to their new positions sans this bubble.
460         updateBubblePositions();
461     }
462 
463     @Override
onChildReordered(View child, int oldIndex, int newIndex)464     void onChildReordered(View child, int oldIndex, int newIndex) {
465         updateBubblePositions();
466 
467         // We expect reordering during collapse, since we'll put the last selected bubble on top.
468         // Update the collapse animation so they end up in the right stacked positions.
469         if (mAnimatingCollapse) {
470             startOrUpdatePathAnimation(false /* expanding */);
471         }
472     }
473 
updateBubblePositions()474     private void updateBubblePositions() {
475         if (mAnimatingExpand || mAnimatingCollapse) {
476             return;
477         }
478 
479         for (int i = 0; i < mLayout.getChildCount(); i++) {
480             final View bubble = mLayout.getChildAt(i);
481 
482             // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
483             // will be snapped to the correct X value after the drag (if it's not dismissed).
484             if (bubble.equals(mBubbleDraggingOut)) {
485                 return;
486             }
487 
488             animationForChild(bubble)
489                     .translationX(getBubbleLeft(i))
490                     .start();
491         }
492     }
493 
494     /**
495      * @param index Bubble index in row.
496      * @return Bubble left x from left edge of screen.
497      */
getBubbleLeft(int index)498     public float getBubbleLeft(int index) {
499         final float bubbleFromRowLeft = index * (mBubbleSizePx + getSpaceBetweenBubbles());
500         return getRowLeft() + bubbleFromRowLeft;
501     }
502 
503     /**
504      * When expanded, the bubbles are centered in the screen. In portrait, all available space is
505      * used. In landscape we have too much space so the value is restricted. This method accounts
506      * for window decorations (nav bar, cutouts).
507      *
508      * @return the desired width to display the expanded bubbles in.
509      */
getWidthForDisplayingBubbles()510     private float getWidthForDisplayingBubbles() {
511         final float availableWidth = getAvailableScreenWidth(true /* includeStableInsets */);
512         if (mScreenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
513             // display size y in landscape will be the smaller dimension of the screen
514             return Math.max(mDisplaySize.y, availableWidth * CENTER_BUBBLES_LANDSCAPE_PERCENT);
515         } else {
516             return availableWidth;
517         }
518     }
519 
520     /**
521      * Determines the available screen width without the cutout.
522      *
523      * @param subtractStableInsets Whether or not stable insets should also be removed from the
524      *                            returned width.
525      * @return the total screen width available accounting for cutouts and insets,
526      * iff {@param includeStableInsets} is true.
527      */
getAvailableScreenWidth(boolean subtractStableInsets)528     private float getAvailableScreenWidth(boolean subtractStableInsets) {
529         float availableSize = mDisplaySize.x;
530         WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null;
531         if (insets != null) {
532             int cutoutLeft = 0;
533             int cutoutRight = 0;
534             DisplayCutout cutout = insets.getDisplayCutout();
535             if (cutout != null) {
536                 cutoutLeft = cutout.getSafeInsetLeft();
537                 cutoutRight = cutout.getSafeInsetRight();
538             }
539             final int stableLeft = subtractStableInsets ? insets.getStableInsetLeft() : 0;
540             final int stableRight = subtractStableInsets ? insets.getStableInsetRight() : 0;
541             availableSize -= Math.max(stableLeft, cutoutLeft);
542             availableSize -= Math.max(stableRight, cutoutRight);
543         }
544         return availableSize;
545     }
546 
getRowLeft()547     private float getRowLeft() {
548         if (mLayout == null) {
549             return 0;
550         }
551 
552         int bubbleCount = mLayout.getChildCount();
553 
554         final float totalBubbleWidth = bubbleCount * mBubbleSizePx;
555         final float totalGapWidth = (bubbleCount - 1) * getSpaceBetweenBubbles();
556         final float rowWidth = totalGapWidth + totalBubbleWidth;
557 
558         // This display size we're using includes the size of the insets, we want the true
559         // center of the display minus the notch here, which means we should include the
560         // stable insets (e.g. status bar, nav bar) in this calculation.
561         final float trueCenter = getAvailableScreenWidth(false /* subtractStableInsets */) / 2f;
562         final float halfRow = rowWidth / 2f;
563         final float rowLeft = trueCenter - halfRow;
564 
565         return rowLeft;
566     }
567 
568     /**
569      * @return Space between bubbles in row above expanded view.
570      */
getSpaceBetweenBubbles()571     private float getSpaceBetweenBubbles() {
572         /**
573          * Ordered left to right:
574          *  Screen edge
575          *      [mExpandedViewPadding]
576          *  Expanded view edge
577          *      [launcherGridDiff] --- arbitrary value until launcher exports widths
578          *  Launcher's app icon grid edge that we must match
579          */
580         final float rowMargins = (mExpandedViewPadding + mLauncherGridDiff) * 2;
581         final float maxRowWidth = getWidthForDisplayingBubbles() - rowMargins;
582 
583         final float totalBubbleWidth = mBubblesMaxRendered * mBubbleSizePx;
584         final float totalGapWidth = maxRowWidth - totalBubbleWidth;
585 
586         final int gapCount = mBubblesMaxRendered - 1;
587         final float gapWidth = totalGapWidth / gapCount;
588         return gapWidth;
589     }
590 }
591