blob: ab8752e4195fad2644ce249b8fdc719aa0473efe [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 */
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -040089 private PointF mStackPosition = new PointF(-1, -1);
90
91 /** Whether or not the stack's start position has been set. */
92 private boolean mStackMovedToStartPosition = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080093
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050094 /** The most recent position in which the stack was resting on the edge of the screen. */
95 private PointF mRestingStackPosition;
96
Joshua Tsujia19515f2019-02-13 18:02:29 -050097 /** The height of the most recently visible IME. */
98 private float mImeHeight = 0f;
99
100 /**
101 * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
102 * IME is not visible or the user moved the stack since the IME became visible.
103 */
104 private float mPreImeY = Float.MIN_VALUE;
105
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800106 /**
107 * Animations on the stack position itself, which would have been started in
108 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to
109 * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect)
110 * to a legal position on the side of the screen.
111 */
112 private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations =
113 new HashMap<>();
114
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400115 /**
116 * Whether the current motion of the stack is due to a fling animation (vs. being dragged
117 * manually).
118 */
119 private boolean mIsMovingFromFlinging = false;
120
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400121 /**
122 * Whether the stack is within the dismiss target (either by being dragged, magnet'd, or flung).
123 */
124 private boolean mWithinDismissTarget = false;
125
126 /**
127 * Whether the first bubble is springing towards the touch point, rather than using the default
128 * behavior of moving directly to the touch point with the rest of the stack following it.
129 *
130 * This happens when the user's finger exits the dismiss area while the stack is magnetized to
131 * the center. Since the touch point differs from the stack location, we need to animate the
132 * stack back to the touch point to avoid a jarring instant location change from the center of
133 * the target to the touch point just outside the target bounds.
134 *
135 * This is reset once the spring animations end, since that means the first bubble has
136 * successfully 'caught up' to the touch.
137 */
138 private boolean mFirstBubbleSpringingToTouch = false;
139
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800140 /** Horizontal offset of bubbles in the stack. */
141 private float mStackOffset;
142 /** Diameter of the bubbles themselves. */
143 private int mIndividualBubbleSize;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -0400144 /**
145 * The amount of space to add between the bubbles and certain UI elements, such as the top of
146 * the screen or the IME. This does not apply to the left/right sides of the screen since the
147 * stack goes offscreen intentionally.
148 */
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800149 private int mBubblePadding;
150 /** How far offscreen the stack rests. */
151 private int mBubbleOffscreen;
152 /** How far down the screen the stack starts, when there is no pre-existing location. */
153 private int mStackStartingVerticalOffset;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500154 /** Height of the status bar. */
155 private float mStatusBarHeight;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800156
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800157 /**
158 * Instantly move the first bubble to the given point, and animate the rest of the stack behind
159 * it with the 'following' effect.
160 */
161 public void moveFirstBubbleWithStackFollowing(float x, float y) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500162 // If we manually move the bubbles with the IME open, clear the return point since we don't
163 // want the stack to snap away from the new position.
164 mPreImeY = Float.MIN_VALUE;
165
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800166 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x);
167 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y);
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400168
169 // This method is called when the stack is being dragged manually, so we're clearly no
170 // longer flinging.
171 mIsMovingFromFlinging = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800172 }
173
174 /**
175 * The position of the stack - typically the position of the first bubble; if no bubbles have
176 * been added yet, it will be where the first bubble will go when added.
177 */
178 public PointF getStackPosition() {
179 return mStackPosition;
180 }
181
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400182 /** Whether the stack is on the left side of the screen. */
183 public boolean isStackOnLeftSide() {
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400184 if (mLayout == null || !isStackPositionSet()) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400185 return false;
186 }
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400187
Lyn Hane68d0912019-05-02 18:28:01 -0700188 float stackCenter = mStackPosition.x + mIndividualBubbleSize / 2;
189 float screenCenter = mLayout.getWidth() / 2;
190 return stackCenter < screenCenter;
191 }
192
193 /**
194 * Fling stack to given corner, within allowable screen bounds.
195 * Note that we need new SpringForce instances per animation despite identical configs because
196 * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
197 */
198 public void springStack(float destinationX, float destinationY) {
199 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
200 new SpringForce()
201 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
202 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
203 0 /* startXVelocity */,
204 destinationX);
205
206 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y,
207 new SpringForce()
208 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
209 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
210 0 /* startYVelocity */,
211 destinationY);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400212 }
213
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800214 /**
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500215 * Flings the stack starting with the given velocities, springing it to the nearest edge
216 * afterward.
Joshua Tsuji6549e702019-05-02 13:13:16 -0400217 *
218 * @return The X value that the stack will end up at after the fling/spring.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500219 */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400220 public float flingStackThenSpringToEdge(float x, float velX, float velY) {
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500221 final boolean stackOnLeftSide = x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2;
222
223 final boolean stackShouldFlingLeft = stackOnLeftSide
224 ? velX < ESCAPE_VELOCITY
225 : velX < -ESCAPE_VELOCITY;
226
227 final RectF stackBounds = getAllowableStackPositionRegion();
228
229 // Target X translation (either the left or right side of the screen).
230 final float destinationRelativeX = stackShouldFlingLeft
231 ? stackBounds.left : stackBounds.right;
232
Joshua Tsujicd169332019-03-06 23:56:52 -0500233 // Minimum velocity required for the stack to make it to the targeted side of the screen,
234 // taking friction into account (4.2f is the number that friction scalars are multiplied by
235 // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
236 // but the SpringAnimation at the end will ensure that it reaches the destination X
237 // regardless.
238 final float minimumVelocityToReachEdge =
239 (destinationRelativeX - x) * (FLING_FRICTION_X * 4.2f);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500240
241 // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
242 // that it'll make it all the way to the side of the screen.
243 final float startXVelocity = stackShouldFlingLeft
Joshua Tsujicd169332019-03-06 23:56:52 -0500244 ? Math.min(minimumVelocityToReachEdge, velX)
245 : Math.max(minimumVelocityToReachEdge, velX);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500246
247 flingThenSpringFirstBubbleWithStackFollowing(
248 DynamicAnimation.TRANSLATION_X,
249 startXVelocity,
250 FLING_FRICTION_X,
251 new SpringForce()
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400252 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
253 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500254 destinationRelativeX);
255
256 flingThenSpringFirstBubbleWithStackFollowing(
257 DynamicAnimation.TRANSLATION_Y,
258 velY,
259 FLING_FRICTION_Y,
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 /* destination */ null);
264
Joshua Tsujic1108432019-02-22 16:10:12 -0500265 mLayout.setEndActionForMultipleProperties(
266 () -> {
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500267 mRestingStackPosition = new PointF();
268 mRestingStackPosition.set(mStackPosition);
Joshua Tsujic1108432019-02-22 16:10:12 -0500269 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
270 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500271 },
272 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400273
Joshua Tsuji4bb3e7e2019-05-29 16:24:43 -0400274 // If we're flinging now, there's no more touch event to catch up to.
275 mFirstBubbleSpringingToTouch = false;
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400276 mIsMovingFromFlinging = true;
Joshua Tsuji6549e702019-05-02 13:13:16 -0400277 return destinationRelativeX;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500278 }
279
280 /**
Joshua Tsuji3829caa2019-03-05 18:09:13 -0500281 * Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
282 */
283 public PointF getStackPositionAlongNearestHorizontalEdge() {
284 final PointF stackPos = getStackPosition();
285 final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x);
286 final RectF bounds = getAllowableStackPositionRegion();
287
288 stackPos.x = onLeft ? bounds.left : bounds.right;
289 return stackPos;
290 }
291
292 /**
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400293 * Moves the stack in response to rotation. We keep it in the most similar position by keeping
294 * it on the same side, and positioning it the same percentage of the way down the screen
295 * (taking status bar/nav bar into account by using the allowable region's height).
296 */
297 public void moveStackToSimilarPositionAfterRotation(boolean wasOnLeft, float verticalPercent) {
298 final RectF allowablePos = getAllowableStackPositionRegion();
299 final float allowableRegionHeight = allowablePos.bottom - allowablePos.top;
300
301 final float x = wasOnLeft ? allowablePos.left : allowablePos.right;
302 final float y = (allowableRegionHeight * verticalPercent) + allowablePos.top;
303
304 setStackPosition(new PointF(x, y));
305 }
306
307 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800308 * Flings the first bubble along the given property's axis, using the provided configuration
309 * values. When the animation ends - either by hitting the min/max, or by friction sufficiently
310 * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final
311 * position.
312 */
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500313 protected void flingThenSpringFirstBubbleWithStackFollowing(
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800314 DynamicAnimation.ViewProperty property,
315 float vel,
316 float friction,
317 SpringForce spring,
318 Float finalPosition) {
319 Log.d(TAG, String.format("Flinging %s.",
320 PhysicsAnimationLayout.getReadablePropertyName(property)));
321
322 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
323 final float currentValue = firstBubbleProperty.getValue(this);
324 final RectF bounds = getAllowableStackPositionRegion();
325 final float min =
326 property.equals(DynamicAnimation.TRANSLATION_X)
327 ? bounds.left
328 : bounds.top;
329 final float max =
330 property.equals(DynamicAnimation.TRANSLATION_X)
331 ? bounds.right
332 : bounds.bottom;
333
334 FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty);
335 flingAnimation.setFriction(friction)
336 .setStartVelocity(vel)
337
338 // If the bubble's property value starts beyond the desired min/max, use that value
339 // instead so that the animation won't immediately end. If, for example, the user
340 // drags the bubbles into the navigation bar, but then flings them upward, we want
341 // the fling to occur despite temporarily having a value outside of the min/max. If
342 // the bubbles are out of bounds and flung even farther out of bounds, the fling
343 // animation will halt immediately and the SpringAnimation will take over, springing
344 // it in reverse to the (legal) final position.
345 .setMinValue(Math.min(currentValue, min))
346 .setMaxValue(Math.max(currentValue, max))
347
348 .addEndListener((animation, canceled, endValue, endVelocity) -> {
349 if (!canceled) {
350 springFirstBubbleWithStackFollowing(property, spring, endVelocity,
351 finalPosition != null
352 ? finalPosition
353 : Math.max(min, Math.min(max, endValue)));
354 }
355 });
356
357 cancelStackPositionAnimation(property);
358 mStackPositionAnimations.put(property, flingAnimation);
359 flingAnimation.start();
360 }
361
362 /**
363 * Cancel any stack position animations that were started by calling
364 * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end
365 * listeners.
366 */
367 public void cancelStackPositionAnimations() {
368 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
369 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
370
Joshua Tsujic1108432019-02-22 16:10:12 -0500371 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
372 mLayout.removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800373 }
374
Joshua Tsuji4b395912019-04-19 17:18:40 -0400375 /** Save the current IME height so that we know where the stack bounds should be. */
376 public void setImeHeight(int imeHeight) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500377 mImeHeight = imeHeight;
Joshua Tsuji4b395912019-04-19 17:18:40 -0400378 }
Joshua Tsujia19515f2019-02-13 18:02:29 -0500379
Joshua Tsuji4b395912019-04-19 17:18:40 -0400380 /**
381 * Animates the stack either away from the newly visible IME, or back to its original position
382 * due to the IME going away.
383 */
384 public void animateForImeVisibility(boolean imeVisible) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500385 final float maxBubbleY = getAllowableStackPositionRegion().bottom;
Joshua Tsuji4b395912019-04-19 17:18:40 -0400386 float destinationY = Float.MIN_VALUE;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500387
Joshua Tsuji4b395912019-04-19 17:18:40 -0400388 if (imeVisible) {
Lyn Hane68d0912019-05-02 18:28:01 -0700389 // Stack is lower than it should be and overlaps the now-visible IME.
Joshua Tsuji4b395912019-04-19 17:18:40 -0400390 if (mStackPosition.y > maxBubbleY && mPreImeY == Float.MIN_VALUE) {
391 mPreImeY = mStackPosition.y;
392 destinationY = maxBubbleY;
393 }
394 } else {
395 if (mPreImeY > Float.MIN_VALUE) {
396 destinationY = mPreImeY;
397 mPreImeY = Float.MIN_VALUE;
398 }
399 }
400
401 if (destinationY > Float.MIN_VALUE) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500402 springFirstBubbleWithStackFollowing(
403 DynamicAnimation.TRANSLATION_Y,
404 getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
405 .setStiffness(SpringForce.STIFFNESS_LOW),
406 /* startVel */ 0f,
Joshua Tsuji4b395912019-04-19 17:18:40 -0400407 destinationY);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500408 }
409 }
410
411 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800412 * Returns the region within which the stack is allowed to rest. This goes slightly off the left
413 * and right sides of the screen, below the status bar/cutout and above the navigation bar.
414 * While the stack is not allowed to rest outside of these bounds, it can temporarily be
415 * animated or dragged beyond them.
416 */
417 public RectF getAllowableStackPositionRegion() {
418 final WindowInsets insets = mLayout.getRootWindowInsets();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500419 final RectF allowableRegion = new RectF();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800420 if (insets != null) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500421 allowableRegion.left =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800422 -mBubbleOffscreen
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800423 + Math.max(
424 insets.getSystemWindowInsetLeft(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500425 insets.getDisplayCutout() != null
426 ? insets.getDisplayCutout().getSafeInsetLeft()
427 : 0);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500428 allowableRegion.right =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800429 mLayout.getWidth()
430 - mIndividualBubbleSize
431 + mBubbleOffscreen
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800432 - Math.max(
433 insets.getSystemWindowInsetRight(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500434 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500435 ? insets.getDisplayCutout().getSafeInsetRight()
436 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800437
Joshua Tsujif44347f2019-02-12 14:28:06 -0500438 allowableRegion.top =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800439 mBubblePadding
440 + Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -0500441 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500442 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500443 ? insets.getDisplayCutout().getSafeInsetTop()
444 : 0);
445 allowableRegion.bottom =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800446 mLayout.getHeight()
447 - mIndividualBubbleSize
448 - mBubblePadding
Joshua Tsujia19515f2019-02-13 18:02:29 -0500449 - (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePadding : 0f)
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800450 - Math.max(
451 insets.getSystemWindowInsetBottom(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500452 insets.getDisplayCutout() != null
453 ? insets.getDisplayCutout().getSafeInsetBottom()
454 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800455 }
456
Joshua Tsujif44347f2019-02-12 14:28:06 -0500457 return allowableRegion;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800458 }
459
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400460 /** Moves the stack in response to a touch event. */
461 public void moveStackFromTouch(float x, float y) {
462
463 // If we're springing to the touch point to 'catch up' after dragging out of the dismiss
464 // target, then update the stack position animations instead of moving the bubble directly.
465 if (mFirstBubbleSpringingToTouch) {
466 final SpringAnimation springToTouchX =
467 (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X);
468 final SpringAnimation springToTouchY =
469 (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y);
470
471 // If either animation is still running, we haven't caught up. Update the animations.
472 if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
473 springToTouchX.animateToFinalPosition(x);
474 springToTouchY.animateToFinalPosition(y);
475 } else {
476 // If the animations have finished, the stack is now at the touch point. We can
477 // resume moving the bubble directly.
478 mFirstBubbleSpringingToTouch = false;
479 }
480 }
481
482 if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) {
483 moveFirstBubbleWithStackFollowing(x, y);
484 }
485 }
486
487 /**
488 * Demagnetizes the stack, springing it towards the given point. This also sets flags so that
489 * subsequent touch events will update the final position of the demagnetization spring instead
490 * of directly moving the bubbles, until demagnetization is complete.
491 */
492 public void demagnetizeFromDismissToPoint(float x, float y, float velX, float velY) {
493 mWithinDismissTarget = false;
494 mFirstBubbleSpringingToTouch = true;
495
496 springFirstBubbleWithStackFollowing(
497 DynamicAnimation.TRANSLATION_X,
498 new SpringForce()
499 .setDampingRatio(DEFAULT_BOUNCINESS)
500 .setStiffness(DEFAULT_STIFFNESS),
501 velX, x);
502
503 springFirstBubbleWithStackFollowing(
504 DynamicAnimation.TRANSLATION_Y,
505 new SpringForce()
506 .setDampingRatio(DEFAULT_BOUNCINESS)
507 .setStiffness(DEFAULT_STIFFNESS),
508 velY, y);
509 }
510
511 /**
512 * Spring the stack towards the dismiss target, respecting existing velocity. This also sets
513 * flags so that subsequent touch events will not move the stack until it's demagnetized.
514 */
515 public void magnetToDismiss(float velX, float velY, float destY, Runnable after) {
516 mWithinDismissTarget = true;
517 mFirstBubbleSpringingToTouch = false;
518
519 animationForChildAtIndex(0)
520 .translationX(mLayout.getWidth() / 2f - mIndividualBubbleSize / 2f)
521 .translationY(destY, after)
522 .withPositionStartVelocities(velX, velY)
523 .withStiffness(SpringForce.STIFFNESS_MEDIUM)
524 .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
525 .start();
526 }
527
528 /**
529 * 'Implode' the stack by shrinking the bubbles via chained animations and fading them out.
530 */
531 public void implodeStack(Runnable after) {
532 // Pop and fade the bubbles sequentially.
533 animationForChildAtIndex(0)
534 .scaleX(0.5f)
535 .scaleY(0.5f)
536 .alpha(0f)
537 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
538 .withStiffness(SpringForce.STIFFNESS_HIGH)
539 .start(() -> {
540 // Run the callback and reset flags. The child translation animations might
541 // still be running, but that's fine. Once the alpha is at 0f they're no longer
542 // visible anyway.
543 after.run();
544 mWithinDismissTarget = false;
545 });
546 }
547
548 /**
549 * Springs the first bubble to the given final position, with the rest of the stack 'following'.
550 */
551 protected void springFirstBubbleWithStackFollowing(
552 DynamicAnimation.ViewProperty property, SpringForce spring,
553 float vel, float finalPosition) {
554
555 if (mLayout.getChildCount() == 0) {
556 return;
557 }
558
559 Log.d(TAG, String.format("Springing %s to final position %f.",
560 PhysicsAnimationLayout.getReadablePropertyName(property),
561 finalPosition));
562
563 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
564 SpringAnimation springAnimation =
565 new SpringAnimation(this, firstBubbleProperty)
566 .setSpring(spring)
567 .setStartVelocity(vel);
568
569 cancelStackPositionAnimation(property);
570 mStackPositionAnimations.put(property, springAnimation);
571 springAnimation.animateToFinalPosition(finalPosition);
572 }
573
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800574 @Override
575 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
576 return Sets.newHashSet(
577 DynamicAnimation.TRANSLATION_X, // For positioning.
578 DynamicAnimation.TRANSLATION_Y,
579 DynamicAnimation.ALPHA, // For fading in new bubbles.
580 DynamicAnimation.SCALE_X, // For 'popping in' new bubbles.
581 DynamicAnimation.SCALE_Y);
582 }
583
584 @Override
585 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
586 if (property.equals(DynamicAnimation.TRANSLATION_X)
587 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400588 return index + 1;
589 } else if (mWithinDismissTarget) {
590 return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800591 } else {
592 return NONE;
593 }
594 }
595
596
597 @Override
598 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
599 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400600 // If we're in the dismiss target, have the bubbles pile on top of each other with no
601 // offset.
602 if (mWithinDismissTarget) {
603 return 0f;
604 } else {
605 // Offset to the left if we're on the left, or the right otherwise.
606 return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
607 ? -mStackOffset : mStackOffset;
608 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800609 } else {
610 return 0f;
611 }
612 }
613
614 @Override
615 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
616 return new SpringForce()
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400617 .setDampingRatio(DEFAULT_BOUNCINESS)
618 .setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800619 }
620
621 @Override
622 void onChildAdded(View child, int index) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800623 if (mLayout.getChildCount() == 1) {
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400624 // If this is the first child added, position the stack in its starting position.
625 moveStackToStartPosition();
626 } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500627 // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
628 // to the back of the stack, it'll be largely invisible so don't bother animating it in.
629 animateInBubble(child);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800630 }
631 }
632
633 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500634 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500635 // Animate the removing view in the opposite direction of the stack.
636 final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujic1108432019-02-22 16:10:12 -0500637 animationForChild(child)
638 .alpha(0f, finishRemoval /* after */)
639 .scaleX(ANIMATE_IN_STARTING_SCALE)
640 .scaleY(ANIMATE_IN_STARTING_SCALE)
641 .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR))
642 .start();
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500643
Joshua Tsujic1108432019-02-22 16:10:12 -0500644 if (mLayout.getChildCount() > 0) {
645 animationForChildAtIndex(0).translationX(mStackPosition.x).start();
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400646 } else {
647 // Set the start position back to the default since we're out of bubbles. New bubbles
648 // will then animate in from the start position.
649 mStackPosition = getDefaultStartPosition();
Joshua Tsujic1108432019-02-22 16:10:12 -0500650 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800651 }
652
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400653 @Override
Joshua Tsujif49ee142019-05-29 16:32:01 -0400654 void onChildReordered(View child, int oldIndex, int newIndex) {}
655
656 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400657 void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
658 Resources res = layout.getResources();
659 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
660 mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
661 mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
662 mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
663 mStackStartingVerticalOffset =
664 res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
665 mStatusBarHeight =
666 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
667 }
668
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800669 /** Moves the stack, without any animation, to the starting position. */
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400670 private void moveStackToStartPosition() {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500671 // Post to ensure that the layout's width and height have been calculated.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500672 mLayout.setVisibility(View.INVISIBLE);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500673 mLayout.post(() -> {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400674 setStackPosition(mRestingStackPosition == null
675 ? getDefaultStartPosition()
676 : mRestingStackPosition);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400677 mStackMovedToStartPosition = true;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500678 mLayout.setVisibility(View.VISIBLE);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400679
680 // Animate in the top bubble now that we're visible.
681 if (mLayout.getChildCount() > 0) {
682 animateInBubble(mLayout.getChildAt(0));
683 }
Joshua Tsujif44347f2019-02-12 14:28:06 -0500684 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800685 }
686
687 /**
688 * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
689 * bubbles to animate 'following' to the new location.
690 */
691 private void moveFirstBubbleWithStackFollowing(
692 DynamicAnimation.ViewProperty property, float value) {
693
694 // Update the canonical stack position.
695 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
696 mStackPosition.x = value;
697 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
698 mStackPosition.y = value;
699 }
700
701 if (mLayout.getChildCount() > 0) {
702 property.setValue(mLayout.getChildAt(0), value);
Joshua Tsujic1108432019-02-22 16:10:12 -0500703 if (mLayout.getChildCount() > 1) {
704 animationForChildAtIndex(1)
705 .property(property, value + getOffsetForChainedPropertyAnimation(property))
706 .start();
707 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800708 }
709 }
710
711 /** Moves the stack to a position instantly, with no animation. */
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500712 private void setStackPosition(PointF pos) {
713 Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
714 mStackPosition.set(pos.x, pos.y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800715
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400716 // If we're not the active controller, we don't want to physically move the bubble views.
717 if (isActiveController()) {
718 mLayout.cancelAllAnimations();
719 cancelStackPositionAnimations();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800720
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400721 // Since we're not using the chained animations, apply the offsets manually.
722 final float xOffset = getOffsetForChainedPropertyAnimation(
723 DynamicAnimation.TRANSLATION_X);
724 final float yOffset = getOffsetForChainedPropertyAnimation(
725 DynamicAnimation.TRANSLATION_Y);
726 for (int i = 0; i < mLayout.getChildCount(); i++) {
727 mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
728 mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
729 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800730 }
731 }
732
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500733 /** Returns the default stack position, which is on the top right. */
734 private PointF getDefaultStartPosition() {
735 return new PointF(
736 getAllowableStackPositionRegion().right,
737 getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
738 }
739
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400740 private boolean isStackPositionSet() {
741 return mStackMovedToStartPosition;
742 }
743
Joshua Tsujif44347f2019-02-12 14:28:06 -0500744 /** Animates in the given bubble. */
745 private void animateInBubble(View child) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400746 if (!isActiveController()) {
747 return;
748 }
749
Joshua Tsujif44347f2019-02-12 14:28:06 -0500750 child.setTranslationY(mStackPosition.y);
751
Joshua Tsujif44347f2019-02-12 14:28:06 -0500752 float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujic1108432019-02-22 16:10:12 -0500753 animationForChild(child)
754 .scaleX(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
755 .scaleY(ANIMATE_IN_STARTING_SCALE /* from */, 1f /* to */)
756 .alpha(0f /* from */, 1f /* to */)
757 .translationX(
758 mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset /* from */,
759 mStackPosition.x /* to */)
760 .start();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500761 }
762
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800763 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800764 * Cancels any outstanding first bubble property animations that are running. This does not
765 * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
766 * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
767 * {@link #flingThenSpringFirstBubbleWithStackFollowing}.
768 */
769 private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
770 if (mStackPositionAnimations.containsKey(property)) {
771 mStackPositionAnimations.get(property).cancel();
772 }
773 }
774
775 /**
776 * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
777 * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
778 * property directly to move the first bubble and cause the stack to 'follow' to the new
779 * location.
780 *
781 * This could also be achieved by simply animating the first bubble view and adding an update
782 * listener to dispatch movement to the rest of the stack. However, this would require
783 * duplication of logic in that update handler - it's simpler to keep all logic contained in the
784 * {@link #moveFirstBubbleWithStackFollowing} method.
785 */
786 private class StackPositionProperty
787 extends FloatPropertyCompat<StackAnimationController> {
788 private final DynamicAnimation.ViewProperty mProperty;
789
790 private StackPositionProperty(DynamicAnimation.ViewProperty property) {
791 super(property.toString());
792 mProperty = property;
793 }
794
795 @Override
796 public float getValue(StackAnimationController controller) {
Joshua Tsujid9422832019-03-05 13:32:37 -0500797 return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800798 }
799
800 @Override
801 public void setValue(StackAnimationController controller, float value) {
802 moveFirstBubbleWithStackFollowing(mProperty, value);
803 }
804 }
805}
806