blob: 00de8b4a51b827aa7e765c589d6891a0cdf1a83c [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;
Joshua Tsuji7155bf12020-02-13 16:14:29 -050021import android.graphics.Rect;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080022import android.graphics.RectF;
23import android.util.Log;
24import android.view.View;
25import android.view.WindowInsets;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080026
Joshua Tsuji20103542020-02-18 14:06:28 -050027import androidx.annotation.NonNull;
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040028import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080029import androidx.dynamicanimation.animation.DynamicAnimation;
30import androidx.dynamicanimation.animation.FlingAnimation;
31import androidx.dynamicanimation.animation.FloatPropertyCompat;
32import androidx.dynamicanimation.animation.SpringAnimation;
33import androidx.dynamicanimation.animation.SpringForce;
34
35import com.android.systemui.R;
Joshua Tsuji7155bf12020-02-13 16:14:29 -050036import com.android.systemui.util.FloatingContentCoordinator;
37import com.android.systemui.util.animation.PhysicsAnimator;
Joshua Tsuji20103542020-02-18 14:06:28 -050038import com.android.systemui.util.magnetictarget.MagnetizedObject;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080039
40import com.google.android.collect.Sets;
41
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040042import java.io.FileDescriptor;
43import java.io.PrintWriter;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080044import java.util.HashMap;
45import java.util.Set;
Joshua Tsuji259c66b82020-03-16 14:40:41 -040046import java.util.function.IntSupplier;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080047
48/**
49 * Animation controller for bubbles when they're in their stacked state. Stacked bubbles sit atop
50 * each other with a slight offset to the left or right (depending on which side of the screen they
51 * are on). Bubbles 'follow' each other when dragged, and can be flung to the left or right sides of
52 * the screen.
53 */
54public class StackAnimationController extends
55 PhysicsAnimationLayout.PhysicsAnimationController {
56
57 private static final String TAG = "Bubbs.StackCtrl";
58
59 /** Scale factor to use initially for new bubbles being animated in. */
60 private static final float ANIMATE_IN_STARTING_SCALE = 1.15f;
61
Joshua Tsujia08b6d32019-01-29 16:15:52 -050062 /** Translation factor (multiplied by stack offset) to use for bubbles being animated in/out. */
63 private static final int ANIMATE_TRANSLATION_FACTOR = 4;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080064
Joshua Tsuji14e68552019-06-06 17:17:08 -040065 /** Values to use for animating bubbles in. */
66 private static final float ANIMATE_IN_STIFFNESS = 1000f;
67 private static final int ANIMATE_IN_START_DELAY = 25;
68
Joshua Tsujib1a796b2019-01-16 15:43:12 -080069 /**
70 * Values to use for the default {@link SpringForce} provided to the physics animation layout.
71 */
Joshua Tsujiff6b0f22020-03-09 14:55:19 -040072 public static final int DEFAULT_STIFFNESS = 12000;
73 public static final float IME_ANIMATION_STIFFNESS = SpringForce.STIFFNESS_LOW;
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040074 private static final int FLING_FOLLOW_STIFFNESS = 20000;
Joshua Tsujiff6b0f22020-03-09 14:55:19 -040075 public static final float DEFAULT_BOUNCINESS = 0.9f;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080076
77 /**
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050078 * Friction applied to fling animations. Since the stack must land on one of the sides of the
79 * screen, we want less friction horizontally so that the stack has a better chance of making it
80 * to the side without needing a spring.
81 */
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040082 private static final float FLING_FRICTION_X = 2.2f;
83 private static final float FLING_FRICTION_Y = 2.2f;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050084
85 /**
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040086 * Values to use for the stack spring animation used to spring the stack to its final position
87 * after a fling.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050088 */
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -040089 private static final int SPRING_AFTER_FLING_STIFFNESS = 750;
90 private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050091
Joshua Tsujid1b9cb72020-03-16 15:52:47 -040092 /** Sentinel value for unset position value. */
93 private static final float UNSET = -Float.MIN_VALUE;
94
Joshua Tsujie13b4fc2019-02-28 18:39:57 -050095 /**
96 * Minimum fling velocity required to trigger moving the stack from one side of the screen to
97 * the other.
98 */
99 private static final float ESCAPE_VELOCITY = 750f;
100
Joshua Tsuji20103542020-02-18 14:06:28 -0500101 /** Velocity required to dismiss the stack without dragging it into the dismiss target. */
102 private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f;
103
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500104 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800105 * The canonical position of the stack. This is typically the position of the first bubble, but
106 * we need to keep track of it separately from the first bubble's translation in case there are
107 * no bubbles, or the first bubble was just added and being animated to its new position.
108 */
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400109 private PointF mStackPosition = new PointF(-1, -1);
110
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500111 /**
Joshua Tsuji20103542020-02-18 14:06:28 -0500112 * MagnetizedObject instance for the stack, which is used by the touch handler for the magnetic
113 * dismiss target.
114 */
115 private MagnetizedObject<StackAnimationController> mMagnetizedStack;
116
117 /**
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500118 * The area that Bubbles will occupy after all animations end. This is used to move other
119 * floating content out of the way proactively.
120 */
121 private Rect mAnimatingToBounds = new Rect();
122
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400123 /** Whether or not the stack's start position has been set. */
124 private boolean mStackMovedToStartPosition = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800125
Joshua Tsujife1ba1e2020-03-09 13:29:29 -0400126 /**
127 * The stack's most recent position along the edge of the screen. This is saved when the last
128 * bubble is removed, so that the stack can be restored in its previous position.
129 */
130 private PointF mRestingStackPosition;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500131
Joshua Tsujia19515f2019-02-13 18:02:29 -0500132 /** The height of the most recently visible IME. */
133 private float mImeHeight = 0f;
134
135 /**
136 * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the
137 * IME is not visible or the user moved the stack since the IME became visible.
138 */
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400139 private float mPreImeY = UNSET;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500140
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800141 /**
142 * Animations on the stack position itself, which would have been started in
143 * {@link #flingThenSpringFirstBubbleWithStackFollowing}. These animations dispatch to
144 * {@link #moveFirstBubbleWithStackFollowing} to move the entire stack (with 'following' effect)
145 * to a legal position on the side of the screen.
146 */
147 private HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mStackPositionAnimations =
148 new HashMap<>();
149
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400150 /**
151 * Whether the current motion of the stack is due to a fling animation (vs. being dragged
152 * manually).
153 */
154 private boolean mIsMovingFromFlinging = false;
155
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400156 /**
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400157 * Whether the first bubble is springing towards the touch point, rather than using the default
158 * behavior of moving directly to the touch point with the rest of the stack following it.
159 *
160 * This happens when the user's finger exits the dismiss area while the stack is magnetized to
161 * the center. Since the touch point differs from the stack location, we need to animate the
162 * stack back to the touch point to avoid a jarring instant location change from the center of
163 * the target to the touch point just outside the target bounds.
164 *
165 * This is reset once the spring animations end, since that means the first bubble has
166 * successfully 'caught up' to the touch.
167 */
168 private boolean mFirstBubbleSpringingToTouch = false;
169
Joshua Tsuji20103542020-02-18 14:06:28 -0500170 /**
171 * Whether to spring the stack to the next touch event coordinates. This is used to animate the
172 * stack (including the first bubble) out of the magnetic dismiss target to the touch location.
173 * Once it 'catches up' and the animation ends, we'll revert to moving the first bubble directly
174 * and only animating the following bubbles.
175 */
176 private boolean mSpringToTouchOnNextMotionEvent = false;
177
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800178 /** Horizontal offset of bubbles in the stack. */
179 private float mStackOffset;
Lyn Hana511e1fb2019-06-17 12:35:08 -0700180 /** Diameter of the bubble icon. */
Lyn Hancd4f87e2020-02-19 20:33:45 -0800181 private int mBubbleBitmapSize;
Lyn Hana511e1fb2019-06-17 12:35:08 -0700182 /** Width of the bubble (icon and padding). */
183 private int mBubbleSize;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -0400184 /**
185 * The amount of space to add between the bubbles and certain UI elements, such as the top of
186 * the screen or the IME. This does not apply to the left/right sides of the screen since the
187 * stack goes offscreen intentionally.
188 */
Lyn Han4a8efe32019-05-30 09:43:27 -0700189 private int mBubblePaddingTop;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800190 /** How far offscreen the stack rests. */
191 private int mBubbleOffscreen;
192 /** How far down the screen the stack starts, when there is no pre-existing location. */
193 private int mStackStartingVerticalOffset;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500194 /** Height of the status bar. */
195 private float mStatusBarHeight;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800196
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500197 /** FloatingContentCoordinator instance for resolving floating content conflicts. */
198 private FloatingContentCoordinator mFloatingContentCoordinator;
199
200 /**
201 * FloatingContent instance that returns the stack's location on the screen, and moves it when
202 * requested.
203 */
204 private final FloatingContentCoordinator.FloatingContent mStackFloatingContent =
205 new FloatingContentCoordinator.FloatingContent() {
206
207 private final Rect mFloatingBoundsOnScreen = new Rect();
208
209 @Override
210 public void moveToBounds(@NonNull Rect bounds) {
211 springStack(bounds.left, bounds.top, SpringForce.STIFFNESS_LOW);
212 }
213
214 @NonNull
215 @Override
216 public Rect getAllowedFloatingBoundsRegion() {
217 final Rect floatingBounds = getFloatingBoundsOnScreen();
218 final Rect allowableStackArea = new Rect();
219 getAllowableStackPositionRegion().roundOut(allowableStackArea);
220 allowableStackArea.right += floatingBounds.width();
221 allowableStackArea.bottom += floatingBounds.height();
222 return allowableStackArea;
223 }
224
225 @NonNull
226 @Override
227 public Rect getFloatingBoundsOnScreen() {
228 if (!mAnimatingToBounds.isEmpty()) {
229 return mAnimatingToBounds;
230 }
231
232 if (mLayout.getChildCount() > 0) {
233 // Calculate the bounds using stack position + bubble size so that we don't need to
234 // wait for the bubble views to lay out.
235 mFloatingBoundsOnScreen.set(
236 (int) mStackPosition.x,
237 (int) mStackPosition.y,
238 (int) mStackPosition.x + mBubbleSize,
239 (int) mStackPosition.y + mBubbleSize + mBubblePaddingTop);
240 } else {
241 mFloatingBoundsOnScreen.setEmpty();
242 }
243
244 return mFloatingBoundsOnScreen;
245 }
246 };
247
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400248 /** Returns the number of 'real' bubbles (excluding the overflow bubble). */
249 private IntSupplier mBubbleCountSupplier;
250
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500251 public StackAnimationController(
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400252 FloatingContentCoordinator floatingContentCoordinator,
253 IntSupplier bubbleCountSupplier) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500254 mFloatingContentCoordinator = floatingContentCoordinator;
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400255 mBubbleCountSupplier = bubbleCountSupplier;
256
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500257 }
258
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800259 /**
260 * Instantly move the first bubble to the given point, and animate the rest of the stack behind
261 * it with the 'following' effect.
262 */
263 public void moveFirstBubbleWithStackFollowing(float x, float y) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500264 // If we're moving the bubble around, we're not animating to any bounds.
265 mAnimatingToBounds.setEmpty();
266
Joshua Tsujia19515f2019-02-13 18:02:29 -0500267 // If we manually move the bubbles with the IME open, clear the return point since we don't
268 // want the stack to snap away from the new position.
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400269 mPreImeY = UNSET;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500270
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800271 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X, x);
272 moveFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y, y);
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400273
274 // This method is called when the stack is being dragged manually, so we're clearly no
275 // longer flinging.
276 mIsMovingFromFlinging = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800277 }
278
279 /**
280 * The position of the stack - typically the position of the first bubble; if no bubbles have
281 * been added yet, it will be where the first bubble will go when added.
282 */
283 public PointF getStackPosition() {
284 return mStackPosition;
285 }
286
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400287 /** Whether the stack is on the left side of the screen. */
288 public boolean isStackOnLeftSide() {
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400289 if (mLayout == null || !isStackPositionSet()) {
Joshua Tsuji2ed260e2020-03-26 14:26:01 -0400290 return true; // Default to left, which is where it starts by default.
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400291 }
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400292
Lyn Hancd4f87e2020-02-19 20:33:45 -0800293 float stackCenter = mStackPosition.x + mBubbleBitmapSize / 2;
Lyn Hane68d0912019-05-02 18:28:01 -0700294 float screenCenter = mLayout.getWidth() / 2;
295 return stackCenter < screenCenter;
296 }
297
298 /**
299 * Fling stack to given corner, within allowable screen bounds.
300 * Note that we need new SpringForce instances per animation despite identical configs because
301 * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
302 */
Joshua Tsuji20103542020-02-18 14:06:28 -0500303 public void springStack(
304 float destinationX, float destinationY, float stiffness) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500305 notifyFloatingCoordinatorStackAnimatingTo(destinationX, destinationY);
306
Lyn Hane68d0912019-05-02 18:28:01 -0700307 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
Joshua Tsujidebd8312019-06-06 17:17:08 -0400308 new SpringForce()
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500309 .setStiffness(stiffness)
Lyn Hane68d0912019-05-02 18:28:01 -0700310 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujidebd8312019-06-06 17:17:08 -0400311 0 /* startXVelocity */,
312 destinationX);
Lyn Hane68d0912019-05-02 18:28:01 -0700313
314 springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_Y,
Joshua Tsujidebd8312019-06-06 17:17:08 -0400315 new SpringForce()
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500316 .setStiffness(stiffness)
Lyn Hane68d0912019-05-02 18:28:01 -0700317 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujidebd8312019-06-06 17:17:08 -0400318 0 /* startYVelocity */,
319 destinationY);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400320 }
321
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800322 /**
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500323 * Springs the stack to the specified x/y coordinates, with the stiffness used for springs after
324 * flings.
325 */
326 public void springStackAfterFling(float destinationX, float destinationY) {
327 springStack(destinationX, destinationY, SPRING_AFTER_FLING_STIFFNESS);
328 }
329
330 /**
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500331 * Flings the stack starting with the given velocities, springing it to the nearest edge
332 * afterward.
Joshua Tsuji6549e702019-05-02 13:13:16 -0400333 *
334 * @return The X value that the stack will end up at after the fling/spring.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500335 */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400336 public float flingStackThenSpringToEdge(float x, float velX, float velY) {
Lyn Hancd4f87e2020-02-19 20:33:45 -0800337 final boolean stackOnLeftSide = x - mBubbleBitmapSize / 2 < mLayout.getWidth() / 2;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500338
339 final boolean stackShouldFlingLeft = stackOnLeftSide
340 ? velX < ESCAPE_VELOCITY
341 : velX < -ESCAPE_VELOCITY;
342
343 final RectF stackBounds = getAllowableStackPositionRegion();
344
345 // Target X translation (either the left or right side of the screen).
346 final float destinationRelativeX = stackShouldFlingLeft
347 ? stackBounds.left : stackBounds.right;
348
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400349 // If all bubbles were removed during a drag event, just return the X we would have animated
350 // to if there were still bubbles.
351 if (mLayout == null || mLayout.getChildCount() == 0) {
352 return destinationRelativeX;
353 }
354
Joshua Tsujicd169332019-03-06 23:56:52 -0500355 // Minimum velocity required for the stack to make it to the targeted side of the screen,
356 // taking friction into account (4.2f is the number that friction scalars are multiplied by
357 // in DynamicAnimation.DragForce). This is an estimate - it could possibly be slightly off,
358 // but the SpringAnimation at the end will ensure that it reaches the destination X
359 // regardless.
360 final float minimumVelocityToReachEdge =
361 (destinationRelativeX - x) * (FLING_FRICTION_X * 4.2f);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500362
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500363 final float estimatedY = PhysicsAnimator.estimateFlingEndValue(
364 mStackPosition.y, velY,
365 new PhysicsAnimator.FlingConfig(
366 FLING_FRICTION_Y, stackBounds.top, stackBounds.bottom));
367
368 notifyFloatingCoordinatorStackAnimatingTo(destinationRelativeX, estimatedY);
369
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500370 // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
371 // that it'll make it all the way to the side of the screen.
372 final float startXVelocity = stackShouldFlingLeft
Joshua Tsujicd169332019-03-06 23:56:52 -0500373 ? Math.min(minimumVelocityToReachEdge, velX)
374 : Math.max(minimumVelocityToReachEdge, velX);
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500375
376 flingThenSpringFirstBubbleWithStackFollowing(
377 DynamicAnimation.TRANSLATION_X,
378 startXVelocity,
379 FLING_FRICTION_X,
380 new SpringForce()
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400381 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
382 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500383 destinationRelativeX);
384
385 flingThenSpringFirstBubbleWithStackFollowing(
386 DynamicAnimation.TRANSLATION_Y,
387 velY,
388 FLING_FRICTION_Y,
389 new SpringForce()
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400390 .setStiffness(SPRING_AFTER_FLING_STIFFNESS)
391 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500392 /* destination */ null);
393
Joshua Tsuji4bb3e7e2019-05-29 16:24:43 -0400394 // If we're flinging now, there's no more touch event to catch up to.
395 mFirstBubbleSpringingToTouch = false;
Joshua Tsuji58f7a5a2019-04-04 17:50:02 -0400396 mIsMovingFromFlinging = true;
Joshua Tsuji6549e702019-05-02 13:13:16 -0400397 return destinationRelativeX;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500398 }
399
400 /**
Joshua Tsuji3829caa2019-03-05 18:09:13 -0500401 * Where the stack would be if it were snapped to the nearest horizontal edge (left or right).
402 */
403 public PointF getStackPositionAlongNearestHorizontalEdge() {
404 final PointF stackPos = getStackPosition();
405 final boolean onLeft = mLayout.isFirstChildXLeftOfCenter(stackPos.x);
406 final RectF bounds = getAllowableStackPositionRegion();
407
408 stackPos.x = onLeft ? bounds.left : bounds.right;
409 return stackPos;
410 }
411
412 /**
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400413 * Moves the stack in response to rotation. We keep it in the most similar position by keeping
414 * it on the same side, and positioning it the same percentage of the way down the screen
415 * (taking status bar/nav bar into account by using the allowable region's height).
416 */
417 public void moveStackToSimilarPositionAfterRotation(boolean wasOnLeft, float verticalPercent) {
418 final RectF allowablePos = getAllowableStackPositionRegion();
419 final float allowableRegionHeight = allowablePos.bottom - allowablePos.top;
420
421 final float x = wasOnLeft ? allowablePos.left : allowablePos.right;
422 final float y = (allowableRegionHeight * verticalPercent) + allowablePos.top;
423
424 setStackPosition(new PointF(x, y));
425 }
426
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400427 /** Description of current animation controller state. */
428 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
429 pw.println("StackAnimationController state:");
430 pw.print(" isActive: "); pw.println(isActiveController());
431 pw.print(" restingStackPos: ");
432 pw.println(mRestingStackPosition != null ? mRestingStackPosition.toString() : "null");
433 pw.print(" currentStackPos: "); pw.println(mStackPosition.toString());
434 pw.print(" isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging);
Joshua Tsuji20103542020-02-18 14:06:28 -0500435 pw.print(" withinDismiss: "); pw.println(isStackStuckToTarget());
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400436 pw.print(" firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch);
437 }
438
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400439 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800440 * Flings the first bubble along the given property's axis, using the provided configuration
441 * values. When the animation ends - either by hitting the min/max, or by friction sufficiently
442 * reducing momentum - a SpringAnimation takes over to snap the bubble to the given final
443 * position.
444 */
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500445 protected void flingThenSpringFirstBubbleWithStackFollowing(
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800446 DynamicAnimation.ViewProperty property,
447 float vel,
448 float friction,
449 SpringForce spring,
450 Float finalPosition) {
451 Log.d(TAG, String.format("Flinging %s.",
Joshua Tsujidebd8312019-06-06 17:17:08 -0400452 PhysicsAnimationLayout.getReadablePropertyName(property)));
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800453
454 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
455 final float currentValue = firstBubbleProperty.getValue(this);
456 final RectF bounds = getAllowableStackPositionRegion();
457 final float min =
458 property.equals(DynamicAnimation.TRANSLATION_X)
459 ? bounds.left
460 : bounds.top;
461 final float max =
462 property.equals(DynamicAnimation.TRANSLATION_X)
463 ? bounds.right
464 : bounds.bottom;
465
466 FlingAnimation flingAnimation = new FlingAnimation(this, firstBubbleProperty);
467 flingAnimation.setFriction(friction)
468 .setStartVelocity(vel)
469
470 // If the bubble's property value starts beyond the desired min/max, use that value
471 // instead so that the animation won't immediately end. If, for example, the user
472 // drags the bubbles into the navigation bar, but then flings them upward, we want
473 // the fling to occur despite temporarily having a value outside of the min/max. If
474 // the bubbles are out of bounds and flung even farther out of bounds, the fling
475 // animation will halt immediately and the SpringAnimation will take over, springing
476 // it in reverse to the (legal) final position.
477 .setMinValue(Math.min(currentValue, min))
478 .setMaxValue(Math.max(currentValue, max))
479
480 .addEndListener((animation, canceled, endValue, endVelocity) -> {
481 if (!canceled) {
Joshua Tsujib35f5912019-07-24 16:15:21 -0400482 mRestingStackPosition.set(mStackPosition);
483
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800484 springFirstBubbleWithStackFollowing(property, spring, endVelocity,
485 finalPosition != null
486 ? finalPosition
487 : Math.max(min, Math.min(max, endValue)));
488 }
489 });
490
491 cancelStackPositionAnimation(property);
492 mStackPositionAnimations.put(property, flingAnimation);
493 flingAnimation.start();
494 }
495
496 /**
497 * Cancel any stack position animations that were started by calling
498 * @link #flingThenSpringFirstBubbleWithStackFollowing}, and remove any corresponding end
499 * listeners.
500 */
501 public void cancelStackPositionAnimations() {
502 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_X);
503 cancelStackPositionAnimation(DynamicAnimation.TRANSLATION_Y);
504
Joshua Tsujidebd8312019-06-06 17:17:08 -0400505 removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
506 removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800507 }
508
Joshua Tsuji4b395912019-04-19 17:18:40 -0400509 /** Save the current IME height so that we know where the stack bounds should be. */
510 public void setImeHeight(int imeHeight) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500511 mImeHeight = imeHeight;
Joshua Tsuji4b395912019-04-19 17:18:40 -0400512 }
Joshua Tsujia19515f2019-02-13 18:02:29 -0500513
Joshua Tsuji4b395912019-04-19 17:18:40 -0400514 /**
515 * Animates the stack either away from the newly visible IME, or back to its original position
516 * due to the IME going away.
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400517 *
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400518 * @return The destination Y value of the stack due to the IME movement (or the current position
519 * of the stack if it's not moving).
Joshua Tsuji4b395912019-04-19 17:18:40 -0400520 */
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400521 public float animateForImeVisibility(boolean imeVisible) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500522 final float maxBubbleY = getAllowableStackPositionRegion().bottom;
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400523 float destinationY = UNSET;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500524
Joshua Tsuji4b395912019-04-19 17:18:40 -0400525 if (imeVisible) {
Lyn Hane68d0912019-05-02 18:28:01 -0700526 // Stack is lower than it should be and overlaps the now-visible IME.
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400527 if (mStackPosition.y > maxBubbleY && mPreImeY == UNSET) {
Joshua Tsuji4b395912019-04-19 17:18:40 -0400528 mPreImeY = mStackPosition.y;
529 destinationY = maxBubbleY;
530 }
531 } else {
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400532 if (mPreImeY != UNSET) {
Joshua Tsuji4b395912019-04-19 17:18:40 -0400533 destinationY = mPreImeY;
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400534 mPreImeY = UNSET;
Joshua Tsuji4b395912019-04-19 17:18:40 -0400535 }
536 }
537
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400538 if (destinationY != UNSET) {
Joshua Tsujia19515f2019-02-13 18:02:29 -0500539 springFirstBubbleWithStackFollowing(
540 DynamicAnimation.TRANSLATION_Y,
541 getSpringForce(DynamicAnimation.TRANSLATION_Y, /* view */ null)
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400542 .setStiffness(IME_ANIMATION_STIFFNESS),
Joshua Tsujia19515f2019-02-13 18:02:29 -0500543 /* startVel */ 0f,
Joshua Tsuji4b395912019-04-19 17:18:40 -0400544 destinationY);
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500545
546 notifyFloatingCoordinatorStackAnimatingTo(mStackPosition.x, destinationY);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500547 }
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400548
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400549 return destinationY != UNSET ? destinationY : mStackPosition.y;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500550 }
551
552 /**
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500553 * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so
554 * we return these bounds from
555 * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
556 */
557 private void notifyFloatingCoordinatorStackAnimatingTo(float x, float y) {
558 final Rect floatingBounds = mStackFloatingContent.getFloatingBoundsOnScreen();
559 floatingBounds.offsetTo((int) x, (int) y);
560 mAnimatingToBounds = floatingBounds;
561 mFloatingContentCoordinator.onContentMoved(mStackFloatingContent);
562 }
563
564 /**
565 * Returns the region that the stack position must stay within. This goes slightly off the left
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800566 * and right sides of the screen, below the status bar/cutout and above the navigation bar.
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500567 * While the stack position is not allowed to rest outside of these bounds, it can temporarily
568 * be animated or dragged beyond them.
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800569 */
570 public RectF getAllowableStackPositionRegion() {
571 final WindowInsets insets = mLayout.getRootWindowInsets();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500572 final RectF allowableRegion = new RectF();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800573 if (insets != null) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500574 allowableRegion.left =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800575 -mBubbleOffscreen
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800576 + Math.max(
577 insets.getSystemWindowInsetLeft(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500578 insets.getDisplayCutout() != null
579 ? insets.getDisplayCutout().getSafeInsetLeft()
580 : 0);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500581 allowableRegion.right =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800582 mLayout.getWidth()
Lyn Hana511e1fb2019-06-17 12:35:08 -0700583 - mBubbleSize
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800584 + mBubbleOffscreen
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800585 - Math.max(
586 insets.getSystemWindowInsetRight(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500587 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500588 ? insets.getDisplayCutout().getSafeInsetRight()
589 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800590
Joshua Tsujif44347f2019-02-12 14:28:06 -0500591 allowableRegion.top =
Lyn Han4a8efe32019-05-30 09:43:27 -0700592 mBubblePaddingTop
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800593 + Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -0500594 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500595 insets.getDisplayCutout() != null
Joshua Tsujif44347f2019-02-12 14:28:06 -0500596 ? insets.getDisplayCutout().getSafeInsetTop()
597 : 0);
598 allowableRegion.bottom =
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800599 mLayout.getHeight()
Mady Mellor818eef02019-08-16 16:12:29 -0700600 - mBubbleSize
Lyn Han4a8efe32019-05-30 09:43:27 -0700601 - mBubblePaddingTop
Joshua Tsujid1b9cb72020-03-16 15:52:47 -0400602 - (mImeHeight != UNSET ? mImeHeight + mBubblePaddingTop : 0f)
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800603 - Math.max(
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400604 insets.getStableInsetBottom(),
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500605 insets.getDisplayCutout() != null
606 ? insets.getDisplayCutout().getSafeInsetBottom()
607 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800608 }
609
Joshua Tsujif44347f2019-02-12 14:28:06 -0500610 return allowableRegion;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800611 }
612
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400613 /** Moves the stack in response to a touch event. */
614 public void moveStackFromTouch(float x, float y) {
Joshua Tsuji20103542020-02-18 14:06:28 -0500615 // Begin the spring-to-touch catch up animation if needed.
616 if (mSpringToTouchOnNextMotionEvent) {
617 springStack(x, y, DEFAULT_STIFFNESS);
618 mSpringToTouchOnNextMotionEvent = false;
619 mFirstBubbleSpringingToTouch = true;
620 } else if (mFirstBubbleSpringingToTouch) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400621 final SpringAnimation springToTouchX =
Joshua Tsuji20103542020-02-18 14:06:28 -0500622 (SpringAnimation) mStackPositionAnimations.get(
623 DynamicAnimation.TRANSLATION_X);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400624 final SpringAnimation springToTouchY =
Joshua Tsuji20103542020-02-18 14:06:28 -0500625 (SpringAnimation) mStackPositionAnimations.get(
626 DynamicAnimation.TRANSLATION_Y);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400627
628 // If either animation is still running, we haven't caught up. Update the animations.
629 if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
630 springToTouchX.animateToFinalPosition(x);
631 springToTouchY.animateToFinalPosition(y);
632 } else {
633 // If the animations have finished, the stack is now at the touch point. We can
634 // resume moving the bubble directly.
635 mFirstBubbleSpringingToTouch = false;
636 }
637 }
638
Joshua Tsuji20103542020-02-18 14:06:28 -0500639 if (!mFirstBubbleSpringingToTouch && !isStackStuckToTarget()) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400640 moveFirstBubbleWithStackFollowing(x, y);
641 }
642 }
643
Joshua Tsuji20103542020-02-18 14:06:28 -0500644 /** Notify the controller that the stack has been unstuck from the dismiss target. */
645 public void onUnstuckFromTarget() {
646 mSpringToTouchOnNextMotionEvent = true;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400647 }
648
649 /**
650 * 'Implode' the stack by shrinking the bubbles via chained animations and fading them out.
651 */
652 public void implodeStack(Runnable after) {
653 // Pop and fade the bubbles sequentially.
654 animationForChildAtIndex(0)
655 .scaleX(0.5f)
656 .scaleY(0.5f)
657 .alpha(0f)
658 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
659 .withStiffness(SpringForce.STIFFNESS_HIGH)
Joshua Tsuji20103542020-02-18 14:06:28 -0500660 .start(after);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400661 }
662
663 /**
664 * Springs the first bubble to the given final position, with the rest of the stack 'following'.
665 */
666 protected void springFirstBubbleWithStackFollowing(
667 DynamicAnimation.ViewProperty property, SpringForce spring,
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400668 float vel, float finalPosition, @Nullable Runnable... after) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400669
670 if (mLayout.getChildCount() == 0) {
671 return;
672 }
673
674 Log.d(TAG, String.format("Springing %s to final position %f.",
675 PhysicsAnimationLayout.getReadablePropertyName(property),
676 finalPosition));
677
678 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
679 SpringAnimation springAnimation =
680 new SpringAnimation(this, firstBubbleProperty)
681 .setSpring(spring)
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400682 .addEndListener((dynamicAnimation, b, v, v1) -> {
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400683 mRestingStackPosition.set(mStackPosition);
684
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400685 if (after != null) {
686 for (Runnable callback : after) {
687 callback.run();
688 }
689 }
690 })
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400691 .setStartVelocity(vel);
692
693 cancelStackPositionAnimation(property);
694 mStackPositionAnimations.put(property, springAnimation);
695 springAnimation.animateToFinalPosition(finalPosition);
696 }
697
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800698 @Override
699 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
700 return Sets.newHashSet(
701 DynamicAnimation.TRANSLATION_X, // For positioning.
702 DynamicAnimation.TRANSLATION_Y,
703 DynamicAnimation.ALPHA, // For fading in new bubbles.
704 DynamicAnimation.SCALE_X, // For 'popping in' new bubbles.
705 DynamicAnimation.SCALE_Y);
706 }
707
708 @Override
709 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
710 if (property.equals(DynamicAnimation.TRANSLATION_X)
711 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400712 return index + 1;
Joshua Tsuji20103542020-02-18 14:06:28 -0500713 } else if (isStackStuckToTarget()) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400714 return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800715 } else {
716 return NONE;
717 }
718 }
719
720
721 @Override
722 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
723 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400724 // If we're in the dismiss target, have the bubbles pile on top of each other with no
725 // offset.
Joshua Tsuji20103542020-02-18 14:06:28 -0500726 if (isStackStuckToTarget()) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400727 return 0f;
728 } else {
729 // Offset to the left if we're on the left, or the right otherwise.
730 return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
731 ? -mStackOffset : mStackOffset;
732 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800733 } else {
734 return 0f;
735 }
736 }
737
738 @Override
739 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
740 return new SpringForce()
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400741 .setDampingRatio(DEFAULT_BOUNCINESS)
742 .setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800743 }
744
745 @Override
746 void onChildAdded(View child, int index) {
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400747 // Don't animate additions within the dismiss target.
Joshua Tsuji20103542020-02-18 14:06:28 -0500748 if (isStackStuckToTarget()) {
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400749 return;
750 }
751
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400752 if (getBubbleCount() == 1) {
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400753 // If this is the first child added, position the stack in its starting position.
754 moveStackToStartPosition();
755 } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500756 // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
757 // 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 -0400758 animateInBubble(child, index);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800759 }
760 }
761
762 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500763 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Mady Mellor88552b82019-08-05 22:38:59 +0000764 // Animate the removing view in the opposite direction of the stack.
765 final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujic1108432019-02-22 16:10:12 -0500766 animationForChild(child)
Mady Mellor88552b82019-08-05 22:38:59 +0000767 .alpha(0f, finishRemoval /* after */)
768 .scaleX(ANIMATE_IN_STARTING_SCALE)
769 .scaleY(ANIMATE_IN_STARTING_SCALE)
770 .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR))
Joshua Tsujic1108432019-02-22 16:10:12 -0500771 .start();
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500772
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400773 // If there are other bubbles, pull them into the correct position.
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400774 if (getBubbleCount() > 0) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500775 animationForChildAtIndex(0).translationX(mStackPosition.x).start();
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400776 } else {
Mady Mellord6edbca2019-11-18 15:06:44 -0800777 // When all children are removed ensure stack position is sane
778 setStackPosition(mRestingStackPosition == null
779 ? getDefaultStartPosition()
780 : mRestingStackPosition);
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500781
782 // Remove the stack from the coordinator since we don't have any bubbles and aren't
783 // visible.
784 mFloatingContentCoordinator.onContentRemoved(mStackFloatingContent);
Joshua Tsujic1108432019-02-22 16:10:12 -0500785 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800786 }
787
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400788 @Override
Joshua Tsujid2a7c2152019-07-15 15:45:20 -0400789 void onChildReordered(View child, int oldIndex, int newIndex) {
790 if (isStackPositionSet()) {
791 setStackPosition(mStackPosition);
792 }
793 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400794
795 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400796 void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
797 Resources res = layout.getResources();
798 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
Lyn Hana511e1fb2019-06-17 12:35:08 -0700799 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Lyn Hancd4f87e2020-02-19 20:33:45 -0800800 mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400801 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400802 mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
803 mStackStartingVerticalOffset =
804 res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
805 mStatusBarHeight =
806 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
807 }
808
Mady Mellor818eef02019-08-16 16:12:29 -0700809 /**
810 * Update effective screen width based on current orientation.
811 * @param orientation Landscape or portrait.
812 */
813 public void updateOrientation(int orientation) {
814 if (mLayout != null) {
815 Resources res = mLayout.getContext().getResources();
816 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
817 mStatusBarHeight = res.getDimensionPixelSize(
818 com.android.internal.R.dimen.status_bar_height);
819 }
820 }
821
Joshua Tsuji20103542020-02-18 14:06:28 -0500822 private boolean isStackStuckToTarget() {
823 return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget();
824 }
Mady Mellor818eef02019-08-16 16:12:29 -0700825
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800826 /** Moves the stack, without any animation, to the starting position. */
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400827 private void moveStackToStartPosition() {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500828 // Post to ensure that the layout's width and height have been calculated.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500829 mLayout.setVisibility(View.INVISIBLE);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500830 mLayout.post(() -> {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400831 setStackPosition(mRestingStackPosition == null
832 ? getDefaultStartPosition()
833 : mRestingStackPosition);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400834 mStackMovedToStartPosition = true;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500835 mLayout.setVisibility(View.VISIBLE);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400836
837 // Animate in the top bubble now that we're visible.
838 if (mLayout.getChildCount() > 0) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500839 // Add the stack to the floating content coordinator now that we have a bubble and
840 // are visible.
841 mFloatingContentCoordinator.onContentAdded(mStackFloatingContent);
842
Joshua Tsuji14e68552019-06-06 17:17:08 -0400843 animateInBubble(mLayout.getChildAt(0), 0 /* index */);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400844 }
Joshua Tsujif44347f2019-02-12 14:28:06 -0500845 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800846 }
847
848 /**
849 * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
850 * bubbles to animate 'following' to the new location.
851 */
852 private void moveFirstBubbleWithStackFollowing(
853 DynamicAnimation.ViewProperty property, float value) {
854
855 // Update the canonical stack position.
856 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
857 mStackPosition.x = value;
858 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
859 mStackPosition.y = value;
860 }
861
862 if (mLayout.getChildCount() > 0) {
863 property.setValue(mLayout.getChildAt(0), value);
Joshua Tsujic1108432019-02-22 16:10:12 -0500864 if (mLayout.getChildCount() > 1) {
865 animationForChildAtIndex(1)
866 .property(property, value + getOffsetForChainedPropertyAnimation(property))
867 .start();
868 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800869 }
870 }
871
872 /** Moves the stack to a position instantly, with no animation. */
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800873 public void setStackPosition(PointF pos) {
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500874 Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
875 mStackPosition.set(pos.x, pos.y);
Joshua Tsujife1ba1e2020-03-09 13:29:29 -0400876
877 if (mRestingStackPosition == null) {
878 mRestingStackPosition = new PointF();
879 }
880
881 mRestingStackPosition.set(mStackPosition);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800882
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400883 // If we're not the active controller, we don't want to physically move the bubble views.
884 if (isActiveController()) {
Joshua Tsujif75ca272019-08-02 10:18:51 -0400885 // Cancel animations that could be moving the views.
886 mLayout.cancelAllAnimationsOfProperties(
887 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400888 cancelStackPositionAnimations();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800889
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400890 // Since we're not using the chained animations, apply the offsets manually.
891 final float xOffset = getOffsetForChainedPropertyAnimation(
892 DynamicAnimation.TRANSLATION_X);
893 final float yOffset = getOffsetForChainedPropertyAnimation(
894 DynamicAnimation.TRANSLATION_Y);
895 for (int i = 0; i < mLayout.getChildCount(); i++) {
896 mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
897 mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
898 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800899 }
900 }
901
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800902 /** Returns the default stack position, which is on the top left. */
903 public PointF getDefaultStartPosition() {
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500904 return new PointF(
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800905 getAllowableStackPositionRegion().left,
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500906 getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
907 }
908
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400909 private boolean isStackPositionSet() {
910 return mStackMovedToStartPosition;
911 }
912
Joshua Tsujif44347f2019-02-12 14:28:06 -0500913 /** Animates in the given bubble. */
Joshua Tsuji14e68552019-06-06 17:17:08 -0400914 private void animateInBubble(View child, int index) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400915 if (!isActiveController()) {
916 return;
917 }
918
Joshua Tsuji14e68552019-06-06 17:17:08 -0400919 final float xOffset =
920 getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500921
Joshua Tsuji14e68552019-06-06 17:17:08 -0400922 // Position the new bubble in the correct position, scaled down completely.
923 child.setTranslationX(mStackPosition.x + xOffset * index);
924 child.setTranslationY(mStackPosition.y);
925 child.setScaleX(0f);
926 child.setScaleY(0f);
927
928 // Push the subsequent views out of the way, if there are subsequent views.
929 if (index + 1 < mLayout.getChildCount()) {
930 animationForChildAtIndex(index + 1)
931 .translationX(mStackPosition.x + xOffset * (index + 1))
932 .withStiffness(SpringForce.STIFFNESS_LOW)
933 .start();
934 }
935
936 // Scale in the new bubble, slightly delayed.
Joshua Tsujic1108432019-02-22 16:10:12 -0500937 animationForChild(child)
Joshua Tsuji14e68552019-06-06 17:17:08 -0400938 .scaleX(1f)
939 .scaleY(1f)
940 .withStiffness(ANIMATE_IN_STIFFNESS)
941 .withStartDelay(mLayout.getChildCount() > 1 ? ANIMATE_IN_START_DELAY : 0)
Joshua Tsujic1108432019-02-22 16:10:12 -0500942 .start();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500943 }
944
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800945 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800946 * Cancels any outstanding first bubble property animations that are running. This does not
947 * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
948 * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
949 * {@link #flingThenSpringFirstBubbleWithStackFollowing}.
950 */
951 private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
952 if (mStackPositionAnimations.containsKey(property)) {
953 mStackPositionAnimations.get(property).cancel();
954 }
955 }
956
957 /**
Joshua Tsuji20103542020-02-18 14:06:28 -0500958 * Returns the {@link MagnetizedObject} instance for the bubble stack, with the provided
959 * {@link MagnetizedObject.MagneticTarget} added as a target.
960 */
961 public MagnetizedObject<StackAnimationController> getMagnetizedStack(
962 MagnetizedObject.MagneticTarget target) {
963 if (mMagnetizedStack == null) {
964 mMagnetizedStack = new MagnetizedObject<StackAnimationController>(
965 mLayout.getContext(),
966 this,
967 new StackPositionProperty(DynamicAnimation.TRANSLATION_X),
968 new StackPositionProperty(DynamicAnimation.TRANSLATION_Y)
969 ) {
970 @Override
971 public float getWidth(@NonNull StackAnimationController underlyingObject) {
972 return mBubbleSize;
973 }
974
975 @Override
976 public float getHeight(@NonNull StackAnimationController underlyingObject) {
977 return mBubbleSize;
978 }
979
980 @Override
981 public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject,
982 @NonNull int[] loc) {
983 loc[0] = (int) mStackPosition.x;
984 loc[1] = (int) mStackPosition.y;
985 }
986 };
987 mMagnetizedStack.addTarget(target);
988 mMagnetizedStack.setHapticsEnabled(true);
989 mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
990 }
991
992 return mMagnetizedStack;
993 }
994
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400995 /** Returns the number of 'real' bubbles (excluding overflow). */
996 private int getBubbleCount() {
997 return mBubbleCountSupplier.getAsInt();
998 }
999
Joshua Tsuji20103542020-02-18 14:06:28 -05001000 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001001 * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
1002 * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
1003 * property directly to move the first bubble and cause the stack to 'follow' to the new
1004 * location.
1005 *
1006 * This could also be achieved by simply animating the first bubble view and adding an update
1007 * listener to dispatch movement to the rest of the stack. However, this would require
1008 * duplication of logic in that update handler - it's simpler to keep all logic contained in the
1009 * {@link #moveFirstBubbleWithStackFollowing} method.
1010 */
1011 private class StackPositionProperty
1012 extends FloatPropertyCompat<StackAnimationController> {
1013 private final DynamicAnimation.ViewProperty mProperty;
1014
1015 private StackPositionProperty(DynamicAnimation.ViewProperty property) {
1016 super(property.toString());
1017 mProperty = property;
1018 }
1019
1020 @Override
1021 public float getValue(StackAnimationController controller) {
Joshua Tsujid9422832019-03-05 13:32:37 -05001022 return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001023 }
1024
1025 @Override
1026 public void setValue(StackAnimationController controller, float value) {
1027 moveFirstBubbleWithStackFollowing(mProperty, value);
1028 }
1029 }
1030}
1031