blob: 3c4bc7259a6200d397e902d206d35a56a84a6fde [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;
Joshua Tsuji010c2b12019-02-25 18:11:25 -050033import com.android.systemui.bubbles.BubbleController;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080034
35import com.google.android.collect.Sets;
36
37import java.util.HashMap;
38import java.util.Set;
39
40/**
41 * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop
42 * each other with a slight offset to the left or right (depending on which side of the screen they
43 * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of
44 * the screen.
45 */
46public class StackAnimationController extends
47 PhysicsAnimationLayout.PhysicsAnimationController {
48
49 private static final String TAG = "Bubbs.StackCtrl";
50
51 /** Scale factor to use initially for new bubbles being animated in. */
52 private static final float ANIMATE_IN_STARTING_SCALE = 1.15f;
53
Joshua Tsujia08b6d32019-01-29 16:15:52 -050054 /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */
55 private static final int ANIMATE_TRANSLATION_FACTOR = 4;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080056
57 /**
58 * Values to use for the default {@link SpringForce} provided to the physics animation layout.
59 */
60 private static final float DEFAULT_STIFFNESS = 2500f;
61 private static final float DEFAULT_BOUNCINESS = 0.85f;
62
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 */
68 private static final float FLING_FRICTION_X = 1.15f;
69 private static final float FLING_FRICTION_Y = 1.5f;
70
71 /**
72 * Damping ratio to use for the stack spring animation used to spring the stack to its final
73 * position after a fling.
74 */
75 private static final float SPRING_DAMPING_RATIO = 0.85f;
76
77 /**
78 * Minimum fling velocity required to trigger moving the stack from one side of the screen to
79 * the other.
80 */
81 private static final float ESCAPE_VELOCITY = 750f;
82
83 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -080084 * The canonical position of the stack. This is typically the position of the first bubble, but
85 * we need to keep track of it separately from the first bubble's translation in case there are
86 * no bubbles, or the first bubble was just added and being animated to its new position.
87 */
88 private PointF mStackPosition = new PointF();
89
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050090 /** The most recent position in which the stack was resting on the edge of the screen. */
91 private PointF mRestingStackPosition;
92
Joshua Tsujia19515f2019-02-13 18:02:29 -050093 /** The height of the most recently visible IME. */
94 private float mImeHeight = 0f;
95
96 /**
97 * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
98 * IME is not visible or the user moved the stack since the IME became visible.
99 */
100 private float mPreImeY = Float.MIN_VALUE;
101
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800102 /**
103 * Animations on the stack position itself, which would have been started in
104 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to
105 * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect)
106 * to a legal position on the side of the screen.
107 */
108 private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations =
109 new HashMap<>();
110
111 /** Horizontal offset of bubbles in the stack. */
112 private float mStackOffset;
113 /** Diameter of the bubbles themselves. */
114 private int mIndividualBubbleSize;
115 /** Size of spacing around the bubbles, separating it from the edge of the screen. */
116 private int mBubblePadding;
117 /** How far offscreen the stack rests. */
118 private int mBubbleOffscreen;
119 /** How far down the screen the stack starts, when there is no pre-existing location. */
120 private int mStackStartingVerticalOffset;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500121 /** Height of the status bar. */
122 private float mStatusBarHeight;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800123
124 @Override
125 protected void setLayout(PhysicsAnimationLayout layout) {
126 super.setLayout(layout);
127
128 Resources res = layout.getResources();
129 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
130 mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
131 mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
132 mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
133 mStackStartingVerticalOffset =
134 res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500135 mStatusBarHeight =
136 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800137 }
138
139 /**
140 * Instantly move the first bubble to the given point, and animate the rest of the stack behind
141 * it with the 'following' effect.
142 */
143 public void moveFirstBubbleWithStackFollowing(float x, float y) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500144 // If we manually move the bubbles with the IME open, clear the return point since we don't
145 // want the stack to snap away from the new position.
146 mPreImeY = Float.MIN_VALUE;
147
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800148 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x);
149 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y);
150 }
151
152 /**
153 * The position of the stack - typically the position of the first bubble; if no bubbles have
154 * been added yet, it will be where the first bubble will go when added.
155 */
156 public PointF getStackPosition() {
157 return mStackPosition;
158 }
159
160 /**
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500161 * Flings the stack starting with the given velocities, springing it to the nearest edge
162 * afterward.
163 */
164 public void flingStackThenSpringToEdge(float x, float velX, float velY) {
165 final boolean stackOnLeftSide = x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2;
166
167 final boolean stackShouldFlingLeft = stackOnLeftSide
168 ? velX < ESCAPE_VELOCITY
169 : velX < -ESCAPE_VELOCITY;
170
171 final RectF stackBounds = getAllowableStackPositionRegion();
172
173 // Target X translation (either the left or right side of the screen).
174 final float destinationRelativeX = stackShouldFlingLeft
175 ? stackBounds.left : stackBounds.right;
176
Joshua Tsujicd169332019-03-06 23:56:52 -0500177 // Minimum velocity required for the stack to make it to the targeted side of the screen,
178 // taking friction into account (4.2f is the number that friction scalars are multiplied by
179 // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
180 // but the SpringAnimation at the end will ensure that it reaches the destination X
181 // regardless.
182 final float minimumVelocityToReachEdge =
183 (destinationRelativeX - x) * (FLING_FRICTION_X * 4.2f);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500184
185 // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
186 // that it'll make it all the way to the side of the screen.
187 final float startXVelocity = stackShouldFlingLeft
Joshua Tsujicd169332019-03-06 23:56:52 -0500188 ? Math.min(minimumVelocityToReachEdge, velX)
189 : Math.max(minimumVelocityToReachEdge, velX);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500190
191 flingThenSpringFirstBubbleWithStackFollowing(
192 DynamicAnimation.TRANSLATION_X,
193 startXVelocity,
194 FLING_FRICTION_X,
195 new SpringForce()
196 .setStiffness(SpringForce.STIFFNESS_LOW)
197 .setDampingRatio(SPRING_DAMPING_RATIO),
198 destinationRelativeX);
199
200 flingThenSpringFirstBubbleWithStackFollowing(
201 DynamicAnimation.TRANSLATION_Y,
202 velY,
203 FLING_FRICTION_Y,
204 new SpringForce()
205 .setStiffness(SpringForce.STIFFNESS_LOW)
206 .setDampingRatio(SPRING_DAMPING_RATIO),
207 /* destination */ null);
208
209 mLayout.setEndListenerForProperties(
210 (animation, canceled, value, velocity) -> {
211 mRestingStackPosition = new PointF();
212 mRestingStackPosition.set(mStackPosition);
213 mLayout.removeEndListenerForProperty(DynamicAnimation.TRANSLATION_X);
214 mLayout.removeEndListenerForProperty(DynamicAnimation.TRANSLATION_Y);
215 },
216 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
217 }
218
219 /**
Joshua Tsuji3829caa2019-03-05 18:09:13 -0500220 * Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
221 */
222 public PointF getStackPositionAlongNearestHorizontalEdge() {
223 final PointF stackPos = getStackPosition();
224 final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x);
225 final RectF bounds = getAllowableStackPositionRegion();
226
227 stackPos.x = onLeft ? bounds.left : bounds.right;
228 return stackPos;
229 }
230
231 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800232 * Flings the first bubble along the given property's axis, using the provided configuration
233 * values. When the animation ends - either by hitting the min/max, or by friction sufficiently
234 * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final
235 * position.
236 */
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500237 protected void flingThenSpringFirstBubbleWithStackFollowing(
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800238 DynamicAnimation.ViewProperty property,
239 float vel,
240 float friction,
241 SpringForce spring,
242 Float finalPosition) {
243 Log.d(TAG, String.format("Flinging %s.",
244 PhysicsAnimationLayout.getReadablePropertyName(property)));
245
246 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
247 final float currentValue = firstBubbleProperty.getValue(this);
248 final RectF bounds = getAllowableStackPositionRegion();
249 final float min =
250 property.equals(DynamicAnimation.TRANSLATION_X)
251 ? bounds.left
252 : bounds.top;
253 final float max =
254 property.equals(DynamicAnimation.TRANSLATION_X)
255 ? bounds.right
256 : bounds.bottom;
257
258 FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty);
259 flingAnimation.setFriction(friction)
260 .setStartVelocity(vel)
261
262 // If the bubble's property value starts beyond the desired min/max, use that value
263 // instead so that the animation won't immediately end. If, for example, the user
264 // drags the bubbles into the navigation bar, but then flings them upward, we want
265 // the fling to occur despite temporarily having a value outside of the min/max. If
266 // the bubbles are out of bounds and flung even farther out of bounds, the fling
267 // animation will halt immediately and the SpringAnimation will take over, springing
268 // it in reverse to the (legal) final position.
269 .setMinValue(Math.min(currentValue, min))
270 .setMaxValue(Math.max(currentValue, max))
271
272 .addEndListener((animation, canceled, endValue, endVelocity) -> {
273 if (!canceled) {
274 springFirstBubbleWithStackFollowing(property, spring, endVelocity,
275 finalPosition != null
276 ? finalPosition
277 : Math.max(min, Math.min(max, endValue)));
278 }
279 });
280
281 cancelStackPositionAnimation(property);
282 mStackPositionAnimations.put(property, flingAnimation);
283 flingAnimation.start();
284 }
285
286 /**
287 * Cancel any stack position animations that were started by calling
288 * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end
289 * listeners.
290 */
291 public void cancelStackPositionAnimations() {
292 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
293 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
294
295 mLayout.removeEndListenerForProperty(DynamicAnimation.TRANSLATION_X);
296 mLayout.removeEndListenerForProperty(DynamicAnimation.TRANSLATION_Y);
297 }
298
299 /**
Joshua Tsujia19515f2019-02-13 18:02:29 -0500300 * Save the IME height so that the allowable stack bounds reflect the now-visible IME, and
301 * animate the stack out of the way if necessary.
302 */
303 public void updateBoundsForVisibleImeAndAnimate(int imeHeight) {
304 mImeHeight = imeHeight;
305
306 final float maxBubbleY = getAllowableStackPositionRegion().bottom;
307 if (mStackPosition.y > maxBubbleY && mPreImeY == Float.MIN_VALUE) {
308 mPreImeY = mStackPosition.y;
309
310 springFirstBubbleWithStackFollowing(
311 DynamicAnimation.TRANSLATION_Y,
312 getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
313 .setStiffness(SpringForce.STIFFNESS_LOW),
314 /* startVel */ 0f,
315 maxBubbleY);
316 }
317 }
318
319 /**
320 * Clear the IME height from the bounds and animate the stack back to its original position,
321 * assuming it wasn't moved in the meantime.
322 */
323 public void updateBoundsForInvisibleImeAndAnimate() {
324 mImeHeight = 0;
325
326 if (mPreImeY > Float.MIN_VALUE) {
327 springFirstBubbleWithStackFollowing(
328 DynamicAnimation.TRANSLATION_Y,
329 getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
330 .setStiffness(SpringForce.STIFFNESS_LOW),
331 /* startVel */ 0f,
332 mPreImeY);
333 mPreImeY = Float.MIN_VALUE;
334 }
335 }
336
337 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800338 * Returns the region within which the stack is allowed to rest. This goes slightly off the left
339 * and right sides of the screen, below the status bar/cutout and above the navigation bar.
340 * While the stack is not allowed to rest outside of these bounds, it can temporarily be
341 * animated or dragged beyond them.
342 */
343 public RectF getAllowableStackPositionRegion() {
344 final WindowInsets insets = mLayout.getRootWindowInsets();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500345 final RectF allowableRegion = new RectF();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800346 if (insets != null) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500347 allowableRegion.left =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800348 -mBubbleOffscreen
349 - mBubblePadding
350 + Math.max(
351 insets.getSystemWindowInsetLeft(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500352 insets.getDisplayCutout() != null
353 ? insets.getDisplayCutout().getSafeInsetLeft()
354 : 0);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500355 allowableRegion.right =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800356 mLayout.getWidth()
357 - mIndividualBubbleSize
358 + mBubbleOffscreen
359 - mBubblePadding
360 - Math.max(
361 insets.getSystemWindowInsetRight(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500362 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500363 ? insets.getDisplayCutout().getSafeInsetRight()
364 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800365
Joshua Tsujif44347f2019-02-12 14:28:06 -0500366 allowableRegion.top =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800367 mBubblePadding
368 + Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -0500369 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500370 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500371 ? insets.getDisplayCutout().getSafeInsetTop()
372 : 0);
373 allowableRegion.bottom =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800374 mLayout.getHeight()
375 - mIndividualBubbleSize
376 - mBubblePadding
Joshua Tsujia19515f2019-02-13 18:02:29 -0500377 - (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePadding : 0f)
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800378 - Math.max(
379 insets.getSystemWindowInsetBottom(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500380 insets.getDisplayCutout() != null
381 ? insets.getDisplayCutout().getSafeInsetBottom()
382 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800383 }
384
Joshua Tsujif44347f2019-02-12 14:28:06 -0500385 return allowableRegion;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800386 }
387
388 @Override
389 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
390 return Sets.newHashSet(
391 DynamicAnimation.TRANSLATION_X, // For positioning.
392 DynamicAnimation.TRANSLATION_Y,
393 DynamicAnimation.ALPHA, // For fading in new bubbles.
394 DynamicAnimation.SCALE_X, // For 'popping in' new bubbles.
395 DynamicAnimation.SCALE_Y);
396 }
397
398 @Override
399 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
400 if (property.equals(DynamicAnimation.TRANSLATION_X)
401 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
402 return index + 1; // Just chain them linearly.
403 } else {
404 return NONE;
405 }
406 }
407
408
409 @Override
410 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
411 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
412 // Offset to the left if we're on the left, or the right otherwise.
413 return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
414 ? -mStackOffset : mStackOffset;
415 } else {
416 return 0f;
417 }
418 }
419
420 @Override
421 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
422 return new SpringForce()
Joshua Tsuji010c2b12019-02-25 18:11:25 -0500423 .setDampingRatio(BubbleController.getBubbleBounciness(
424 mLayout.getContext(), DEFAULT_BOUNCINESS))
425 .setStiffness(BubbleController.getBubbleStiffness(
Joshua Tsujia96f2db2019-03-08 14:43:25 -0500426 mLayout.getContext(), (int) DEFAULT_STIFFNESS));
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800427 }
428
429 @Override
430 void onChildAdded(View child, int index) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800431 if (mLayout.getChildCount() == 1) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500432 // If this is the first child added, position the stack in its starting position before
433 // animating in.
434 moveStackToStartPosition(() -> animateInBubble(child));
435 } else if (mLayout.indexOfChild(child) == 0) {
436 // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
437 // to the back of the stack, it'll be largely invisible so don't bother animating it in.
438 animateInBubble(child);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800439 }
440 }
441
442 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500443 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800444 // Animate the child out, actually removing it once its alpha is zero.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500445 mLayout.animateValueForChild(DynamicAnimation.ALPHA, child, 0f, finishRemoval);
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500446 mLayout.animateValueForChild(DynamicAnimation.SCALE_X, child, ANIMATE_IN_STARTING_SCALE);
447 mLayout.animateValueForChild(DynamicAnimation.SCALE_Y, child, ANIMATE_IN_STARTING_SCALE);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800448
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500449 // Animate the removing view in the opposite direction of the stack.
450 final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
451 mLayout.animateValueForChild(DynamicAnimation.TRANSLATION_X, child,
452 mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR));
453
454 // Pull the top of the stack to the correct position, the chained animations will instruct
455 // any children that are out of place to animate to the correct position.
456 mLayout.animateValueForChildAtIndex(DynamicAnimation.TRANSLATION_X, 0, mStackPosition.x);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800457 }
458
459 /** Moves the stack, without any animation, to the starting position. */
Joshua Tsujif44347f2019-02-12 14:28:06 -0500460 private void moveStackToStartPosition(Runnable after) {
461 // Post to ensure that the layout's width and height have been calculated.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500462 mLayout.setVisibility(View.INVISIBLE);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500463 mLayout.post(() -> {
464 setStackPosition(
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500465 mRestingStackPosition == null
466 ? getDefaultStartPosition()
467 : mRestingStackPosition);
468 mLayout.setVisibility(View.VISIBLE);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500469 after.run();
470 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800471 }
472
473 /**
474 * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
475 * bubbles to animate 'following' to the new location.
476 */
477 private void moveFirstBubbleWithStackFollowing(
478 DynamicAnimation.ViewProperty property, float value) {
479
480 // Update the canonical stack position.
481 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
482 mStackPosition.x = value;
483 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
484 mStackPosition.y = value;
485 }
486
487 if (mLayout.getChildCount() > 0) {
488 property.setValue(mLayout.getChildAt(0), value);
489 mLayout.animateValueForChildAtIndex(
490 property,
491 /* index */ 1,
492 value + getOffsetForChainedPropertyAnimation(property));
493 }
494 }
495
496 /** Moves the stack to a position instantly, with no animation. */
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500497 private void setStackPosition(PointF pos) {
498 Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
499 mStackPosition.set(pos.x, pos.y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800500
501 cancelStackPositionAnimations();
502
503 // Since we're not using the chained animations, apply the offsets manually.
504 final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
505 final float yOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y);
506 for (int i = 0; i < mLayout.getChildCount(); i++) {
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500507 mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
508 mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800509 }
510 }
511
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500512 /** Returns the default stack position, which is on the top right. */
513 private PointF getDefaultStartPosition() {
514 return new PointF(
515 getAllowableStackPositionRegion().right,
516 getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
517 }
518
Joshua Tsujif44347f2019-02-12 14:28:06 -0500519 /** Animates in the given bubble. */
520 private void animateInBubble(View child) {
521 child.setTranslationY(mStackPosition.y);
522
523 // Pop in the new bubble.
524 child.setScaleX(ANIMATE_IN_STARTING_SCALE);
525 child.setScaleY(ANIMATE_IN_STARTING_SCALE);
526 mLayout.animateValueForChildAtIndex(DynamicAnimation.SCALE_X, 0, 1f);
527 mLayout.animateValueForChildAtIndex(DynamicAnimation.SCALE_Y, 0, 1f);
528
529 // Fade in the new bubble.
530 child.setAlpha(0);
531 mLayout.animateValueForChildAtIndex(DynamicAnimation.ALPHA, 0, 1f);
532
533 // Start the new bubble 4x the normal offset distance in the opposite direction. We'll
534 // animate in from this position. Since the animations are chained, when the new bubble
535 // flies in from the side, it will push the other ones out of the way.
536 float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
537 child.setTranslationX(mStackPosition.x - ANIMATE_TRANSLATION_FACTOR * xOffset);
538 mLayout.animateValueForChildAtIndex(
539 DynamicAnimation.TRANSLATION_X, 0, mStackPosition.x);
540 }
541
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800542 /**
543 * Springs the first bubble to the given final position, with the rest of the stack 'following'.
544 */
545 private void springFirstBubbleWithStackFollowing(
546 DynamicAnimation.ViewProperty property, SpringForce spring,
547 float vel, float finalPosition) {
548
Joshua Tsujid9422832019-03-05 13:32:37 -0500549 if (mLayout.getChildCount() == 0) {
550 return;
551 }
552
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800553 Log.d(TAG, String.format("Springing %s to final position %f.",
Joshua Tsujia19515f2019-02-13 18:02:29 -0500554 PhysicsAnimationLayout.getReadablePropertyName(property),
555 finalPosition));
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800556
557 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
558 SpringAnimation springAnimation =
559 new SpringAnimation(this, firstBubbleProperty)
560 .setSpring(spring)
561 .setStartVelocity(vel);
562
563 cancelStackPositionAnimation(property);
564 mStackPositionAnimations.put(property, springAnimation);
565 springAnimation.animateToFinalPosition(finalPosition);
566 }
567
568 /**
569 * Cancels any outstanding first bubble property animations that are running. This does not
570 * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
571 * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
572 * {@link #flingThenSpringFirstBubbleWithStackFollowing}.
573 */
574 private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
575 if (mStackPositionAnimations.containsKey(property)) {
576 mStackPositionAnimations.get(property).cancel();
577 }
578 }
579
580 /**
581 * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
582 * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
583 * property directly to move the first bubble and cause the stack to 'follow' to the new
584 * location.
585 *
586 * This could also be achieved by simply animating the first bubble view and adding an update
587 * listener to dispatch movement to the rest of the stack. However, this would require
588 * duplication of logic in that update handler - it's simpler to keep all logic contained in the
589 * {@link #moveFirstBubbleWithStackFollowing} method.
590 */
591 private class StackPositionProperty
592 extends FloatPropertyCompat<StackAnimationController> {
593 private final DynamicAnimation.ViewProperty mProperty;
594
595 private StackPositionProperty(DynamicAnimation.ViewProperty property) {
596 super(property.toString());
597 mProperty = property;
598 }
599
600 @Override
601 public float getValue(StackAnimationController controller) {
Joshua Tsujid9422832019-03-05 13:32:37 -0500602 return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800603 }
604
605 @Override
606 public void setValue(StackAnimationController controller, float value) {
607 moveFirstBubbleWithStackFollowing(mProperty, value);
608 }
609 }
610}
611