blob: 2ec09a9dbea241c934d229f8cc6e3ea367461be7 [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
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040026import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080027import androidx.dynamicanimation.animation.DynamicAnimation;
28import androidx.dynamicanimation.animation.FlingAnimation;
29import androidx.dynamicanimation.animation.FloatPropertyCompat;
30import androidx.dynamicanimation.animation.SpringAnimation;
31import androidx.dynamicanimation.animation.SpringForce;
32
33import com.android.systemui.R;
34
35import com.google.android.collect.Sets;
36
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040037import java.io.FileDescriptor;
38import java.io.PrintWriter;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080039import java.util.HashMap;
40import java.util.Set;
41
42/**
43 * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop
44 * each other with a slight offset to the left or right (depending on which side of the screen they
45 * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of
46 * the screen.
47 */
48public class StackAnimationController extends
49 PhysicsAnimationLayout.PhysicsAnimationController {
50
51 private static final String TAG = "Bubbs.StackCtrl";
52
53 /** Scale factor to use initially for new bubbles being animated in. */
54 private static final float ANIMATE_IN_STARTING_SCALE = 1.15f;
55
Joshua Tsujia08b6d32019-01-29 16:15:52 -050056 /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */
57 private static final int ANIMATE_TRANSLATION_FACTOR = 4;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080058
Joshua Tsuji14e68552019-06-06 17:17:08 -040059 /** Values to use for animating bubbles in. */
60 private static final float ANIMATE_IN_STIFFNESS = 1000f;
61 private static final int ANIMATE_IN_START_DELAY = 25;
62
Joshua Tsujib1a796b2019-01-16 15:43:12 -080063 /**
64 * Values to use for the default {@link SpringForce} provided to the physics animation layout.
65 */
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040066 private static final int DEFAULT_STIFFNESS = 12000;
67 private static final int FLING_FOLLOW_STIFFNESS = 20000;
68 private static final float DEFAULT_BOUNCINESS = 0.9f;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080069
70 /**
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050071 * Friction applied to fling animations. Since the stack must land on one of the sides of the
72 * screen, we want less friction horizontally so that the stack has a better chance of making it
73 * to the side without needing a spring.
74 */
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040075 private static final float FLING_FRICTION_X = 2.2f;
76 private static final float FLING_FRICTION_Y = 2.2f;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050077
78 /**
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040079 * Values to use for the stack spring animation used to spring the stack to its final position
80 * after a fling.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050081 */
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040082 private static final int SPRING_AFTER_FLING_STIFFNESS = 750;
83 private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050084
85 /**
86 * Minimum fling velocity required to trigger moving the stack from one side of the screen to
87 * the other.
88 */
89 private static final float ESCAPE_VELOCITY = 750f;
90
91 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -080092 * The canonical position of the stack. This is typically the position of the first bubble, but
93 * we need to keep track of it separately from the first bubble's translation in case there are
94 * no bubbles, or the first bubble was just added and being animated to its new position.
95 */
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -040096 private PointF mStackPosition = new PointF(-1, -1);
97
98 /** Whether or not the stack's start position has been set. */
99 private boolean mStackMovedToStartPosition = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800100
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500101 /** The most recent position in which the stack was resting on the edge of the screen. */
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400102 @Nullable private PointF mRestingStackPosition;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500103
Joshua Tsujia19515f2019-02-13 18:02:29 -0500104 /** The height of the most recently visible IME. */
105 private float mImeHeight = 0f;
106
107 /**
108 * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
109 * IME is not visible or the user moved the stack since the IME became visible.
110 */
111 private float mPreImeY = Float.MIN_VALUE;
112
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800113 /**
114 * Animations on the stack position itself, which would have been started in
115 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to
116 * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect)
117 * to a legal position on the side of the screen.
118 */
119 private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations =
120 new HashMap<>();
121
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400122 /**
123 * Whether the current motion of the stack is due to a fling animation (vs. being dragged
124 * manually).
125 */
126 private boolean mIsMovingFromFlinging = false;
127
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400128 /**
129 * Whether the stack is within the dismiss target (either by being dragged, magnet'd, or flung).
130 */
131 private boolean mWithinDismissTarget = false;
132
133 /**
134 * Whether the first bubble is springing towards the touch point, rather than using the default
135 * behavior of moving directly to the touch point with the rest of the stack following it.
136 *
137 * This happens when the user's finger exits the dismiss area while the stack is magnetized to
138 * the center. Since the touch point differs from the stack location, we need to animate the
139 * stack back to the touch point to avoid a jarring instant location change from the center of
140 * the target to the touch point just outside the target bounds.
141 *
142 * This is reset once the spring animations end, since that means the first bubble has
143 * successfully 'caught up' to the touch.
144 */
145 private boolean mFirstBubbleSpringingToTouch = false;
146
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800147 /** Horizontal offset of bubbles in the stack. */
148 private float mStackOffset;
Lyn Hana511e1fb2019-06-17 12:35:08 -0700149 /** Diameter of the bubble icon. */
Lyn Han1b4f25e2019-06-11 13:56:34 -0700150 private int mBubbleIconBitmapSize;
Lyn Hana511e1fb2019-06-17 12:35:08 -0700151 /** Width of the bubble (icon and padding). */
152 private int mBubbleSize;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -0400153 /**
154 * The amount of space to add between the bubbles and certain UI elements, such as the top of
155 * the screen or the IME. This does not apply to the left/right sides of the screen since the
156 * stack goes offscreen intentionally.
157 */
Lyn Han4a8efe32019-05-30 09:43:27 -0700158 private int mBubblePaddingTop;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800159 /** How far offscreen the stack rests. */
160 private int mBubbleOffscreen;
161 /** How far down the screen the stack starts, when there is no pre-existing location. */
162 private int mStackStartingVerticalOffset;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500163 /** Height of the status bar. */
164 private float mStatusBarHeight;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800165
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800166 /**
167 * Instantly move the first bubble to the given point, and animate the rest of the stack behind
168 * it with the 'following' effect.
169 */
170 public void moveFirstBubbleWithStackFollowing(float x, float y) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500171 // If we manually move the bubbles with the IME open, clear the return point since we don't
172 // want the stack to snap away from the new position.
173 mPreImeY = Float.MIN_VALUE;
174
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800175 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x);
176 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y);
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400177
178 // This method is called when the stack is being dragged manually, so we're clearly no
179 // longer flinging.
180 mIsMovingFromFlinging = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800181 }
182
183 /**
184 * The position of the stack - typically the position of the first bubble; if no bubbles have
185 * been added yet, it will be where the first bubble will go when added.
186 */
187 public PointF getStackPosition() {
188 return mStackPosition;
189 }
190
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400191 /** Whether the stack is on the left side of the screen. */
192 public boolean isStackOnLeftSide() {
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400193 if (mLayout == null || !isStackPositionSet()) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400194 return false;
195 }
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400196
Lyn Han1b4f25e2019-06-11 13:56:34 -0700197 float stackCenter = mStackPosition.x + mBubbleIconBitmapSize / 2;
Lyn Hane68d0912019-05-02 18:28:01 -0700198 float screenCenter = mLayout.getWidth() / 2;
199 return stackCenter < screenCenter;
200 }
201
202 /**
203 * Fling stack to given corner, within allowable screen bounds.
204 * Note that we need new SpringForce instances per animation despite identical configs because
205 * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
206 */
207 public void springStack(float destinationX, float destinationY) {
208 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
Joshua Tsujidebd8312019-06-06 17:17:08 -0400209 new SpringForce()
Lyn Hane68d0912019-05-02 18:28:01 -0700210 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
211 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujidebd8312019-06-06 17:17:08 -0400212 0 /* startXVelocity */,
213 destinationX);
Lyn Hane68d0912019-05-02 18:28:01 -0700214
215 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y,
Joshua Tsujidebd8312019-06-06 17:17:08 -0400216 new SpringForce()
Lyn Hane68d0912019-05-02 18:28:01 -0700217 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
218 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujidebd8312019-06-06 17:17:08 -0400219 0 /* startYVelocity */,
220 destinationY);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400221 }
222
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800223 /**
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500224 * Flings the stack starting with the given velocities, springing it to the nearest edge
225 * afterward.
Joshua Tsuji6549e702019-05-02 13:13:16 -0400226 *
227 * @return The X value that the stack will end up at after the fling/spring.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500228 */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400229 public float flingStackThenSpringToEdge(float x, float velX, float velY) {
Lyn Han1b4f25e2019-06-11 13:56:34 -0700230 final boolean stackOnLeftSide = x - mBubbleIconBitmapSize / 2 < mLayout.getWidth() / 2;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500231
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 Tsujiaf8df2d2019-08-01 16:08:01 -0400242 // If all bubbles were removed during a drag event, just return the X we would have animated
243 // to if there were still bubbles.
244 if (mLayout == null || mLayout.getChildCount() == 0) {
245 return destinationRelativeX;
246 }
247
Joshua Tsujicd169332019-03-06 23:56:52 -0500248 // Minimum velocity required for the stack to make it to the targeted side of the screen,
249 // taking friction into account (4.2f is the number that friction scalars are multiplied by
250 // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
251 // but the SpringAnimation at the end will ensure that it reaches the destination X
252 // regardless.
253 final float minimumVelocityToReachEdge =
254 (destinationRelativeX - x) * (FLING_FRICTION_X * 4.2f);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500255
256 // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
257 // that it'll make it all the way to the side of the screen.
258 final float startXVelocity = stackShouldFlingLeft
Joshua Tsujicd169332019-03-06 23:56:52 -0500259 ? Math.min(minimumVelocityToReachEdge, velX)
260 : Math.max(minimumVelocityToReachEdge, velX);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500261
262 flingThenSpringFirstBubbleWithStackFollowing(
263 DynamicAnimation.TRANSLATION_X,
264 startXVelocity,
265 FLING_FRICTION_X,
266 new SpringForce()
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400267 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
268 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500269 destinationRelativeX);
270
271 flingThenSpringFirstBubbleWithStackFollowing(
272 DynamicAnimation.TRANSLATION_Y,
273 velY,
274 FLING_FRICTION_Y,
275 new SpringForce()
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400276 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
277 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500278 /* destination */ null);
279
Joshua Tsuji4bb3e7e2019-05-29 16:24:43 -0400280 // If we're flinging now, there's no more touch event to catch up to.
281 mFirstBubbleSpringingToTouch = false;
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400282 mIsMovingFromFlinging = true;
Joshua Tsuji6549e702019-05-02 13:13:16 -0400283 return destinationRelativeX;
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
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400313 /** Description of current animation controller state. */
314 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
315 pw.println("StackAnimationController state:");
316 pw.print(" isActive: "); pw.println(isActiveController());
317 pw.print(" restingStackPos: ");
318 pw.println(mRestingStackPosition != null ? mRestingStackPosition.toString() : "null");
319 pw.print(" currentStackPos: "); pw.println(mStackPosition.toString());
320 pw.print(" isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging);
321 pw.print(" withinDismiss: "); pw.println(mWithinDismissTarget);
322 pw.print(" firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch);
323 }
324
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400325 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800326 * Flings the first bubble along the given property's axis, using the provided configuration
327 * values. When the animation ends - either by hitting the min/max, or by friction sufficiently
328 * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final
329 * position.
330 */
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500331 protected void flingThenSpringFirstBubbleWithStackFollowing(
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800332 DynamicAnimation.ViewProperty property,
333 float vel,
334 float friction,
335 SpringForce spring,
336 Float finalPosition) {
337 Log.d(TAG, String.format("Flinging %s.",
Joshua Tsujidebd8312019-06-06 17:17:08 -0400338 PhysicsAnimationLayout.getReadablePropertyName(property)));
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800339
340 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
341 final float currentValue = firstBubbleProperty.getValue(this);
342 final RectF bounds = getAllowableStackPositionRegion();
343 final float min =
344 property.equals(DynamicAnimation.TRANSLATION_X)
345 ? bounds.left
346 : bounds.top;
347 final float max =
348 property.equals(DynamicAnimation.TRANSLATION_X)
349 ? bounds.right
350 : bounds.bottom;
351
352 FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty);
353 flingAnimation.setFriction(friction)
354 .setStartVelocity(vel)
355
356 // If the bubble's property value starts beyond the desired min/max, use that value
357 // instead so that the animation won't immediately end. If, for example, the user
358 // drags the bubbles into the navigation bar, but then flings them upward, we want
359 // the fling to occur despite temporarily having a value outside of the min/max. If
360 // the bubbles are out of bounds and flung even farther out of bounds, the fling
361 // animation will halt immediately and the SpringAnimation will take over, springing
362 // it in reverse to the (legal) final position.
363 .setMinValue(Math.min(currentValue, min))
364 .setMaxValue(Math.max(currentValue, max))
365
366 .addEndListener((animation, canceled, endValue, endVelocity) -> {
367 if (!canceled) {
Joshua Tsujib35f5912019-07-24 16:15:21 -0400368 mRestingStackPosition = new PointF();
369 mRestingStackPosition.set(mStackPosition);
370
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800371 springFirstBubbleWithStackFollowing(property, spring, endVelocity,
372 finalPosition != null
373 ? finalPosition
374 : Math.max(min, Math.min(max, endValue)));
375 }
376 });
377
378 cancelStackPositionAnimation(property);
379 mStackPositionAnimations.put(property, flingAnimation);
380 flingAnimation.start();
381 }
382
383 /**
384 * Cancel any stack position animations that were started by calling
385 * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end
386 * listeners.
387 */
388 public void cancelStackPositionAnimations() {
389 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
390 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
391
Joshua Tsujidebd8312019-06-06 17:17:08 -0400392 removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
393 removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800394 }
395
Joshua Tsuji4b395912019-04-19 17:18:40 -0400396 /** Save the current IME height so that we know where the stack bounds should be. */
397 public void setImeHeight(int imeHeight) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500398 mImeHeight = imeHeight;
Joshua Tsuji4b395912019-04-19 17:18:40 -0400399 }
Joshua Tsujia19515f2019-02-13 18:02:29 -0500400
Joshua Tsuji4b395912019-04-19 17:18:40 -0400401 /**
402 * Animates the stack either away from the newly visible IME, or back to its original position
403 * due to the IME going away.
404 */
405 public void animateForImeVisibility(boolean imeVisible) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500406 final float maxBubbleY = getAllowableStackPositionRegion().bottom;
Joshua Tsuji4b395912019-04-19 17:18:40 -0400407 float destinationY = Float.MIN_VALUE;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500408
Joshua Tsuji4b395912019-04-19 17:18:40 -0400409 if (imeVisible) {
Lyn Hane68d0912019-05-02 18:28:01 -0700410 // Stack is lower than it should be and overlaps the now-visible IME.
Joshua Tsuji4b395912019-04-19 17:18:40 -0400411 if (mStackPosition.y > maxBubbleY && mPreImeY == Float.MIN_VALUE) {
412 mPreImeY = mStackPosition.y;
413 destinationY = maxBubbleY;
414 }
415 } else {
416 if (mPreImeY > Float.MIN_VALUE) {
417 destinationY = mPreImeY;
418 mPreImeY = Float.MIN_VALUE;
419 }
420 }
421
422 if (destinationY > Float.MIN_VALUE) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500423 springFirstBubbleWithStackFollowing(
424 DynamicAnimation.TRANSLATION_Y,
425 getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
426 .setStiffness(SpringForce.STIFFNESS_LOW),
427 /* startVel */ 0f,
Joshua Tsuji4b395912019-04-19 17:18:40 -0400428 destinationY);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500429 }
430 }
431
432 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800433 * Returns the region within which the stack is allowed to rest. This goes slightly off the left
434 * and right sides of the screen, below the status bar/cutout and above the navigation bar.
435 * While the stack is not allowed to rest outside of these bounds, it can temporarily be
436 * animated or dragged beyond them.
437 */
438 public RectF getAllowableStackPositionRegion() {
439 final WindowInsets insets = mLayout.getRootWindowInsets();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500440 final RectF allowableRegion = new RectF();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800441 if (insets != null) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500442 allowableRegion.left =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800443 -mBubbleOffscreen
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800444 + Math.max(
445 insets.getSystemWindowInsetLeft(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500446 insets.getDisplayCutout() != null
447 ? insets.getDisplayCutout().getSafeInsetLeft()
448 : 0);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500449 allowableRegion.right =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800450 mLayout.getWidth()
Lyn Hana511e1fb2019-06-17 12:35:08 -0700451 - mBubbleSize
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800452 + mBubbleOffscreen
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800453 - Math.max(
454 insets.getSystemWindowInsetRight(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500455 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500456 ? insets.getDisplayCutout().getSafeInsetRight()
457 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800458
Joshua Tsujif44347f2019-02-12 14:28:06 -0500459 allowableRegion.top =
Lyn Han4a8efe32019-05-30 09:43:27 -0700460 mBubblePaddingTop
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800461 + Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -0500462 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500463 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500464 ? insets.getDisplayCutout().getSafeInsetTop()
465 : 0);
466 allowableRegion.bottom =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800467 mLayout.getHeight()
Mady Mellor818eef02019-08-16 16:12:29 -0700468 - mBubbleSize
Lyn Han4a8efe32019-05-30 09:43:27 -0700469 - mBubblePaddingTop
470 - (mImeHeight > Float.MIN_VALUE ? mImeHeight + mBubblePaddingTop : 0f)
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800471 - Math.max(
472 insets.getSystemWindowInsetBottom(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500473 insets.getDisplayCutout() != null
474 ? insets.getDisplayCutout().getSafeInsetBottom()
475 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800476 }
477
Joshua Tsujif44347f2019-02-12 14:28:06 -0500478 return allowableRegion;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800479 }
480
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400481 /** Moves the stack in response to a touch event. */
482 public void moveStackFromTouch(float x, float y) {
483
484 // If we're springing to the touch point to 'catch up' after dragging out of the dismiss
485 // target, then update the stack position animations instead of moving the bubble directly.
486 if (mFirstBubbleSpringingToTouch) {
487 final SpringAnimation springToTouchX =
488 (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X);
489 final SpringAnimation springToTouchY =
490 (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y);
491
492 // If either animation is still running, we haven't caught up. Update the animations.
493 if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
494 springToTouchX.animateToFinalPosition(x);
495 springToTouchY.animateToFinalPosition(y);
496 } else {
497 // If the animations have finished, the stack is now at the touch point. We can
498 // resume moving the bubble directly.
499 mFirstBubbleSpringingToTouch = false;
500 }
501 }
502
503 if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) {
504 moveFirstBubbleWithStackFollowing(x, y);
505 }
506 }
507
508 /**
509 * Demagnetizes the stack, springing it towards the given point. This also sets flags so that
510 * subsequent touch events will update the final position of the demagnetization spring instead
511 * of directly moving the bubbles, until demagnetization is complete.
512 */
513 public void demagnetizeFromDismissToPoint(float x, float y, float velX, float velY) {
514 mWithinDismissTarget = false;
515 mFirstBubbleSpringingToTouch = true;
516
517 springFirstBubbleWithStackFollowing(
518 DynamicAnimation.TRANSLATION_X,
519 new SpringForce()
520 .setDampingRatio(DEFAULT_BOUNCINESS)
521 .setStiffness(DEFAULT_STIFFNESS),
522 velX, x);
523
524 springFirstBubbleWithStackFollowing(
525 DynamicAnimation.TRANSLATION_Y,
526 new SpringForce()
527 .setDampingRatio(DEFAULT_BOUNCINESS)
528 .setStiffness(DEFAULT_STIFFNESS),
529 velY, y);
530 }
531
532 /**
533 * Spring the stack towards the dismiss target, respecting existing velocity. This also sets
534 * flags so that subsequent touch events will not move the stack until it's demagnetized.
535 */
536 public void magnetToDismiss(float velX, float velY, float destY, Runnable after) {
537 mWithinDismissTarget = true;
538 mFirstBubbleSpringingToTouch = false;
539
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400540 springFirstBubbleWithStackFollowing(
541 DynamicAnimation.TRANSLATION_X,
542 new SpringForce()
543 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
544 .setStiffness(SpringForce.STIFFNESS_MEDIUM),
545 velX, mLayout.getWidth() / 2f - mBubbleIconBitmapSize / 2f);
546
547 springFirstBubbleWithStackFollowing(
548 DynamicAnimation.TRANSLATION_Y,
549 new SpringForce()
550 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
551 .setStiffness(SpringForce.STIFFNESS_MEDIUM),
552 velY, destY, after);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400553 }
554
555 /**
556 * 'Implode' the stack by shrinking the bubbles via chained animations and fading them out.
557 */
558 public void implodeStack(Runnable after) {
559 // Pop and fade the bubbles sequentially.
560 animationForChildAtIndex(0)
561 .scaleX(0.5f)
562 .scaleY(0.5f)
563 .alpha(0f)
564 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
565 .withStiffness(SpringForce.STIFFNESS_HIGH)
566 .start(() -> {
567 // Run the callback and reset flags. The child translation animations might
568 // still be running, but that's fine. Once the alpha is at 0f they're no longer
569 // visible anyway.
570 after.run();
571 mWithinDismissTarget = false;
572 });
573 }
574
575 /**
576 * Springs the first bubble to the given final position, with the rest of the stack 'following'.
577 */
578 protected void springFirstBubbleWithStackFollowing(
579 DynamicAnimation.ViewProperty property, SpringForce spring,
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400580 float vel, float finalPosition, @Nullable Runnable... after) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400581
582 if (mLayout.getChildCount() == 0) {
583 return;
584 }
585
586 Log.d(TAG, String.format("Springing %s to final position %f.",
587 PhysicsAnimationLayout.getReadablePropertyName(property),
588 finalPosition));
589
590 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
591 SpringAnimation springAnimation =
592 new SpringAnimation(this, firstBubbleProperty)
593 .setSpring(spring)
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400594 .addEndListener((dynamicAnimation, b, v, v1) -> {
595 if (after != null) {
596 for (Runnable callback : after) {
597 callback.run();
598 }
599 }
600 })
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400601 .setStartVelocity(vel);
602
603 cancelStackPositionAnimation(property);
604 mStackPositionAnimations.put(property, springAnimation);
605 springAnimation.animateToFinalPosition(finalPosition);
606 }
607
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800608 @Override
609 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
610 return Sets.newHashSet(
611 DynamicAnimation.TRANSLATION_X, // For positioning.
612 DynamicAnimation.TRANSLATION_Y,
613 DynamicAnimation.ALPHA, // For fading in new bubbles.
614 DynamicAnimation.SCALE_X, // For 'popping in' new bubbles.
615 DynamicAnimation.SCALE_Y);
616 }
617
618 @Override
619 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
620 if (property.equals(DynamicAnimation.TRANSLATION_X)
621 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400622 return index + 1;
623 } else if (mWithinDismissTarget) {
624 return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800625 } else {
626 return NONE;
627 }
628 }
629
630
631 @Override
632 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
633 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400634 // If we're in the dismiss target, have the bubbles pile on top of each other with no
635 // offset.
636 if (mWithinDismissTarget) {
637 return 0f;
638 } else {
639 // Offset to the left if we're on the left, or the right otherwise.
640 return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
641 ? -mStackOffset : mStackOffset;
642 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800643 } else {
644 return 0f;
645 }
646 }
647
648 @Override
649 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
650 return new SpringForce()
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400651 .setDampingRatio(DEFAULT_BOUNCINESS)
652 .setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800653 }
654
655 @Override
656 void onChildAdded(View child, int index) {
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400657 // Don't animate additions within the dismiss target.
658 if (mWithinDismissTarget) {
659 return;
660 }
661
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800662 if (mLayout.getChildCount() == 1) {
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400663 // If this is the first child added, position the stack in its starting position.
664 moveStackToStartPosition();
665 } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500666 // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
667 // to the back of the stack, it'll be largely invisible so don't bother animating it in.
Joshua Tsuji14e68552019-06-06 17:17:08 -0400668 animateInBubble(child, index);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800669 }
670 }
671
672 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500673 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Mady Mellor88552b82019-08-05 22:38:59 +0000674 // Animate the removing view in the opposite direction of the stack.
675 final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujic1108432019-02-22 16:10:12 -0500676 animationForChild(child)
Mady Mellor88552b82019-08-05 22:38:59 +0000677 .alpha(0f, finishRemoval /* after */)
678 .scaleX(ANIMATE_IN_STARTING_SCALE)
679 .scaleY(ANIMATE_IN_STARTING_SCALE)
680 .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR))
Joshua Tsujic1108432019-02-22 16:10:12 -0500681 .start();
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500682
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400683 // If there are other bubbles, pull them into the correct position.
Joshua Tsujic1108432019-02-22 16:10:12 -0500684 if (mLayout.getChildCount() > 0) {
685 animationForChildAtIndex(0).translationX(mStackPosition.x).start();
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400686 } else {
687 // If there's no other bubbles, and we were in the dismiss target, reset the flag.
688 mWithinDismissTarget = false;
Joshua Tsujic1108432019-02-22 16:10:12 -0500689 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800690 }
691
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400692 @Override
Joshua Tsujid2a7c2152019-07-15 15:45:20 -0400693 void onChildReordered(View child, int oldIndex, int newIndex) {
694 if (isStackPositionSet()) {
695 setStackPosition(mStackPosition);
696 }
697 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400698
699 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400700 void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
701 Resources res = layout.getResources();
702 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
Lyn Hana511e1fb2019-06-17 12:35:08 -0700703 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Lyn Han1b4f25e2019-06-11 13:56:34 -0700704 mBubbleIconBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_icon_bitmap_size);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400705 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400706 mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
707 mStackStartingVerticalOffset =
708 res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
709 mStatusBarHeight =
710 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
711 }
712
Mady Mellor818eef02019-08-16 16:12:29 -0700713 /**
714 * Update effective screen width based on current orientation.
715 * @param orientation Landscape or portrait.
716 */
717 public void updateOrientation(int orientation) {
718 if (mLayout != null) {
719 Resources res = mLayout.getContext().getResources();
720 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
721 mStatusBarHeight = res.getDimensionPixelSize(
722 com.android.internal.R.dimen.status_bar_height);
723 }
724 }
725
726
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800727 /** Moves the stack, without any animation, to the starting position. */
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400728 private void moveStackToStartPosition() {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500729 // Post to ensure that the layout's width and height have been calculated.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500730 mLayout.setVisibility(View.INVISIBLE);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500731 mLayout.post(() -> {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400732 setStackPosition(mRestingStackPosition == null
733 ? getDefaultStartPosition()
734 : mRestingStackPosition);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400735 mStackMovedToStartPosition = true;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500736 mLayout.setVisibility(View.VISIBLE);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400737
738 // Animate in the top bubble now that we're visible.
739 if (mLayout.getChildCount() > 0) {
Joshua Tsuji14e68552019-06-06 17:17:08 -0400740 animateInBubble(mLayout.getChildAt(0), 0 /* index */);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400741 }
Joshua Tsujif44347f2019-02-12 14:28:06 -0500742 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800743 }
744
745 /**
746 * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
747 * bubbles to animate 'following' to the new location.
748 */
749 private void moveFirstBubbleWithStackFollowing(
750 DynamicAnimation.ViewProperty property, float value) {
751
752 // Update the canonical stack position.
753 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
754 mStackPosition.x = value;
755 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
756 mStackPosition.y = value;
757 }
758
759 if (mLayout.getChildCount() > 0) {
760 property.setValue(mLayout.getChildAt(0), value);
Joshua Tsujic1108432019-02-22 16:10:12 -0500761 if (mLayout.getChildCount() > 1) {
762 animationForChildAtIndex(1)
763 .property(property, value + getOffsetForChainedPropertyAnimation(property))
764 .start();
765 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800766 }
767 }
768
769 /** Moves the stack to a position instantly, with no animation. */
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500770 private void setStackPosition(PointF pos) {
771 Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
772 mStackPosition.set(pos.x, pos.y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800773
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400774 // If we're not the active controller, we don't want to physically move the bubble views.
775 if (isActiveController()) {
Joshua Tsujif75ca272019-08-02 10:18:51 -0400776 // Cancel animations that could be moving the views.
777 mLayout.cancelAllAnimationsOfProperties(
778 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400779 cancelStackPositionAnimations();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800780
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400781 // Since we're not using the chained animations, apply the offsets manually.
782 final float xOffset = getOffsetForChainedPropertyAnimation(
783 DynamicAnimation.TRANSLATION_X);
784 final float yOffset = getOffsetForChainedPropertyAnimation(
785 DynamicAnimation.TRANSLATION_Y);
786 for (int i = 0; i < mLayout.getChildCount(); i++) {
787 mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
788 mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
789 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800790 }
791 }
792
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500793 /** Returns the default stack position, which is on the top right. */
794 private PointF getDefaultStartPosition() {
795 return new PointF(
796 getAllowableStackPositionRegion().right,
797 getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
798 }
799
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400800 private boolean isStackPositionSet() {
801 return mStackMovedToStartPosition;
802 }
803
Joshua Tsujif44347f2019-02-12 14:28:06 -0500804 /** Animates in the given bubble. */
Joshua Tsuji14e68552019-06-06 17:17:08 -0400805 private void animateInBubble(View child, int index) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400806 if (!isActiveController()) {
807 return;
808 }
809
Joshua Tsuji14e68552019-06-06 17:17:08 -0400810 final float xOffset =
811 getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500812
Joshua Tsuji14e68552019-06-06 17:17:08 -0400813 // Position the new bubble in the correct position, scaled down completely.
814 child.setTranslationX(mStackPosition.x + xOffset * index);
815 child.setTranslationY(mStackPosition.y);
816 child.setScaleX(0f);
817 child.setScaleY(0f);
818
819 // Push the subsequent views out of the way, if there are subsequent views.
820 if (index + 1 < mLayout.getChildCount()) {
821 animationForChildAtIndex(index + 1)
822 .translationX(mStackPosition.x + xOffset * (index + 1))
823 .withStiffness(SpringForce.STIFFNESS_LOW)
824 .start();
825 }
826
827 // Scale in the new bubble, slightly delayed.
Joshua Tsujic1108432019-02-22 16:10:12 -0500828 animationForChild(child)
Joshua Tsuji14e68552019-06-06 17:17:08 -0400829 .scaleX(1f)
830 .scaleY(1f)
831 .withStiffness(ANIMATE_IN_STIFFNESS)
832 .withStartDelay(mLayout.getChildCount() > 1 ? ANIMATE_IN_START_DELAY : 0)
Joshua Tsujic1108432019-02-22 16:10:12 -0500833 .start();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500834 }
835
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800836 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800837 * Cancels any outstanding first bubble property animations that are running. This does not
838 * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
839 * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
840 * {@link #flingThenSpringFirstBubbleWithStackFollowing}.
841 */
842 private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
843 if (mStackPositionAnimations.containsKey(property)) {
844 mStackPositionAnimations.get(property).cancel();
845 }
846 }
847
848 /**
849 * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
850 * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
851 * property directly to move the first bubble and cause the stack to 'follow' to the new
852 * location.
853 *
854 * This could also be achieved by simply animating the first bubble view and adding an update
855 * listener to dispatch movement to the rest of the stack. However, this would require
856 * duplication of logic in that update handler - it's simpler to keep all logic contained in the
857 * {@link #moveFirstBubbleWithStackFollowing} method.
858 */
859 private class StackPositionProperty
860 extends FloatPropertyCompat<StackAnimationController> {
861 private final DynamicAnimation.ViewProperty mProperty;
862
863 private StackPositionProperty(DynamicAnimation.ViewProperty property) {
864 super(property.toString());
865 mProperty = property;
866 }
867
868 @Override
869 public float getValue(StackAnimationController controller) {
Joshua Tsujid9422832019-03-05 13:32:37 -0500870 return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800871 }
872
873 @Override
874 public void setValue(StackAnimationController controller, float value) {
875 moveFirstBubbleWithStackFollowing(mProperty, value);
876 }
877 }
878}
879