blob: f7896b0b120183a74737cff3a98758e06b493235 [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 Tsuji1575e6b2019-01-30 13:43:28 -050052 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -080053 * The stack position from which the bubbles were expanded. Saved in {@link #expandFromStack}
54 * and used to return to stack form in {@link #collapseBackToStack}.
55 */
56 private PointF mExpandedFrom;
57
58 /** Horizontal offset between bubbles, which we need to know to re-stack them. */
59 private float mStackOffsetPx;
60 /** Spacing between bubbles in the expanded state. */
61 private float mBubblePaddingPx;
62 /** Size of each bubble. */
63 private float mBubbleSizePx;
Joshua Tsujif44347f2019-02-12 14:28:06 -050064 /** Height of the status bar. */
65 private float mStatusBarHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080066 /** Size of display. */
67 private Point mDisplaySize;
68 /** Size of dismiss target at bottom of screen. */
69 private float mPipDismissHeight;
70
71 public ExpandedAnimationController(Point displaySize) {
72 mDisplaySize = displaySize;
73 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -080074
Joshua Tsuji442b6272019-02-08 13:23:43 -050075 /**
76 * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
77 * the rest of the bubbles to animate to fill the gap.
78 */
79 private boolean mBubbleDraggedOutEnough = false;
80
81 /** The bubble currently being dragged out of the row (to potentially be dismissed). */
82 private View mBubbleDraggingOut;
83
84 /**
85 * Drag velocities for the dragging-out bubble when the drag finished. These are used by
86 * {@link #onChildRemoved} to animate out the bubble while respecting touch velocity.
87 */
88 private float mBubbleDraggingOutVelX;
89 private float mBubbleDraggingOutVelY;
90
Joshua Tsujib1a796b2019-01-16 15:43:12 -080091 @Override
92 protected void setLayout(PhysicsAnimationLayout layout) {
93 super.setLayout(layout);
Joshua Tsujif44347f2019-02-12 14:28:06 -050094
95 final Resources res = layout.getResources();
96 mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
97 mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding);
98 mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
99 mStatusBarHeight =
100 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800101 mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800102 }
103
104 /**
105 * Animates expanding the bubbles into a row along the top of the screen.
106 *
107 * @return The y-value to which the bubbles were expanded, in case that's useful.
108 */
109 public float expandFromStack(PointF expandedFrom, Runnable after) {
110 mExpandedFrom = expandedFrom;
111
112 // How much to translate the next bubble, so that it is not overlapping the previous one.
113 float translateNextBubbleXBy = mBubblePaddingPx;
114 for (int i = 0; i < mLayout.getChildCount(); i++) {
115 mLayout.animatePositionForChildAtIndex(i, translateNextBubbleXBy, getExpandedY());
116 translateNextBubbleXBy += mBubbleSizePx + mBubblePaddingPx;
117 }
118
119 runAfterTranslationsEnd(after);
120 return getExpandedY();
121 }
122
123 /** Animate collapsing the bubbles back to their stacked position. */
124 public void collapseBackToStack(Runnable after) {
125 // Stack to the left if we're going to the left, or right if not.
126 final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mExpandedFrom.x) ? -1 : 1;
127 for (int i = 0; i < mLayout.getChildCount(); i++) {
128 mLayout.animatePositionForChildAtIndex(
129 i, mExpandedFrom.x + (sideMultiplier * i * mStackOffsetPx), mExpandedFrom.y);
130 }
131
132 runAfterTranslationsEnd(after);
133 }
134
Joshua Tsuji442b6272019-02-08 13:23:43 -0500135 /** Prepares the given bubble to be dragged out. */
136 public void prepareForBubbleDrag(View bubble) {
137 mLayout.cancelAnimationsOnView(bubble);
138
139 mBubbleDraggingOut = bubble;
140 mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE);
141 }
142
143 /**
144 * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
145 * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
146 * bubble is dragged back into the row.
147 */
148 public void dragBubbleOut(View bubbleView, float x, float y) {
149 bubbleView.setTranslationX(x);
150 bubbleView.setTranslationY(y);
151
152 final boolean draggedOutEnough =
153 y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
154 if (draggedOutEnough != mBubbleDraggedOutEnough) {
155 animateStackByBubbleWidthsStartingFrom(
156 /* numBubbleWidths */ draggedOutEnough ? -1 : 0,
157 /* startIndex */ mLayout.indexOfChild(bubbleView) + 1);
158 mBubbleDraggedOutEnough = draggedOutEnough;
159 }
160 }
161
162 /**
163 * Snaps a bubble back to its position within the bubble row, and animates the rest of the
164 * bubbles to accommodate it if it was previously dragged out past the threshold.
165 */
166 public void snapBubbleBack(View bubbleView, float velX, float velY) {
167 final int index = mLayout.indexOfChild(bubbleView);
168
169 // Snap the bubble back, respecting its current velocity.
170 mLayout.animateValueForChildAtIndex(
171 DynamicAnimation.TRANSLATION_X, index, getXForChildAtIndex(index), velX);
172 mLayout.animateValueForChildAtIndex(
173 DynamicAnimation.TRANSLATION_Y, index, getExpandedY(), velY);
174 mLayout.setEndListenerForProperties(
175 mLayout.new OneTimeMultiplePropertyEndListener() {
176 @Override
177 void onAllAnimationsForPropertiesEnd() {
178 // Reset Z translation once the bubble is done snapping back.
179 bubbleView.setTranslationZ(0f);
180 }
181 },
182 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
183
184 animateStackByBubbleWidthsStartingFrom(
185 /* numBubbleWidths */ 0, /* startIndex */ index + 1);
186
187 mBubbleDraggingOut = null;
188 mBubbleDraggedOutEnough = false;
189 }
190
191 /**
192 * Sets configuration variables so that when the given bubble is removed, the animations are
193 * started with the given velocities.
194 */
195 public void prepareForDismissalWithVelocity(View bubbleView, float velX, float velY) {
196 mBubbleDraggingOut = bubbleView;
197 mBubbleDraggingOutVelX = velX;
198 mBubbleDraggingOutVelY = velY;
199 mBubbleDraggedOutEnough = false;
200 }
201
202 /**
203 * Animates the bubbles, starting at the given index, to the left or right by the given number
204 * of bubble widths. Passing zero for numBubbleWidths will animate the bubbles to their normal
205 * positions.
206 */
207 private void animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex) {
208 for (int i = startIndex; i < mLayout.getChildCount(); i++) {
209 mLayout.animateValueForChildAtIndex(
210 DynamicAnimation.TRANSLATION_X,
211 i,
212 getXForChildAtIndex(i + numBubbleWidths));
213 }
214 }
215
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800216 /** The Y value of the row of expanded bubbles. */
Mady Mellorfe7ec032019-01-30 17:32:49 -0800217 public float getExpandedY() {
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800218 boolean showOnTop = mLayout != null
219 && BubbleController.showBubblesAtTop(mLayout.getContext());
Mady Mellorfe7ec032019-01-30 17:32:49 -0800220 final WindowInsets insets = mLayout != null ? mLayout.getRootWindowInsets() : null;
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800221 if (showOnTop && insets != null) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800222 return mBubblePaddingPx + Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -0500223 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500224 insets.getDisplayCutout() != null
225 ? insets.getDisplayCutout().getSafeInsetTop()
226 : 0);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800227 } else {
228 int bottomInset = insets != null ? insets.getSystemWindowInsetBottom() : 0;
229 return mDisplaySize.y - mBubbleSizePx - (mPipDismissHeight - bottomInset);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800230 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800231 }
232
233 /** Runs the given Runnable after all translation-related animations have ended. */
234 private void runAfterTranslationsEnd(Runnable after) {
235 DynamicAnimation.OnAnimationEndListener allEndedListener =
236 (animation, canceled, value, velocity) -> {
237 if (!mLayout.arePropertiesAnimating(
238 DynamicAnimation.TRANSLATION_X,
239 DynamicAnimation.TRANSLATION_Y)) {
240 after.run();
241 }
242 };
243
244 mLayout.setEndListenerForProperty(allEndedListener, DynamicAnimation.TRANSLATION_X);
245 mLayout.setEndListenerForProperty(allEndedListener, DynamicAnimation.TRANSLATION_Y);
246 }
247
248 @Override
249 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
250 return Sets.newHashSet(
251 DynamicAnimation.TRANSLATION_X,
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500252 DynamicAnimation.TRANSLATION_Y,
253 DynamicAnimation.SCALE_X,
254 DynamicAnimation.SCALE_Y,
255 DynamicAnimation.ALPHA);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800256 }
257
258 @Override
259 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
260 return NONE;
261 }
262
263 @Override
264 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
265 return 0;
266 }
267
268 @Override
269 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
270 return new SpringForce()
271 .setStiffness(SpringForce.STIFFNESS_LOW)
272 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
273 }
274
275 @Override
276 void onChildAdded(View child, int index) {
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500277 // Pop in from the top.
278 // TODO: Reverse this when bubbles are at the bottom.
279 child.setTranslationX(getXForChildAtIndex(index));
280 child.setTranslationY(getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
281 mLayout.animateValueForChild(DynamicAnimation.TRANSLATION_Y, child, getExpandedY());
Joshua Tsuji442b6272019-02-08 13:23:43 -0500282 animateBubblesAfterIndexToCorrectX(index);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800283 }
284
285 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500286 void onChildRemoved(View child, int index, Runnable finishRemoval) {
287 // Bubble pops out to the top.
288 // TODO: Reverse this when bubbles are at the bottom.
289 mLayout.animateValueForChild(
290 DynamicAnimation.ALPHA, child, 0f, finishRemoval);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500291
Joshua Tsuji442b6272019-02-08 13:23:43 -0500292 // If we're removing the dragged-out bubble, that means it got dismissed.
293 if (child.equals(mBubbleDraggingOut)) {
294 // Throw it to the bottom of the screen, towards the center horizontally.
295 mLayout.animateValueForChild(
296 DynamicAnimation.TRANSLATION_X,
297 child,
298 mLayout.getWidth() / 2f - mBubbleSizePx / 2f,
299 mBubbleDraggingOutVelX);
300 mLayout.animateValueForChild(
301 DynamicAnimation.TRANSLATION_Y,
302 child,
303 mLayout.getHeight() + mBubbleSizePx,
304 mBubbleDraggingOutVelY);
305
306 // Scale it down a bit so it looks like it's disappearing.
307 mLayout.animateValueForChild(DynamicAnimation.SCALE_X, child, ANIMATE_SCALE_PERCENT);
308 mLayout.animateValueForChild(DynamicAnimation.SCALE_Y, child, ANIMATE_SCALE_PERCENT);
309
310 mBubbleDraggingOut = null;
311 } else {
312 // If we're removing some random bubble just throw it off the top.
313 mLayout.animateValueForChild(
314 DynamicAnimation.TRANSLATION_Y,
315 child,
316 getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500317 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500318
319 // Animate all the other bubbles to their new positions sans this bubble.
320 animateBubblesAfterIndexToCorrectX(index);
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500321 }
322
323 @Override
324 protected void setChildVisibility(View child, int index, int visibility) {
325 if (visibility == View.VISIBLE) {
326 // Set alpha to 0 but then become visible immediately so the animation is visible.
327 child.setAlpha(0f);
328 child.setVisibility(View.VISIBLE);
329 }
330
331 // Fade in.
332 mLayout.animateValueForChild(
333 DynamicAnimation.ALPHA,
334 child,
335 /* value */ visibility == View.GONE ? 0f : 1f,
336 () -> super.setChildVisibility(child, index, visibility));
337 }
338
Joshua Tsuji442b6272019-02-08 13:23:43 -0500339 /**
340 * Animates the bubbles after the given index to the X position they should be in according to
341 * {@link #getXForChildAtIndex}.
342 */
343 private void animateBubblesAfterIndexToCorrectX(int start) {
344 for (int i = start; i < mLayout.getChildCount(); i++) {
345 final View bubble = mLayout.getChildAt(i);
346
347 // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
348 // will be snapped to the correct X value after the drag (if it's not dismissed).
349 if (!bubble.equals(mBubbleDraggingOut)) {
350 mLayout.animateValueForChild(
351 DynamicAnimation.TRANSLATION_X, bubble, getXForChildAtIndex(i));
352 }
353 }
354 }
355
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500356 /** Returns the appropriate X translation value for a bubble at the given index. */
357 private float getXForChildAtIndex(int index) {
358 return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800359 }
360}