blob: f937525cf4179a1c29a8b0e081274a8c127c271d [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 Tsujib1a796b2019-01-16 15:43:12 -080019import android.content.res.Resources;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080020import android.graphics.PointF;
21import android.graphics.RectF;
22import android.util.Log;
23import android.view.View;
24import android.view.WindowInsets;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080025
26import androidx.dynamicanimation.animation.DynamicAnimation;
27import androidx.dynamicanimation.animation.FlingAnimation;
28import androidx.dynamicanimation.animation.FloatPropertyCompat;
29import androidx.dynamicanimation.animation.SpringAnimation;
30import androidx.dynamicanimation.animation.SpringForce;
31
32import com.android.systemui.R;
33
34import com.google.android.collect.Sets;
35
36import java.util.HashMap;
37import java.util.Set;
38
39/**
40 * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop
41 * each other with a slight offset to the left or right (depending on which side of the screen they
42 * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of
43 * the screen.
44 */
45public class StackAnimationController extends
46 PhysicsAnimationLayout.PhysicsAnimationController {
47
48 private static final String TAG = "Bubbs.StackCtrl";
49
50 /** Scale factor to use initially for new bubbles being animated in. */
51 private static final float ANIMATE_IN_STARTING_SCALE = 1.15f;
52
Joshua Tsujia08b6d32019-01-29 16:15:52 -050053 /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */
54 private static final int ANIMATE_TRANSLATION_FACTOR = 4;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080055
56 /**
57 * Values to use for the default {@link SpringForce} provided to the physics animation layout.
58 */
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040059 private static final int DEFAULT_STIFFNESS = 12000;
60 private static final int FLING_FOLLOW_STIFFNESS = 20000;
61 private static final float DEFAULT_BOUNCINESS = 0.9f;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080062
63 /**
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050064 * Friction applied to fling animations. Since the stack must land on one of the sides of the
65 * screen, we want less friction horizontally so that the stack has a better chance of making it
66 * to the side without needing a spring.
67 */
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040068 private static final float FLING_FRICTION_X = 2.2f;
69 private static final float FLING_FRICTION_Y = 2.2f;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050070
71 /**
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040072 * Values to use for the stack spring animation used to spring the stack to its final position
73 * after a fling.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050074 */
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040075 private static final int SPRING_AFTER_FLING_STIFFNESS = 750;
76 private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050077
78 /**
79 * Minimum fling velocity required to trigger moving the stack from one side of the screen to
80 * the other.
81 */
82 private static final float ESCAPE_VELOCITY = 750f;
83
84 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -080085 * The canonical position of the stack. This is typically the position of the first bubble, but
86 * we need to keep track of it separately from the first bubble's translation in case there are
87 * no bubbles, or the first bubble was just added and being animated to its new position.
88 */
89 private PointF mStackPosition = new PointF();
90
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050091 /** The most recent position in which the stack was resting on the edge of the screen. */
92 private PointF mRestingStackPosition;
93
Joshua Tsujia19515f2019-02-13 18:02:29 -050094 /** The height of the most recently visible IME. */
95 private float mImeHeight = 0f;
96
97 /**
98 * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
99 * IME is not visible or the user moved the stack since the IME became visible.
100 */
101 private float mPreImeY = Float.MIN_VALUE;
102
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800103 /**
104 * Animations on the stack position itself, which would have been started in
105 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to
106 * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect)
107 * to a legal position on the side of the screen.
108 */
109 private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations =
110 new HashMap<>();
111
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400112 /**
113 * Whether the current motion of the stack is due to a fling animation (vs. being dragged
114 * manually).
115 */
116 private boolean mIsMovingFromFlinging = false;
117
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400118 /**
119 * Whether the stack is within the dismiss target (either by being dragged, magnet'd, or flung).
120 */
121 private boolean mWithinDismissTarget = false;
122
123 /**
124 * Whether the first bubble is springing towards the touch point, rather than using the default
125 * behavior of moving directly to the touch point with the rest of the stack following it.
126 *
127 * This happens when the user's finger exits the dismiss area while the stack is magnetized to
128 * the center. Since the touch point differs from the stack location, we need to animate the
129 * stack back to the touch point to avoid a jarring instant location change from the center of
130 * the target to the touch point just outside the target bounds.
131 *
132 * This is reset once the spring animations end, since that means the first bubble has
133 * successfully 'caught up' to the touch.
134 */
135 private boolean mFirstBubbleSpringingToTouch = false;
136
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800137 /** Horizontal offset of bubbles in the stack. */
138 private float mStackOffset;
139 /** Diameter of the bubbles themselves. */
140 private int mIndividualBubbleSize;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -0400141 /**
142 * The amount of space to add between the bubbles and certain UI elements, such as the top of
143 * the screen or the IME. This does not apply to the left/right sides of the screen since the
144 * stack goes offscreen intentionally.
145 */
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800146 private int mBubblePadding;
147 /** How far offscreen the stack rests. */
148 private int mBubbleOffscreen;
149 /** How far down the screen the stack starts, when there is no pre-existing location. */
150 private int mStackStartingVerticalOffset;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500151 /** Height of the status bar. */
152 private float mStatusBarHeight;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800153
154 @Override
155 protected void setLayout(PhysicsAnimationLayout layout) {
156 super.setLayout(layout);
157
158 Resources res = layout.getResources();
159 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
160 mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
161 mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
162 mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
163 mStackStartingVerticalOffset =
164 res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500165 mStatusBarHeight =
166 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800167 }
168
169 /**
170 * Instantly move the first bubble to the given point, and animate the rest of the stack behind
171 * it with the 'following' effect.
172 */
173 public void moveFirstBubbleWithStackFollowing(float x, float y) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500174 // If we manually move the bubbles with the IME open, clear the return point since we don't
175 // want the stack to snap away from the new position.
176 mPreImeY = Float.MIN_VALUE;
177
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800178 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x);
179 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y);
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400180
181 // This method is called when the stack is being dragged manually, so we're clearly no
182 // longer flinging.
183 mIsMovingFromFlinging = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800184 }
185
186 /**
187 * The position of the stack - typically the position of the first bubble; if no bubbles have
188 * been added yet, it will be where the first bubble will go when added.
189 */
190 public PointF getStackPosition() {
191 return mStackPosition;
192 }
193
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400194 /** Whether the stack is on the left side of the screen. */
195 public boolean isStackOnLeftSide() {
Lyn Hane68d0912019-05-02 18:28:01 -0700196 if (mLayout == null) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400197 return false;
198 }
Lyn Hane68d0912019-05-02 18:28:01 -0700199 float stackCenter = mStackPosition.x + mIndividualBubbleSize / 2;
200 float screenCenter = mLayout.getWidth() / 2;
201 return stackCenter < screenCenter;
202 }
203
204 /**
205 * Fling stack to given corner, within allowable screen bounds.
206 * Note that we need new SpringForce instances per animation despite identical configs because
207 * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
208 */
209 public void springStack(float destinationX, float destinationY) {
210 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
211 new SpringForce()
212 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
213 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
214 0 /* startXVelocity */,
215 destinationX);
216
217 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y,
218 new SpringForce()
219 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
220 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
221 0 /* startYVelocity */,
222 destinationY);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400223 }
224
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800225 /**
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500226 * Flings the stack starting with the given velocities, springing it to the nearest edge
227 * afterward.
228 */
229 public void flingStackThenSpringToEdge(float x, float velX, float velY) {
230 final boolean stackOnLeftSide = x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2;
231
232 final boolean stackShouldFlingLeft = stackOnLeftSide
233 ? velX < ESCAPE_VELOCITY
234 : velX < -ESCAPE_VELOCITY;
235
236 final RectF stackBounds = getAllowableStackPositionRegion();
237
238 // Target X translation (either the left or right side of the screen).
239 final float destinationRelativeX = stackShouldFlingLeft
240 ? stackBounds.left : stackBounds.right;
241
Joshua Tsujicd169332019-03-06 23:56:52 -0500242 // Minimum velocity required for the stack to make it to the targeted side of the screen,
243 // taking friction into account (4.2f is the number that friction scalars are multiplied by
244 // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
245 // but the SpringAnimation at the end will ensure that it reaches the destination X
246 // regardless.
247 final float minimumVelocityToReachEdge =
248 (destinationRelativeX - x) * (FLING_FRICTION_X * 4.2f);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500249
250 // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
251 // that it'll make it all the way to the side of the screen.
252 final float startXVelocity = stackShouldFlingLeft
Joshua Tsujicd169332019-03-06 23:56:52 -0500253 ? Math.min(minimumVelocityToReachEdge, velX)
254 : Math.max(minimumVelocityToReachEdge, velX);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500255
256 flingThenSpringFirstBubbleWithStackFollowing(
257 DynamicAnimation.TRANSLATION_X,
258 startXVelocity,
259 FLING_FRICTION_X,
260 new SpringForce()
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400261 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
262 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500263 destinationRelativeX);
264
265 flingThenSpringFirstBubbleWithStackFollowing(
266 DynamicAnimation.TRANSLATION_Y,
267 velY,
268 FLING_FRICTION_Y,
269 new SpringForce()
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400270 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
271 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500272 /* destination */ null);
273
Joshua Tsujic1108432019-02-22 16:10:12 -0500274 mLayout.setEndActionForMultipleProperties(
275 () -> {
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500276 mRestingStackPosition = new PointF();
277 mRestingStackPosition.set(mStackPosition);
Joshua Tsujic1108432019-02-22 16:10:12 -0500278 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
279 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500280 },
281 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400282
283 mIsMovingFromFlinging = true;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500284 }
285
286 /**
Joshua Tsuji3829caa2019-03-05 18:09:13 -0500287 * Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
288 */
289 public PointF getStackPositionAlongNearestHorizontalEdge() {
290 final PointF stackPos = getStackPosition();
291 final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x);
292 final RectF bounds = getAllowableStackPositionRegion();
293
294 stackPos.x = onLeft ? bounds.left : bounds.right;
295 return stackPos;
296 }
297
298 /**
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400299 * Moves the stack in response to rotation. We keep it in the most similar position by keeping
300 * it on the same side, and positioning it the same percentage of the way down the screen
301 * (taking status bar/nav bar into account by using the allowable region's height).
302 */
303 public void moveStackToSimilarPositionAfterRotation(boolean wasOnLeft, float verticalPercent) {
304 final RectF allowablePos = getAllowableStackPositionRegion();
305 final float allowableRegionHeight = allowablePos.bottom - allowablePos.top;
306
307 final float x = wasOnLeft ? allowablePos.left : allowablePos.right;
308 final float y = (allowableRegionHeight * verticalPercent) + allowablePos.top;
309
310 setStackPosition(new PointF(x, y));
311 }
312
313 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800314 * Flings the first bubble along the given property's axis, using the provided configuration
315 * values. When the animation ends - either by hitting the min/max, or by friction sufficiently
316 * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final
317 * position.
318 */
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500319 protected void flingThenSpringFirstBubbleWithStackFollowing(
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800320 DynamicAnimation.ViewProperty property,
321 float vel,
322 float friction,
323 SpringForce spring,
324 Float finalPosition) {
325 Log.d(TAG, String.format("Flinging %s.",
326 PhysicsAnimationLayout.getReadablePropertyName(property)));
327
328 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
329 final float currentValue = firstBubbleProperty.getValue(this);
330 final RectF bounds = getAllowableStackPositionRegion();
331 final float min =
332 property.equals(DynamicAnimation.TRANSLATION_X)
333 ? bounds.left
334 : bounds.top;
335 final float max =
336 property.equals(DynamicAnimation.TRANSLATION_X)
337 ? bounds.right
338 : bounds.bottom;
339
340 FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty);
341 flingAnimation.setFriction(friction)
342 .setStartVelocity(vel)
343
344 // If the bubble's property value starts beyond the desired min/max, use that value
345 // instead so that the animation won't immediately end. If, for example, the user
346 // drags the bubbles into the navigation bar, but then flings them upward, we want
347 // the fling to occur despite temporarily having a value outside of the min/max. If
348 // the bubbles are out of bounds and flung even farther out of bounds, the fling
349 // animation will halt immediately and the SpringAnimation will take over, springing
350 // it in reverse to the (legal) final position.
351 .setMinValue(Math.min(currentValue, min))
352 .setMaxValue(Math.max(currentValue, max))
353
354 .addEndListener((animation, canceled, endValue, endVelocity) -> {
355 if (!canceled) {
356 springFirstBubbleWithStackFollowing(property, spring, endVelocity,
357 finalPosition != null
358 ? finalPosition
359 : Math.max(min, Math.min(max, endValue)));
360 }
361 });
362
363 cancelStackPositionAnimation(property);
364 mStackPositionAnimations.put(property, flingAnimation);
365 flingAnimation.start();
366 }
367
368 /**
369 * Cancel any stack position animations that were started by calling
370 * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end
371 * listeners.
372 */
373 public void cancelStackPositionAnimations() {
374 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
375 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
376
Joshua Tsujic1108432019-02-22 16:10:12 -0500377 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
378 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800379 }
380
Joshua Tsuji4b395912019-04-19 17:18:40 -0400381 /** Save the current IME height so that we know where the stack bounds should be. */
382 public void setImeHeight(int imeHeight) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500383 mImeHeight = imeHeight;
Joshua Tsuji4b395912019-04-19 17:18:40 -0400384 }
Joshua Tsujia19515f2019-02-13 18:02:29 -0500385
Joshua Tsuji4b395912019-04-19 17:18:40 -0400386 /**
387 * Animates the stack either away from the newly visible IME, or back to its original position
388 * due to the IME going away.
389 */
390 public void animateForImeVisibility(boolean imeVisible) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500391 final float maxBubbleY = getAllowableStackPositionRegion().bottom;
Joshua Tsuji4b395912019-04-19 17:18:40 -0400392 float destinationY = Float.MIN_VALUE;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500393
Joshua Tsuji4b395912019-04-19 17:18:40 -0400394 if (imeVisible) {
Lyn Hane68d0912019-05-02 18:28:01 -0700395 // Stack is lower than it should be and overlaps the now-visible IME.
Joshua Tsuji4b395912019-04-19 17:18:40 -0400396 if (mStackPosition.y > maxBubbleY && mPreImeY == Float.MIN_VALUE) {
397 mPreImeY = mStackPosition.y;
398 destinationY = maxBubbleY;
399 }
400 } else {
401 if (mPreImeY > Float.MIN_VALUE) {
402 destinationY = mPreImeY;
403 mPreImeY = Float.MIN_VALUE;
404 }
405 }
406
407 if (destinationY > Float.MIN_VALUE) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500408 springFirstBubbleWithStackFollowing(
409 DynamicAnimation.TRANSLATION_Y,
410 getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
411 .setStiffness(SpringForce.STIFFNESS_LOW),
412 /* startVel */ 0f,
Joshua Tsuji4b395912019-04-19 17:18:40 -0400413 destinationY);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500414 }
415 }
416
417 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800418 * Returns the region within which the stack is allowed to rest. This goes slightly off the left
419 * and right sides of the screen, below the status bar/cutout and above the navigation bar.
420 * While the stack is not allowed to rest outside of these bounds, it can temporarily be
421 * animated or dragged beyond them.
422 */
423 public RectF getAllowableStackPositionRegion() {
424 final WindowInsets insets = mLayout.getRootWindowInsets();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500425 final RectF allowableRegion = new RectF();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800426 if (insets != null) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500427 allowableRegion.left =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800428 -mBubbleOffscreen
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800429 + Math.max(
430 insets.getSystemWindowInsetLeft(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500431 insets.getDisplayCutout() != null
432 ? insets.getDisplayCutout().getSafeInsetLeft()
433 : 0);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500434 allowableRegion.right =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800435 mLayout.getWidth()
436 - mIndividualBubbleSize
437 + mBubbleOffscreen
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800438 - Math.max(
439 insets.getSystemWindowInsetRight(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500440 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500441 ? insets.getDisplayCutout().getSafeInsetRight()
442 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800443
Joshua Tsujif44347f2019-02-12 14:28:06 -0500444 allowableRegion.top =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800445 mBubblePadding
446 + Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -0500447 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500448 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500449 ? insets.getDisplayCutout().getSafeInsetTop()
450 : 0);
451 allowableRegion.bottom =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800452 mLayout.getHeight()
453 - mIndividualBubbleSize
454 - mBubblePadding
Joshua Tsujia19515f2019-02-13 18:02:29 -0500455 - (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePadding : 0f)
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800456 - Math.max(
457 insets.getSystemWindowInsetBottom(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500458 insets.getDisplayCutout() != null
459 ? insets.getDisplayCutout().getSafeInsetBottom()
460 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800461 }
462
Joshua Tsujif44347f2019-02-12 14:28:06 -0500463 return allowableRegion;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800464 }
465
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400466 /** Moves the stack in response to a touch event. */
467 public void moveStackFromTouch(float x, float y) {
468
469 // If we're springing to the touch point to 'catch up' after dragging out of the dismiss
470 // target, then update the stack position animations instead of moving the bubble directly.
471 if (mFirstBubbleSpringingToTouch) {
472 final SpringAnimation springToTouchX =
473 (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X);
474 final SpringAnimation springToTouchY =
475 (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y);
476
477 // If either animation is still running, we haven't caught up. Update the animations.
478 if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
479 springToTouchX.animateToFinalPosition(x);
480 springToTouchY.animateToFinalPosition(y);
481 } else {
482 // If the animations have finished, the stack is now at the touch point. We can
483 // resume moving the bubble directly.
484 mFirstBubbleSpringingToTouch = false;
485 }
486 }
487
488 if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) {
489 moveFirstBubbleWithStackFollowing(x, y);
490 }
491 }
492
493 /**
494 * Demagnetizes the stack, springing it towards the given point. This also sets flags so that
495 * subsequent touch events will update the final position of the demagnetization spring instead
496 * of directly moving the bubbles, until demagnetization is complete.
497 */
498 public void demagnetizeFromDismissToPoint(float x, float y, float velX, float velY) {
499 mWithinDismissTarget = false;
500 mFirstBubbleSpringingToTouch = true;
501
502 springFirstBubbleWithStackFollowing(
503 DynamicAnimation.TRANSLATION_X,
504 new SpringForce()
505 .setDampingRatio(DEFAULT_BOUNCINESS)
506 .setStiffness(DEFAULT_STIFFNESS),
507 velX, x);
508
509 springFirstBubbleWithStackFollowing(
510 DynamicAnimation.TRANSLATION_Y,
511 new SpringForce()
512 .setDampingRatio(DEFAULT_BOUNCINESS)
513 .setStiffness(DEFAULT_STIFFNESS),
514 velY, y);
515 }
516
517 /**
518 * Spring the stack towards the dismiss target, respecting existing velocity. This also sets
519 * flags so that subsequent touch events will not move the stack until it's demagnetized.
520 */
521 public void magnetToDismiss(float velX, float velY, float destY, Runnable after) {
522 mWithinDismissTarget = true;
523 mFirstBubbleSpringingToTouch = false;
524
525 animationForChildAtIndex(0)
526 .translationX(mLayout.getWidth() / 2f - mIndividualBubbleSize / 2f)
527 .translationY(destY, after)
528 .withPositionStartVelocities(velX, velY)
529 .withStiffness(SpringForce.STIFFNESS_MEDIUM)
530 .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
531 .start();
532 }
533
534 /**
535 * 'Implode' the stack by shrinking the bubbles via chained animations and fading them out.
536 */
537 public void implodeStack(Runnable after) {
538 // Pop and fade the bubbles sequentially.
539 animationForChildAtIndex(0)
540 .scaleX(0.5f)
541 .scaleY(0.5f)
542 .alpha(0f)
543 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
544 .withStiffness(SpringForce.STIFFNESS_HIGH)
545 .start(() -> {
546 // Run the callback and reset flags. The child translation animations might
547 // still be running, but that's fine. Once the alpha is at 0f they're no longer
548 // visible anyway.
549 after.run();
550 mWithinDismissTarget = false;
551 });
552 }
553
554 /**
555 * Springs the first bubble to the given final position, with the rest of the stack 'following'.
556 */
557 protected void springFirstBubbleWithStackFollowing(
558 DynamicAnimation.ViewProperty property, SpringForce spring,
559 float vel, float finalPosition) {
560
561 if (mLayout.getChildCount() == 0) {
562 return;
563 }
564
565 Log.d(TAG, String.format("Springing %s to final position %f.",
566 PhysicsAnimationLayout.getReadablePropertyName(property),
567 finalPosition));
568
569 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
570 SpringAnimation springAnimation =
571 new SpringAnimation(this, firstBubbleProperty)
572 .setSpring(spring)
573 .setStartVelocity(vel);
574
575 cancelStackPositionAnimation(property);
576 mStackPositionAnimations.put(property, springAnimation);
577 springAnimation.animateToFinalPosition(finalPosition);
578 }
579
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800580 @Override
581 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
582 return Sets.newHashSet(
583 DynamicAnimation.TRANSLATION_X, // For positioning.
584 DynamicAnimation.TRANSLATION_Y,
585 DynamicAnimation.ALPHA, // For fading in new bubbles.
586 DynamicAnimation.SCALE_X, // For 'popping in' new bubbles.
587 DynamicAnimation.SCALE_Y);
588 }
589
590 @Override
591 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
592 if (property.equals(DynamicAnimation.TRANSLATION_X)
593 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400594 return index + 1;
595 } else if (mWithinDismissTarget) {
596 return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800597 } else {
598 return NONE;
599 }
600 }
601
602
603 @Override
604 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
605 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400606 // If we're in the dismiss target, have the bubbles pile on top of each other with no
607 // offset.
608 if (mWithinDismissTarget) {
609 return 0f;
610 } else {
611 // Offset to the left if we're on the left, or the right otherwise.
612 return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
613 ? -mStackOffset : mStackOffset;
614 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800615 } else {
616 return 0f;
617 }
618 }
619
620 @Override
621 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
622 return new SpringForce()
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400623 .setDampingRatio(DEFAULT_BOUNCINESS)
624 .setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800625 }
626
627 @Override
628 void onChildAdded(View child, int index) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800629 if (mLayout.getChildCount() == 1) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500630 // If this is the first child added, position the stack in its starting position before
631 // animating in.
632 moveStackToStartPosition(() -> animateInBubble(child));
633 } else if (mLayout.indexOfChild(child) == 0) {
634 // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
635 // to the back of the stack, it'll be largely invisible so don't bother animating it in.
636 animateInBubble(child);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800637 }
638 }
639
640 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500641 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500642 // Animate the removing view in the opposite direction of the stack.
643 final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujic1108432019-02-22 16:10:12 -0500644 animationForChild(child)
645 .alpha(0f, finishRemoval /* after */)
646 .scaleX(ANIMATE_IN_STARTING_SCALE)
647 .scaleY(ANIMATE_IN_STARTING_SCALE)
648 .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR))
649 .start();
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500650
Joshua Tsujic1108432019-02-22 16:10:12 -0500651 if (mLayout.getChildCount() > 0) {
652 animationForChildAtIndex(0).translationX(mStackPosition.x).start();
653 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800654 }
655
656 /** Moves the stack, without any animation, to the starting position. */
Joshua Tsujif44347f2019-02-12 14:28:06 -0500657 private void moveStackToStartPosition(Runnable after) {
658 // Post to ensure that the layout's width and height have been calculated.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500659 mLayout.setVisibility(View.INVISIBLE);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500660 mLayout.post(() -> {
661 setStackPosition(
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500662 mRestingStackPosition == null
663 ? getDefaultStartPosition()
664 : mRestingStackPosition);
665 mLayout.setVisibility(View.VISIBLE);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500666 after.run();
667 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800668 }
669
670 /**
671 * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
672 * bubbles to animate 'following' to the new location.
673 */
674 private void moveFirstBubbleWithStackFollowing(
675 DynamicAnimation.ViewProperty property, float value) {
676
677 // Update the canonical stack position.
678 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
679 mStackPosition.x = value;
680 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
681 mStackPosition.y = value;
682 }
683
684 if (mLayout.getChildCount() > 0) {
685 property.setValue(mLayout.getChildAt(0), value);
Joshua Tsujic1108432019-02-22 16:10:12 -0500686 if (mLayout.getChildCount() > 1) {
687 animationForChildAtIndex(1)
688 .property(property, value + getOffsetForChainedPropertyAnimation(property))
689 .start();
690 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800691 }
692 }
693
694 /** Moves the stack to a position instantly, with no animation. */
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500695 private void setStackPosition(PointF pos) {
696 Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
697 mStackPosition.set(pos.x, pos.y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800698
Joshua Tsuji19e22e4242019-04-17 13:29:10 -0400699 mLayout.cancelAllAnimations();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800700 cancelStackPositionAnimations();
701
702 // Since we're not using the chained animations, apply the offsets manually.
703 final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
704 final float yOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y);
705 for (int i = 0; i < mLayout.getChildCount(); i++) {
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500706 mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
707 mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800708 }
709 }
710
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500711 /** Returns the default stack position, which is on the top right. */
712 private PointF getDefaultStartPosition() {
713 return new PointF(
714 getAllowableStackPositionRegion().right,
715 getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
716 }
717
Joshua Tsujif44347f2019-02-12 14:28:06 -0500718 /** Animates in the given bubble. */
719 private void animateInBubble(View child) {
720 child.setTranslationY(mStackPosition.y);
721
Joshua Tsujif44347f2019-02-12 14:28:06 -0500722 float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujic1108432019-02-22 16:10:12 -0500723 animationForChild(child)
724 .scaleX(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
725 .scaleY(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
726 .alpha(0f /* from */, 1f /* to */)
727 .translationX(
728 mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset /* from */,
729 mStackPosition.x /* to */)
730 .start();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500731 }
732
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800733 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800734 * Cancels any outstanding first bubble property animations that are running. This does not
735 * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
736 * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
737 * {@link #flingThenSpringFirstBubbleWithStackFollowing}.
738 */
739 private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
740 if (mStackPositionAnimations.containsKey(property)) {
741 mStackPositionAnimations.get(property).cancel();
742 }
743 }
744
745 /**
746 * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
747 * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
748 * property directly to move the first bubble and cause the stack to 'follow' to the new
749 * location.
750 *
751 * This could also be achieved by simply animating the first bubble view and adding an update
752 * listener to dispatch movement to the rest of the stack. However, this would require
753 * duplication of logic in that update handler - it's simpler to keep all logic contained in the
754 * {@link #moveFirstBubbleWithStackFollowing} method.
755 */
756 private class StackPositionProperty
757 extends FloatPropertyCompat<StackAnimationController> {
758 private final DynamicAnimation.ViewProperty mProperty;
759
760 private StackPositionProperty(DynamicAnimation.ViewProperty property) {
761 super(property.toString());
762 mProperty = property;
763 }
764
765 @Override
766 public float getValue(StackAnimationController controller) {
Joshua Tsujid9422832019-03-05 13:32:37 -0500767 return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800768 }
769
770 @Override
771 public void setValue(StackAnimationController controller, float value) {
772 moveFirstBubbleWithStackFollowing(mProperty, value);
773 }
774 }
775}
776