blob: 5f3a2bd9eb8b6f4cd0e14114dcc72df8668a3673 [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 /**
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400650 * 'Implode' the stack by shrinking the bubbles, fading them out, and translating them down.
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400651 */
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400652 public void animateStackDismissal(float translationYBy, Runnable after) {
653 animationsForChildrenFromIndex(0, (index, animation) ->
654 animation
655 .scaleX(0.5f)
656 .scaleY(0.5f)
657 .alpha(0f)
658 .translationY(
659 mLayout.getChildAt(index).getTranslationY() + translationYBy)
660 .withStiffness(SpringForce.STIFFNESS_HIGH))
661 .startAll(after);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400662 }
663
664 /**
665 * Springs the first bubble to the given final position, with the rest of the stack 'following'.
666 */
667 protected void springFirstBubbleWithStackFollowing(
668 DynamicAnimation.ViewProperty property, SpringForce spring,
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400669 float vel, float finalPosition, @Nullable Runnable... after) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400670
671 if (mLayout.getChildCount() == 0) {
672 return;
673 }
674
675 Log.d(TAG, String.format("Springing %s to final position %f.",
676 PhysicsAnimationLayout.getReadablePropertyName(property),
677 finalPosition));
678
679 StackPositionProperty firstBubbleProperty = new StackPositionProperty(property);
680 SpringAnimation springAnimation =
681 new SpringAnimation(this, firstBubbleProperty)
682 .setSpring(spring)
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400683 .addEndListener((dynamicAnimation, b, v, v1) -> {
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400684 mRestingStackPosition.set(mStackPosition);
685
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400686 if (after != null) {
687 for (Runnable callback : after) {
688 callback.run();
689 }
690 }
691 })
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400692 .setStartVelocity(vel);
693
694 cancelStackPositionAnimation(property);
695 mStackPositionAnimations.put(property, springAnimation);
696 springAnimation.animateToFinalPosition(finalPosition);
697 }
698
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800699 @Override
700 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
701 return Sets.newHashSet(
702 DynamicAnimation.TRANSLATION_X, // For positioning.
703 DynamicAnimation.TRANSLATION_Y,
704 DynamicAnimation.ALPHA, // For fading in new bubbles.
705 DynamicAnimation.SCALE_X, // For 'popping in' new bubbles.
706 DynamicAnimation.SCALE_Y);
707 }
708
709 @Override
710 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
711 if (property.equals(DynamicAnimation.TRANSLATION_X)
712 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400713 return index + 1;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800714 } else {
715 return NONE;
716 }
717 }
718
719
720 @Override
721 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
722 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400723 // If we're in the dismiss target, have the bubbles pile on top of each other with no
724 // offset.
Joshua Tsuji20103542020-02-18 14:06:28 -0500725 if (isStackStuckToTarget()) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400726 return 0f;
727 } else {
728 // Offset to the left if we're on the left, or the right otherwise.
729 return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x)
730 ? -mStackOffset : mStackOffset;
731 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800732 } else {
733 return 0f;
734 }
735 }
736
737 @Override
738 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
739 return new SpringForce()
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400740 .setDampingRatio(DEFAULT_BOUNCINESS)
741 .setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800742 }
743
744 @Override
745 void onChildAdded(View child, int index) {
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400746 // Don't animate additions within the dismiss target.
Joshua Tsuji20103542020-02-18 14:06:28 -0500747 if (isStackStuckToTarget()) {
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400748 return;
749 }
750
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400751 if (getBubbleCount() == 1) {
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400752 // If this is the first child added, position the stack in its starting position.
753 moveStackToStartPosition();
754 } else if (isStackPositionSet() && mLayout.indexOfChild(child) == 0) {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500755 // Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
756 // 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 -0400757 animateInBubble(child, index);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800758 }
759 }
760
761 @Override
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500762 void onChildRemoved(View child, int index, Runnable finishRemoval) {
Mady Mellor88552b82019-08-05 22:38:59 +0000763 // Animate the removing view in the opposite direction of the stack.
764 final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujic1108432019-02-22 16:10:12 -0500765 animationForChild(child)
Mady Mellor88552b82019-08-05 22:38:59 +0000766 .alpha(0f, finishRemoval /* after */)
767 .scaleX(ANIMATE_IN_STARTING_SCALE)
768 .scaleY(ANIMATE_IN_STARTING_SCALE)
769 .translationX(mStackPosition.x - (-xOffset * ANIMATE_TRANSLATION_FACTOR))
Joshua Tsujic1108432019-02-22 16:10:12 -0500770 .start();
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500771
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400772 // If there are other bubbles, pull them into the correct position.
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400773 if (getBubbleCount() > 0) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500774 animationForChildAtIndex(0).translationX(mStackPosition.x).start();
Joshua Tsujiaf8df2d2019-08-01 16:08:01 -0400775 } else {
Mady Mellord6edbca2019-11-18 15:06:44 -0800776 // When all children are removed ensure stack position is sane
777 setStackPosition(mRestingStackPosition == null
778 ? getDefaultStartPosition()
779 : mRestingStackPosition);
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500780
781 // Remove the stack from the coordinator since we don't have any bubbles and aren't
782 // visible.
783 mFloatingContentCoordinator.onContentRemoved(mStackFloatingContent);
Joshua Tsujic1108432019-02-22 16:10:12 -0500784 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800785 }
786
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400787 @Override
Joshua Tsujid2a7c2152019-07-15 15:45:20 -0400788 void onChildReordered(View child, int oldIndex, int newIndex) {
789 if (isStackPositionSet()) {
790 setStackPosition(mStackPosition);
791 }
792 }
Joshua Tsujif49ee142019-05-29 16:32:01 -0400793
794 @Override
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400795 void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
796 Resources res = layout.getResources();
797 mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
Lyn Hana511e1fb2019-06-17 12:35:08 -0700798 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Lyn Hancd4f87e2020-02-19 20:33:45 -0800799 mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size);
Joshua Tsuji61b38f52019-05-31 16:20:22 -0400800 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400801 mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
802 mStackStartingVerticalOffset =
803 res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y);
804 mStatusBarHeight =
805 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
806 }
807
Mady Mellor818eef02019-08-16 16:12:29 -0700808 /**
809 * Update effective screen width based on current orientation.
810 * @param orientation Landscape or portrait.
811 */
812 public void updateOrientation(int orientation) {
813 if (mLayout != null) {
814 Resources res = mLayout.getContext().getResources();
815 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
816 mStatusBarHeight = res.getDimensionPixelSize(
817 com.android.internal.R.dimen.status_bar_height);
818 }
819 }
820
Joshua Tsuji20103542020-02-18 14:06:28 -0500821 private boolean isStackStuckToTarget() {
822 return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget();
823 }
Mady Mellor818eef02019-08-16 16:12:29 -0700824
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800825 /** Moves the stack, without any animation, to the starting position. */
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400826 private void moveStackToStartPosition() {
Joshua Tsujif44347f2019-02-12 14:28:06 -0500827 // Post to ensure that the layout's width and height have been calculated.
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500828 mLayout.setVisibility(View.INVISIBLE);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500829 mLayout.post(() -> {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400830 setStackPosition(mRestingStackPosition == null
831 ? getDefaultStartPosition()
832 : mRestingStackPosition);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400833 mStackMovedToStartPosition = true;
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500834 mLayout.setVisibility(View.VISIBLE);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400835
836 // Animate in the top bubble now that we're visible.
837 if (mLayout.getChildCount() > 0) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500838 // Add the stack to the floating content coordinator now that we have a bubble and
839 // are visible.
840 mFloatingContentCoordinator.onContentAdded(mStackFloatingContent);
841
Joshua Tsuji14e68552019-06-06 17:17:08 -0400842 animateInBubble(mLayout.getChildAt(0), 0 /* index */);
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400843 }
Joshua Tsujif44347f2019-02-12 14:28:06 -0500844 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800845 }
846
847 /**
848 * Moves the first bubble instantly to the given X or Y translation, and instructs subsequent
849 * bubbles to animate 'following' to the new location.
850 */
851 private void moveFirstBubbleWithStackFollowing(
852 DynamicAnimation.ViewProperty property, float value) {
853
854 // Update the canonical stack position.
855 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
856 mStackPosition.x = value;
857 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
858 mStackPosition.y = value;
859 }
860
861 if (mLayout.getChildCount() > 0) {
862 property.setValue(mLayout.getChildAt(0), value);
Joshua Tsujic1108432019-02-22 16:10:12 -0500863 if (mLayout.getChildCount() > 1) {
864 animationForChildAtIndex(1)
865 .property(property, value + getOffsetForChainedPropertyAnimation(property))
866 .start();
867 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800868 }
869 }
870
871 /** Moves the stack to a position instantly, with no animation. */
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800872 public void setStackPosition(PointF pos) {
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500873 Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
874 mStackPosition.set(pos.x, pos.y);
Joshua Tsujife1ba1e2020-03-09 13:29:29 -0400875
876 if (mRestingStackPosition == null) {
877 mRestingStackPosition = new PointF();
878 }
879
880 mRestingStackPosition.set(mStackPosition);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800881
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400882 // If we're not the active controller, we don't want to physically move the bubble views.
883 if (isActiveController()) {
Joshua Tsujif75ca272019-08-02 10:18:51 -0400884 // Cancel animations that could be moving the views.
885 mLayout.cancelAllAnimationsOfProperties(
886 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400887 cancelStackPositionAnimations();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800888
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400889 // Since we're not using the chained animations, apply the offsets manually.
890 final float xOffset = getOffsetForChainedPropertyAnimation(
891 DynamicAnimation.TRANSLATION_X);
892 final float yOffset = getOffsetForChainedPropertyAnimation(
893 DynamicAnimation.TRANSLATION_Y);
894 for (int i = 0; i < mLayout.getChildCount(); i++) {
895 mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset));
896 mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset));
897 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800898 }
899 }
900
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800901 /** Returns the default stack position, which is on the top left. */
902 public PointF getDefaultStartPosition() {
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500903 return new PointF(
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800904 getAllowableStackPositionRegion().left,
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500905 getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
906 }
907
Joshua Tsuji33c0e9c2019-05-14 16:45:39 -0400908 private boolean isStackPositionSet() {
909 return mStackMovedToStartPosition;
910 }
911
Joshua Tsujif44347f2019-02-12 14:28:06 -0500912 /** Animates in the given bubble. */
Joshua Tsuji14e68552019-06-06 17:17:08 -0400913 private void animateInBubble(View child, int index) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400914 if (!isActiveController()) {
915 return;
916 }
917
Joshua Tsuji14e68552019-06-06 17:17:08 -0400918 final float xOffset =
919 getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500920
Joshua Tsuji14e68552019-06-06 17:17:08 -0400921 // Position the new bubble in the correct position, scaled down completely.
922 child.setTranslationX(mStackPosition.x + xOffset * index);
923 child.setTranslationY(mStackPosition.y);
924 child.setScaleX(0f);
925 child.setScaleY(0f);
926
927 // Push the subsequent views out of the way, if there are subsequent views.
928 if (index + 1 < mLayout.getChildCount()) {
929 animationForChildAtIndex(index + 1)
930 .translationX(mStackPosition.x + xOffset * (index + 1))
931 .withStiffness(SpringForce.STIFFNESS_LOW)
932 .start();
933 }
934
935 // Scale in the new bubble, slightly delayed.
Joshua Tsujic1108432019-02-22 16:10:12 -0500936 animationForChild(child)
Joshua Tsuji14e68552019-06-06 17:17:08 -0400937 .scaleX(1f)
938 .scaleY(1f)
939 .withStiffness(ANIMATE_IN_STIFFNESS)
940 .withStartDelay(mLayout.getChildCount() > 1 ? ANIMATE_IN_START_DELAY : 0)
Joshua Tsujic1108432019-02-22 16:10:12 -0500941 .start();
Joshua Tsujif44347f2019-02-12 14:28:06 -0500942 }
943
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800944 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800945 * Cancels any outstanding first bubble property animations that are running. This does not
946 * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only
947 * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and
948 * {@link #flingThenSpringFirstBubbleWithStackFollowing}.
949 */
950 private void cancelStackPositionAnimation(DynamicAnimation.ViewProperty property) {
951 if (mStackPositionAnimations.containsKey(property)) {
952 mStackPositionAnimations.get(property).cancel();
953 }
954 }
955
956 /**
Joshua Tsuji20103542020-02-18 14:06:28 -0500957 * Returns the {@link MagnetizedObject} instance for the bubble stack, with the provided
958 * {@link MagnetizedObject.MagneticTarget} added as a target.
959 */
960 public MagnetizedObject<StackAnimationController> getMagnetizedStack(
961 MagnetizedObject.MagneticTarget target) {
962 if (mMagnetizedStack == null) {
963 mMagnetizedStack = new MagnetizedObject<StackAnimationController>(
964 mLayout.getContext(),
965 this,
966 new StackPositionProperty(DynamicAnimation.TRANSLATION_X),
967 new StackPositionProperty(DynamicAnimation.TRANSLATION_Y)
968 ) {
969 @Override
970 public float getWidth(@NonNull StackAnimationController underlyingObject) {
971 return mBubbleSize;
972 }
973
974 @Override
975 public float getHeight(@NonNull StackAnimationController underlyingObject) {
976 return mBubbleSize;
977 }
978
979 @Override
980 public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject,
981 @NonNull int[] loc) {
982 loc[0] = (int) mStackPosition.x;
983 loc[1] = (int) mStackPosition.y;
984 }
985 };
986 mMagnetizedStack.addTarget(target);
987 mMagnetizedStack.setHapticsEnabled(true);
988 mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
989 }
990
991 return mMagnetizedStack;
992 }
993
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400994 /** Returns the number of 'real' bubbles (excluding overflow). */
995 private int getBubbleCount() {
996 return mBubbleCountSupplier.getAsInt();
997 }
998
Joshua Tsuji20103542020-02-18 14:06:28 -0500999 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001000 * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
1001 * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
1002 * property directly to move the first bubble and cause the stack to 'follow' to the new
1003 * location.
1004 *
1005 * This could also be achieved by simply animating the first bubble view and adding an update
1006 * listener to dispatch movement to the rest of the stack. However, this would require
1007 * duplication of logic in that update handler - it's simpler to keep all logic contained in the
1008 * {@link #moveFirstBubbleWithStackFollowing} method.
1009 */
1010 private class StackPositionProperty
1011 extends FloatPropertyCompat<StackAnimationController> {
1012 private final DynamicAnimation.ViewProperty mProperty;
1013
1014 private StackPositionProperty(DynamicAnimation.ViewProperty property) {
1015 super(property.toString());
1016 mProperty = property;
1017 }
1018
1019 @Override
1020 public float getValue(StackAnimationController controller) {
Joshua Tsujid9422832019-03-05 13:32:37 -05001021 return mLayout.getChildCount() > 0 ? mProperty.getValue(mLayout.getChildAt(0)) : 0;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001022 }
1023
1024 @Override
1025 public void setValue(StackAnimationController controller, float value) {
1026 moveFirstBubbleWithStackFollowing(mProperty, value);
1027 }
1028 }
1029}
1030