blob: 1f29883b180aff3c6d1132c9c4bfe7e985386010 [file] [log] [blame]
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001/*
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
17package com.android.systemui.bubbles.animation;
18
Joshua Tsujif44347f2019-02-12 14:28:06 -050019import android.content.res.Resources;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080020import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080021import android.graphics.PointF;
22import android.view.View;
23import android.view.WindowInsets;
24
25import androidx.dynamicanimation.animation.DynamicAnimation;
26import androidx.dynamicanimation.animation.SpringForce;
27
28import com.android.systemui.R;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080029import com.android.systemui.bubbles.BubbleController;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080030
31import com.google.android.collect.Sets;
32
33import 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 */
40public class ExpandedAnimationController
41 extends PhysicsAnimationLayout.PhysicsAnimationController {
42
43 /**
Joshua Tsuji1575e6b2019-01-30 13:43:28 -050044 * 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
Joshua Tsuji442b6272019-02-08 13:23:43 -050049 /** How much to scale down bubbles when they're animating in/out. */
50 private static final float ANIMATE_SCALE_PERCENT = 0.5f;
51
Joshua Tsuji3829caa2019-03-05 18:09:13 -050052 /** The stack position to collapse back to in {@link #collapseBackToStack}. */
53 private PointF mCollapseToPoint;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080054
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;
Joshua Tsujif44347f2019-02-12 14:28:06 -050061 /** Height of the status bar. */
62 private float mStatusBarHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080063 /** Size of display. */
64 private Point mDisplaySize;
65 /** Size of dismiss target at bottom of screen. */
66 private float mPipDismissHeight;
67
68 public ExpandedAnimationController(Point displaySize) {
69 mDisplaySize = displaySize;
70 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -080071
Joshua Tsuji442b6272019-02-08 13:23:43 -050072 /**
73 * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
74 * the rest of the bubbles to animate to fill the gap.
75 */
76 private boolean mBubbleDraggedOutEnough = false;
77
78 /** The bubble currently being dragged out of the row (to potentially be dismissed). */
79 private View mBubbleDraggingOut;
80
81 /**
82 * Drag velocities for the dragging-out bubble when the drag finished. These are used by
83 * {@link #onChildRemoved} to animate out the bubble while respecting touch velocity.
84 */
85 private float mBubbleDraggingOutVelX;
86 private float mBubbleDraggingOutVelY;
87
Joshua Tsujib1a796b2019-01-16 15:43:12 -080088 @Override
89 protected void setLayout(PhysicsAnimationLayout layout) {
90 super.setLayout(layout);
Joshua Tsujif44347f2019-02-12 14:28:06 -050091
92 final Resources res = layout.getResources();
93 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
94 mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding);
95 mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
96 mStatusBarHeight =
97 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Mady Mellor44ee2fe2019-01-30 17:51:16 -080098 mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height);
Joshua Tsujib1a796b2019-01-16 15:43:12 -080099 }
100
101 /**
102 * Animates expanding the bubbles into a row along the top of the screen.
103 *
104 * @return The y-value to which the bubbles were expanded, in case that's useful.
105 */
Joshua Tsuji3829caa2019-03-05 18:09:13 -0500106 public float expandFromStack(PointF collapseTo, Runnable after) {
107 mCollapseToPoint = collapseTo;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800108
109 // How much to translate the next bubble, so that it is not overlapping the previous one.
110 float translateNextBubbleXBy = mBubblePaddingPx;
111 for (int i = 0; i < mLayout.getChildCount(); i++) {
112 mLayout.animatePositionForChildAtIndex(i, translateNextBubbleXBy, getExpandedY());
113 translateNextBubbleXBy += mBubbleSizePx + mBubblePaddingPx;
114 }
115
116 runAfterTranslationsEnd(after);
117 return getExpandedY();
118 }
119
120 /** Animate collapsing the bubbles back to their stacked position. */
121 public void collapseBackToStack(Runnable after) {
122 // Stack to the left if we're going to the left, or right if not.
Joshua Tsuji3829caa2019-03-05 18:09:13 -0500123 final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapseToPoint.x) ? -1 : 1;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800124 for (int i = 0; i < mLayout.getChildCount(); i++) {
125 mLayout.animatePositionForChildAtIndex(
Joshua Tsuji3829caa2019-03-05 18:09:13 -0500126 i,
127 mCollapseToPoint.x + (sideMultiplier * i * mStackOffsetPx), mCollapseToPoint.y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800128 }
129
130 runAfterTranslationsEnd(after);
131 }
132
Joshua Tsuji442b6272019-02-08 13:23:43 -0500133 /** Prepares the given bubble to be dragged out. */
134 public void prepareForBubbleDrag(View bubble) {
135 mLayout.cancelAnimationsOnView(bubble);
136
137 mBubbleDraggingOut = bubble;
138 mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE);
139 }
140
141 /**
142 * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
143 * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
144 * bubble is dragged back into the row.
145 */
146 public void dragBubbleOut(View bubbleView, float x, float y) {
147 bubbleView.setTranslationX(x);
148 bubbleView.setTranslationY(y);
149
150 final boolean draggedOutEnough =
151 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
152 if (draggedOutEnough != mBubbleDraggedOutEnough) {
153 animateStackByBubbleWidthsStartingFrom(
154 /* numBubbleWidths */ draggedOutEnough ? -1 : 0,
155 /* startIndex */ mLayout.indexOfChild(bubbleView) + 1);
156 mBubbleDraggedOutEnough = draggedOutEnough;
157 }
158 }
159
160 /**
161 * Snaps a bubble back to its position within the bubble row, and animates the rest of the
162 * bubbles to accommodate it if it was previously dragged out past the threshold.
163 */
164 public void snapBubbleBack(View bubbleView, float velX, float velY) {
165 final int index = mLayout.indexOfChild(bubbleView);
166
167 // Snap the bubble back, respecting its current velocity.
168 mLayout.animateValueForChildAtIndex(
169 DynamicAnimation.TRANSLATION_X, index, getXForChildAtIndex(index), velX);
170 mLayout.animateValueForChildAtIndex(
171 DynamicAnimation.TRANSLATION_Y, index, getExpandedY(), velY);
172 mLayout.setEndListenerForProperties(
173 mLayout.new OneTimeMultiplePropertyEndListener() {
174 @Override
175 void onAllAnimationsForPropertiesEnd() {
176 // Reset Z translation once the bubble is done snapping back.
177 bubbleView.setTranslationZ(0f);
178 }
179 },
180 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
181
182 animateStackByBubbleWidthsStartingFrom(
183 /* numBubbleWidths */ 0, /* startIndex */ index + 1);
184
185 mBubbleDraggingOut = null;
186 mBubbleDraggedOutEnough = false;
187 }
188
189 /**
190 * Sets configuration variables so that when the given bubble is removed, the animations are
191 * started with the given velocities.
192 */
193 public void prepareForDismissalWithVelocity(View bubbleView, float velX, float velY) {
194 mBubbleDraggingOut = bubbleView;
195 mBubbleDraggingOutVelX = velX;
196 mBubbleDraggingOutVelY = velY;
197 mBubbleDraggedOutEnough = false;
198 }
199
200 /**
201 * Animates the bubbles, starting at the given index, to the left or right by the given number
202 * of bubble widths. Passing zero for numBubbleWidths will animate the bubbles to their normal
203 * positions.
204 */
205 private void animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex) {
206 for (int i = startIndex; i < mLayout.getChildCount(); i++) {
207 mLayout.animateValueForChildAtIndex(
208 DynamicAnimation.TRANSLATION_X,
209 i,
210 getXForChildAtIndex(i + numBubbleWidths));
211 }
212 }
213
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800214 /** The Y value of the row of expanded bubbles. */
Mady Mellorfe7ec032019-01-30 17:32:49 -0800215 public float getExpandedY() {
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800216 boolean showOnTop = mLayout != null
217 && BubbleController.showBubblesAtTop(mLayout.getContext());
Mady Mellorfe7ec032019-01-30 17:32:49 -0800218 final WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null;
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800219 if (showOnTop && insets != null) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800220 return mBubblePaddingPx + Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -0500221 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500222 insets.getDisplayCutout() != null
223 ? insets.getDisplayCutout().getSafeInsetTop()
224 : 0);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800225 } else {
226 int bottomInset = insets != null ? insets.getSystemWindowInsetBottom() : 0;
227 return mDisplaySize.y - mBubbleSizePx - (mPipDismissHeight - bottomInset);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800228 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800229 }
230
231 /** Runs the given Runnable after all translation-related animations have ended. */
232 private void runAfterTranslationsEnd(Runnable after) {
233 DynamicAnimation.OnAnimationEndListener allEndedListener =
234 (animation, canceled, value, velocity) -> {
235 if (!mLayout.arePropertiesAnimating(
236 DynamicAnimation.TRANSLATION_X,
237 DynamicAnimation.TRANSLATION_Y)) {
238 after.run();
239 }
240 };
241
242 mLayout.setEndListenerForProperty(allEndedListener, DynamicAnimation.TRANSLATION_X);
243 mLayout.setEndListenerForProperty(allEndedListener, DynamicAnimation.TRANSLATION_Y);
244 }
245
246 @Override
247 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
248 return Sets.newHashSet(
249 DynamicAnimation.TRANSLATION_X,
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500250 DynamicAnimation.TRANSLATION_Y,
251 DynamicAnimation.SCALE_X,
252 DynamicAnimation.SCALE_Y,
253 DynamicAnimation.ALPHA);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800254 }
255
256 @Override
257 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
258 return NONE;
259 }
260
261 @Override
262 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
263 return 0;
264 }
265
266 @Override
267 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
268 return new SpringForce()
Joshua Tsuji010c2b12019-02-25 18:11:25 -0500269 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
270 .setStiffness(SpringForce.STIFFNESS_LOW);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800271 }
272
273 @Override
274 void onChildAdded(View child, int index) {
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500275 // Pop in from the top.
276 // TODO: Reverse this when bubbles are at the bottom.
277 child.setTranslationX(getXForChildAtIndex(index));
278 child.setTranslationY(getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
279 mLayout.animateValueForChild(DynamicAnimation.TRANSLATION_Y, child, getExpandedY());
Joshua Tsuji442b6272019-02-08 13:23:43 -0500280 animateBubblesAfterIndexToCorrectX(index);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800281 }
282
283 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500284 void onChildRemoved(View child, int index, Runnable finishRemoval) {
285 // Bubble pops out to the top.
286 // TODO: Reverse this when bubbles are at the bottom.
287 mLayout.animateValueForChild(
288 DynamicAnimation.ALPHA, child, 0f, finishRemoval);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500289
Joshua Tsuji442b6272019-02-08 13:23:43 -0500290 // If we're removing the dragged-out bubble, that means it got dismissed.
291 if (child.equals(mBubbleDraggingOut)) {
292 // Throw it to the bottom of the screen, towards the center horizontally.
293 mLayout.animateValueForChild(
294 DynamicAnimation.TRANSLATION_X,
295 child,
296 mLayout.getWidth() / 2f - mBubbleSizePx / 2f,
297 mBubbleDraggingOutVelX);
298 mLayout.animateValueForChild(
299 DynamicAnimation.TRANSLATION_Y,
300 child,
301 mLayout.getHeight() + mBubbleSizePx,
302 mBubbleDraggingOutVelY);
303
304 // Scale it down a bit so it looks like it's disappearing.
305 mLayout.animateValueForChild(DynamicAnimation.SCALE_X, child, ANIMATE_SCALE_PERCENT);
306 mLayout.animateValueForChild(DynamicAnimation.SCALE_Y, child, ANIMATE_SCALE_PERCENT);
307
308 mBubbleDraggingOut = null;
309 } else {
310 // If we're removing some random bubble just throw it off the top.
311 mLayout.animateValueForChild(
312 DynamicAnimation.TRANSLATION_Y,
313 child,
314 getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500315 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500316
317 // Animate all the other bubbles to their new positions sans this bubble.
318 animateBubblesAfterIndexToCorrectX(index);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500319 }
320
321 @Override
322 protected void setChildVisibility(View child, int index, int visibility) {
323 if (visibility == View.VISIBLE) {
324 // Set alpha to 0 but then become visible immediately so the animation is visible.
325 child.setAlpha(0f);
326 child.setVisibility(View.VISIBLE);
327 }
328
329 // Fade in.
330 mLayout.animateValueForChild(
331 DynamicAnimation.ALPHA,
332 child,
333 /* value */ visibility == View.GONE ? 0f : 1f,
334 () -> super.setChildVisibility(child, index, visibility));
335 }
336
Joshua Tsuji442b6272019-02-08 13:23:43 -0500337 /**
338 * Animates the bubbles after the given index to the X position they should be in according to
339 * {@link #getXForChildAtIndex}.
340 */
341 private void animateBubblesAfterIndexToCorrectX(int start) {
342 for (int i = start; i < mLayout.getChildCount(); i++) {
343 final View bubble = mLayout.getChildAt(i);
344
345 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
346 // will be snapped to the correct X value after the drag (if it's not dismissed).
347 if (!bubble.equals(mBubbleDraggingOut)) {
348 mLayout.animateValueForChild(
349 DynamicAnimation.TRANSLATION_X, bubble, getXForChildAtIndex(i));
350 }
351 }
352 }
353
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500354 /** Returns the appropriate X translation value for a bubble at the given index. */
355 private float getXForChildAtIndex(int index) {
356 return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800357 }
358}