blob: 3a3339249d5b04871a530f6a6c3a4e0eadbe50a5 [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
19import android.content.Context;
Joshua Tsujic36ee6f2019-05-28 17:00:16 -040020import android.util.Log;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080021import android.view.View;
22import android.view.ViewGroup;
23import android.widget.FrameLayout;
24
Joshua Tsujic36ee6f2019-05-28 17:00:16 -040025import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080026import androidx.dynamicanimation.animation.DynamicAnimation;
27import androidx.dynamicanimation.animation.SpringAnimation;
28import androidx.dynamicanimation.animation.SpringForce;
29
30import com.android.systemui.R;
31
Joshua Tsujic1108432019-02-22 16:10:12 -050032import java.util.ArrayList;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080033import java.util.HashMap;
Joshua Tsujia08b6d32019-01-29 16:15:52 -050034import java.util.HashSet;
Joshua Tsujic1108432019-02-22 16:10:12 -050035import java.util.List;
36import java.util.Map;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080037import java.util.Set;
38
39/**
40 * Layout that constructs physics-based animations for each of its children, which behave according
41 * to settings provided by a {@link PhysicsAnimationController} instance.
42 *
43 * See physics-animation-layout.md.
44 */
45public class PhysicsAnimationLayout extends FrameLayout {
46 private static final String TAG = "Bubbs.PAL";
47
48 /**
49 * Controls the construction, configuration, and use of the physics animations supplied by this
50 * layout.
51 */
52 abstract static class PhysicsAnimationController {
53
Joshua Tsujic1108432019-02-22 16:10:12 -050054 /** Configures a given {@link PhysicsPropertyAnimator} for a view at the given index. */
55 interface ChildAnimationConfigurator {
56
57 /**
58 * Called to configure the animator for the view at the given index.
59 *
60 * This method should make use of methods such as
61 * {@link PhysicsPropertyAnimator#translationX} and
62 * {@link PhysicsPropertyAnimator#withStartDelay} to configure the animation.
63 *
64 * Implementations should not call {@link PhysicsPropertyAnimator#start}, this will
65 * happen elsewhere after configuration is complete.
66 */
67 void configureAnimationForChildAtIndex(int index, PhysicsPropertyAnimator animation);
68 }
69
70 /**
71 * Returned by {@link #animationsForChildrenFromIndex} to allow starting multiple animations
72 * on multiple child views at the same time.
73 */
74 interface MultiAnimationStarter {
75
76 /**
77 * Start all animations and call the given end actions once all animations have
78 * completed.
79 */
80 void startAll(Runnable... endActions);
81 }
82
Joshua Tsujib1a796b2019-01-16 15:43:12 -080083 /**
84 * Constant to return from {@link #getNextAnimationInChain} if the animation should not be
85 * chained at all.
86 */
87 protected static final int NONE = -1;
88
89 /** Set of properties for which the layout should construct physics animations. */
90 abstract Set<DynamicAnimation.ViewProperty> getAnimatedProperties();
91
92 /**
93 * Returns the index of the next animation after the given index in the animation chain, or
94 * {@link #NONE} if it should not be chained, or if the chain should end at the given index.
95 *
96 * If a next index is returned, an update listener will be added to the animation at the
97 * given index that dispatches value updates to the animation at the next index. This
98 * creates a 'following' effect.
99 *
100 * Typical implementations of this method will return either index + 1, or index - 1, to
101 * create forward or backward chains between adjacent child views, but this is not required.
102 */
103 abstract int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index);
104
105 /**
106 * Offsets to be added to the value that chained animations of the given property dispatch
107 * to subsequent child animations.
108 *
109 * This is used for things like maintaining the 'stack' effect in Bubbles, where bubbles
110 * stack off to the left or right side slightly.
111 */
112 abstract float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property);
113
114 /**
115 * Returns the SpringForce to be used for the given child view's property animation. Despite
116 * these usually being similar or identical across properties and views, {@link SpringForce}
117 * also contains the SpringAnimation's final position, so we have to construct a new one for
118 * each animation rather than using a constant.
119 */
120 abstract SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view);
121
122 /**
123 * Called when a new child is added at the specified index. Controllers can use this
124 * opportunity to animate in the new view.
125 */
126 abstract void onChildAdded(View child, int index);
127
128 /**
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500129 * Called with a child view that has been removed from the layout, from the given index. The
130 * passed view has been removed from the layout and added back as a transient view, which
131 * renders normally, but is not part of the normal view hierarchy and will not be considered
132 * by getChildAt() and getChildCount().
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800133 *
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500134 * The controller can perform animations on the child (either manually, or by using
Joshua Tsujic1108432019-02-22 16:10:12 -0500135 * {@link #animationForChild(View)}), and then call finishRemoval when complete.
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500136 *
137 * finishRemoval must be called by implementations of this method, or transient views will
138 * never be removed.
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800139 */
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500140 abstract void onChildRemoved(View child, int index, Runnable finishRemoval);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800141
Joshua Tsujif49ee142019-05-29 16:32:01 -0400142 /** Called when a child view has been reordered in the view hierachy. */
143 abstract void onChildReordered(View child, int oldIndex, int newIndex);
144
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400145 /**
146 * Called when the controller is set as the active animation controller for the given
147 * layout. Once active, the controller can start animations using the animator instances
148 * returned by {@link #animationForChild}.
149 *
150 * While all animations started by the previous controller will be cancelled, the new
151 * controller should not make any assumptions about the state of the layout or its children.
152 * Their translation, alpha, scale, etc. values may have been changed by the previous
153 * controller and should be reset here if relevant.
154 */
155 abstract void onActiveControllerForLayout(PhysicsAnimationLayout layout);
156
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800157 protected PhysicsAnimationLayout mLayout;
158
159 PhysicsAnimationController() { }
160
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400161 /** Whether this controller is the currently active controller for its associated layout. */
162 protected boolean isActiveController() {
163 return this == mLayout.mController;
164 }
165
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800166 protected void setLayout(PhysicsAnimationLayout layout) {
167 this.mLayout = layout;
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400168 onActiveControllerForLayout(layout);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800169 }
170
171 protected PhysicsAnimationLayout getLayout() {
172 return mLayout;
173 }
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500174
175 /**
Joshua Tsujic1108432019-02-22 16:10:12 -0500176 * Returns a {@link PhysicsPropertyAnimator} instance for the given child view.
177 */
178 protected PhysicsPropertyAnimator animationForChild(View child) {
179 PhysicsPropertyAnimator animator =
180 (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag);
181
182 if (animator == null) {
183 animator = mLayout.new PhysicsPropertyAnimator(child);
184 child.setTag(R.id.physics_animator_tag, animator);
185 }
186
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400187 animator.clearAnimator();
188 animator.setAssociatedController(this);
189
Joshua Tsujic1108432019-02-22 16:10:12 -0500190 return animator;
191 }
192
193 /** Returns a {@link PhysicsPropertyAnimator} instance for child at the given index. */
194 protected PhysicsPropertyAnimator animationForChildAtIndex(int index) {
195 return animationForChild(mLayout.getChildAt(index));
196 }
197
198 /**
199 * Returns a {@link MultiAnimationStarter} whose startAll method will start the physics
200 * animations for all children from startIndex onward. The provided configurator will be
201 * called with each child's {@link PhysicsPropertyAnimator}, where it can set up each
202 * animation appropriately.
203 */
204 protected MultiAnimationStarter animationsForChildrenFromIndex(
205 int startIndex, ChildAnimationConfigurator configurator) {
206 final Set<DynamicAnimation.ViewProperty> allAnimatedProperties = new HashSet<>();
207 final List<PhysicsPropertyAnimator> allChildAnims = new ArrayList<>();
208
209 // Retrieve the animator for each child, ask the configurator to configure it, then save
210 // it and the properties it chose to animate.
211 for (int i = startIndex; i < mLayout.getChildCount(); i++) {
212 final PhysicsPropertyAnimator anim = animationForChildAtIndex(i);
213 configurator.configureAnimationForChildAtIndex(i, anim);
214 allAnimatedProperties.addAll(anim.getAnimatedProperties());
215 allChildAnims.add(anim);
216 }
217
218 // Return a MultiAnimationStarter that will start all of the child animations, and also
219 // add a multiple property end listener to the layout that will call the end action
220 // provided to startAll() once all animations on the animated properties complete.
221 return (endActions) -> {
Joshua Tsujid54e8e02019-04-05 14:21:53 -0400222 final Runnable runAllEndActions = () -> {
223 for (Runnable action : endActions) {
224 action.run();
225 }
226 };
227
228 // If there aren't any children to animate, just run the end actions.
229 if (mLayout.getChildCount() == 0) {
230 runAllEndActions.run();
231 return;
232 }
233
Joshua Tsujic1108432019-02-22 16:10:12 -0500234 if (endActions != null) {
235 mLayout.setEndActionForMultipleProperties(
Joshua Tsujid54e8e02019-04-05 14:21:53 -0400236 runAllEndActions,
Joshua Tsujic1108432019-02-22 16:10:12 -0500237 allAnimatedProperties.toArray(
238 new DynamicAnimation.ViewProperty[0]));
239 }
240
241 for (PhysicsPropertyAnimator childAnim : allChildAnims) {
242 childAnim.start();
243 }
244 };
245 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800246 }
247
248 /**
Joshua Tsujic1108432019-02-22 16:10:12 -0500249 * End actions that are called when every child's animation of the given property has finished.
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800250 */
Joshua Tsujic1108432019-02-22 16:10:12 -0500251 protected final HashMap<DynamicAnimation.ViewProperty, Runnable> mEndActionForProperty =
252 new HashMap<>();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800253
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800254 /** The currently active animation controller. */
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400255 @Nullable protected PhysicsAnimationController mController;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800256
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800257 public PhysicsAnimationLayout(Context context) {
258 super(context);
259 }
260
261 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800262 * Sets the animation controller and constructs or reconfigures the layout's physics animations
263 * to meet the controller's specifications.
264 */
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400265 public void setActiveController(PhysicsAnimationController controller) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800266 cancelAllAnimations();
Joshua Tsujic1108432019-02-22 16:10:12 -0500267 mEndActionForProperty.clear();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800268
269 this.mController = controller;
270 mController.setLayout(this);
271
272 // Set up animations for this controller's animated properties.
273 for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
274 setUpAnimationsForProperty(property);
275 }
276 }
277
278 /**
Joshua Tsujic1108432019-02-22 16:10:12 -0500279 * Sets an end action that will be run when all child animations for a given property have
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800280 * stopped running.
281 */
Joshua Tsujic1108432019-02-22 16:10:12 -0500282 public void setEndActionForProperty(Runnable action, DynamicAnimation.ViewProperty property) {
283 mEndActionForProperty.put(property, action);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800284 }
285
286 /**
Joshua Tsujic1108432019-02-22 16:10:12 -0500287 * Sets an end action that will be run when all child animations for all of the given properties
288 * have stopped running.
Joshua Tsuji442b6272019-02-08 13:23:43 -0500289 */
Joshua Tsujic1108432019-02-22 16:10:12 -0500290 public void setEndActionForMultipleProperties(
291 Runnable action, DynamicAnimation.ViewProperty... properties) {
292 final Runnable checkIfAllFinished = () -> {
293 if (!arePropertiesAnimating(properties)) {
294 action.run();
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400295
296 for (DynamicAnimation.ViewProperty property : properties) {
297 removeEndActionForProperty(property);
298 }
Joshua Tsujic1108432019-02-22 16:10:12 -0500299 }
300 };
301
Joshua Tsuji442b6272019-02-08 13:23:43 -0500302 for (DynamicAnimation.ViewProperty property : properties) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500303 setEndActionForProperty(checkIfAllFinished, property);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500304 }
305 }
306
307 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800308 * Removes the end listener that would have been called when all child animations for a given
309 * property stopped running.
310 */
Joshua Tsujic1108432019-02-22 16:10:12 -0500311 public void removeEndActionForProperty(DynamicAnimation.ViewProperty property) {
312 mEndActionForProperty.remove(property);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800313 }
314
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800315 @Override
316 public void addView(View child, int index, ViewGroup.LayoutParams params) {
Joshua Tsujif49ee142019-05-29 16:32:01 -0400317 addViewInternal(child, index, params, false /* isReorder */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800318 }
319
320 @Override
321 public void removeView(View view) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800322 if (mController != null) {
323 final int index = indexOfChild(view);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800324
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500325 // Remove the view and add it back as a transient view so we can animate it out.
326 super.removeView(view);
327 addTransientView(view, index);
328
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500329 // Tell the controller to animate this view out, and call the callback when it's
330 // finished.
Joshua Tsuji1575e6b2019-01-30 13:43:28 -0500331 mController.onChildRemoved(view, index, () -> {
Joshua Tsuji7df42862019-03-22 15:54:31 -0400332 // The controller says it's done with the transient view, cancel animations in case
333 // any are still running and then remove it.
334 cancelAnimationsOnView(view);
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500335 removeTransientView(view);
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500336 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800337 } else {
338 // Without a controller, nobody will animate this view out, so it gets an unceremonious
339 // departure.
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500340 super.removeView(view);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400341 }
342 }
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500343
Joshua Tsujif49ee142019-05-29 16:32:01 -0400344 @Override
345 public void removeViewAt(int index) {
346 removeView(getChildAt(index));
347 }
348
349 /** Immediately re-orders the view to the given index. */
350 public void reorderView(View view, int index) {
351 final int oldIndex = indexOfChild(view);
352
353 super.removeView(view);
354 addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */);
355
356 if (mController != null) {
357 mController.onChildReordered(view, oldIndex, index);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800358 }
359 }
360
361 /** Checks whether any animations of the given properties are still running. */
362 public boolean arePropertiesAnimating(DynamicAnimation.ViewProperty... properties) {
363 for (int i = 0; i < getChildCount(); i++) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400364 if (arePropertiesAnimatingOnView(getChildAt(i), properties)) {
365 return true;
366 }
367 }
368
369 return false;
370 }
371
372 /** Checks whether any animations of the given properties are running on the given view. */
373 public boolean arePropertiesAnimatingOnView(
374 View view, DynamicAnimation.ViewProperty... properties) {
375 for (DynamicAnimation.ViewProperty property : properties) {
376 final SpringAnimation animation = getAnimationFromView(property, view);
377 if (animation != null && animation.isRunning()) {
378 return true;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800379 }
380 }
381
382 return false;
383 }
384
385 /** Cancels all animations that are running on all child views, for all properties. */
386 public void cancelAllAnimations() {
387 if (mController == null) {
388 return;
389 }
390
391 for (int i = 0; i < getChildCount(); i++) {
392 for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500393 final DynamicAnimation anim = getAnimationAtIndex(property, i);
394 if (anim != null) {
395 anim.cancel();
396 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800397 }
398 }
399 }
400
Joshua Tsuji442b6272019-02-08 13:23:43 -0500401 /** Cancels all of the physics animations running on the given view. */
402 public void cancelAnimationsOnView(View view) {
403 for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
404 getAnimationFromView(property, view).cancel();
405 }
406 }
407
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400408 protected boolean isActiveController(PhysicsAnimationController controller) {
409 return mController == controller;
410 }
411
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800412 /** Whether the first child would be left of center if translated to the given x value. */
413 protected boolean isFirstChildXLeftOfCenter(float x) {
414 if (getChildCount() > 0) {
415 return x + (getChildAt(0).getWidth() / 2) < getWidth() / 2;
416 } else {
417 return false; // If there's no first child, really anything is correct, right?
418 }
419 }
420
421 /** ViewProperty's toString is useless, this returns a readable name for debug logging. */
422 protected static String getReadablePropertyName(DynamicAnimation.ViewProperty property) {
423 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
424 return "TRANSLATION_X";
425 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
426 return "TRANSLATION_Y";
427 } else if (property.equals(DynamicAnimation.SCALE_X)) {
428 return "SCALE_X";
429 } else if (property.equals(DynamicAnimation.SCALE_Y)) {
430 return "SCALE_Y";
431 } else if (property.equals(DynamicAnimation.ALPHA)) {
432 return "ALPHA";
433 } else {
434 return "Unknown animation property.";
435 }
436 }
437
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800438 /**
Joshua Tsujif49ee142019-05-29 16:32:01 -0400439 * Adds a view to the layout. If this addition is not the result of a call to
440 * {@link #reorderView}, this will also notify the controller via
441 * {@link PhysicsAnimationController#onChildAdded} and set up animations for the view.
442 */
443 private void addViewInternal(
444 View child, int index, ViewGroup.LayoutParams params, boolean isReorder) {
445 super.addView(child, index, params);
446
447 // Set up animations for the new view, if the controller is set. If it isn't set, we'll be
448 // setting up animations for all children when setActiveController is called.
449 if (mController != null && !isReorder) {
450 for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) {
451 setUpAnimationForChild(property, child, index);
452 }
453
454 mController.onChildAdded(child, index);
455 }
456 }
457
458 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800459 * Retrieves the animation of the given property from the view at the given index via the view
460 * tag system.
461 */
462 private SpringAnimation getAnimationAtIndex(
463 DynamicAnimation.ViewProperty property, int index) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500464 return getAnimationFromView(property, getChildAt(index));
465 }
466
467 /** Retrieves the animation of the given property from the view via the view tag system. */
468 private SpringAnimation getAnimationFromView(
469 DynamicAnimation.ViewProperty property, View view) {
470 return (SpringAnimation) view.getTag(getTagIdForProperty(property));
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800471 }
472
473 /** Sets up SpringAnimations of the given property for each child view in the layout. */
474 private void setUpAnimationsForProperty(DynamicAnimation.ViewProperty property) {
475 for (int i = 0; i < getChildCount(); i++) {
476 setUpAnimationForChild(property, getChildAt(i), i);
477 }
478 }
479
480 /** Constructs a SpringAnimation of the given property for a child view. */
481 private void setUpAnimationForChild(
482 DynamicAnimation.ViewProperty property, View child, int index) {
483 SpringAnimation newAnim = new SpringAnimation(child, property);
484 newAnim.addUpdateListener((animation, value, velocity) -> {
Joshua Tsuji7df42862019-03-22 15:54:31 -0400485 final int indexOfChild = indexOfChild(child);
Joshua Tsujicb97a112019-05-29 16:20:41 -0400486 final int nextAnimInChain = mController.getNextAnimationInChain(property, indexOfChild);
Joshua Tsujia08b6d32019-01-29 16:15:52 -0500487
Joshua Tsuji7df42862019-03-22 15:54:31 -0400488 if (nextAnimInChain == PhysicsAnimationController.NONE || indexOfChild < 0) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800489 return;
490 }
491
Joshua Tsujicb97a112019-05-29 16:20:41 -0400492 final float offset = mController.getOffsetForChainedPropertyAnimation(property);
493 if (nextAnimInChain < getChildCount()) {
494 getAnimationAtIndex(property, nextAnimInChain)
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800495 .animateToFinalPosition(value + offset);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800496 }
497 });
498
499 newAnim.setSpring(mController.getSpringForce(property, child));
500 newAnim.addEndListener(new AllAnimationsForPropertyFinishedEndListener(property));
501 child.setTag(getTagIdForProperty(property), newAnim);
502 }
503
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800504 /** Return a stable ID to use as a tag key for the given property's animations. */
505 private int getTagIdForProperty(DynamicAnimation.ViewProperty property) {
506 if (property.equals(DynamicAnimation.TRANSLATION_X)) {
507 return R.id.translation_x_dynamicanimation_tag;
508 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
509 return R.id.translation_y_dynamicanimation_tag;
510 } else if (property.equals(DynamicAnimation.SCALE_X)) {
511 return R.id.scale_x_dynamicanimation_tag;
512 } else if (property.equals(DynamicAnimation.SCALE_Y)) {
513 return R.id.scale_y_dynamicanimation_tag;
514 } else if (property.equals(DynamicAnimation.ALPHA)) {
515 return R.id.alpha_dynamicanimation_tag;
516 }
517
518 return -1;
519 }
520
521 /**
522 * End listener that is added to each individual DynamicAnimation, which dispatches to a single
523 * listener when every other animation of the given property is no longer running.
524 *
525 * This is required since chained DynamicAnimations can stop and start again due to changes in
526 * upstream animations. This means that adding an end listener to just the last animation is not
527 * sufficient. By firing only when every other animation on the property has stopped running, we
528 * ensure that no animation will be restarted after the single end listener is called.
529 */
530 protected class AllAnimationsForPropertyFinishedEndListener
531 implements DynamicAnimation.OnAnimationEndListener {
532 private DynamicAnimation.ViewProperty mProperty;
533
534 AllAnimationsForPropertyFinishedEndListener(DynamicAnimation.ViewProperty property) {
535 this.mProperty = property;
536 }
537
538 @Override
539 public void onAnimationEnd(
540 DynamicAnimation anim, boolean canceled, float value, float velocity) {
541 if (!arePropertiesAnimating(mProperty)) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500542 if (mEndActionForProperty.containsKey(mProperty)) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400543 final Runnable callback = mEndActionForProperty.get(mProperty);
544
545 if (callback != null) {
546 callback.run();
547 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800548 }
549 }
550 }
551 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500552
553 /**
Joshua Tsujic1108432019-02-22 16:10:12 -0500554 * Animator class returned by {@link PhysicsAnimationController#animationForChild}, to allow
555 * controllers to animate child views using physics animations.
556 *
557 * See docs/physics-animation-layout.md for documentation and examples.
Joshua Tsuji442b6272019-02-08 13:23:43 -0500558 */
Joshua Tsujic1108432019-02-22 16:10:12 -0500559 protected class PhysicsPropertyAnimator {
560 /** The view whose properties this animator animates. */
561 private View mView;
Joshua Tsuji442b6272019-02-08 13:23:43 -0500562
Joshua Tsujic1108432019-02-22 16:10:12 -0500563 /** Start velocity to use for all property animations. */
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400564 private float mDefaultStartVelocity = -Float.MAX_VALUE;
Joshua Tsujic1108432019-02-22 16:10:12 -0500565
566 /** Start delay to use when start is called. */
567 private long mStartDelay = 0;
568
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400569 /** Damping ratio to use for the animations. */
570 private float mDampingRatio = -1;
571
572 /** Stiffness to use for the animations. */
573 private float mStiffness = -1;
574
Joshua Tsujic1108432019-02-22 16:10:12 -0500575 /** End actions to call when animations for the given property complete. */
576 private Map<DynamicAnimation.ViewProperty, Runnable[]> mEndActionsForProperty =
577 new HashMap<>();
578
579 /**
580 * Start velocities to use for TRANSLATION_X and TRANSLATION_Y, since these are often
581 * provided by VelocityTrackers and differ from each other.
582 */
583 private Map<DynamicAnimation.ViewProperty, Float> mPositionStartVelocities =
584 new HashMap<>();
585
586 /**
587 * End actions to call when both TRANSLATION_X and TRANSLATION_Y animations have completed,
588 * if {@link #position} was used to animate TRANSLATION_X and TRANSLATION_Y simultaneously.
589 */
590 private Runnable[] mPositionEndActions;
591
592 /**
593 * All of the properties that have been set and will animate when {@link #start} is called.
594 */
595 private Map<DynamicAnimation.ViewProperty, Float> mAnimatedProperties = new HashMap<>();
596
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400597 /**
598 * All of the initial property values that have been set. These values will be instantly set
599 * when {@link #start} is called, just before the animation begins.
600 */
601 private Map<DynamicAnimation.ViewProperty, Float> mInitialPropertyValues = new HashMap<>();
602
603 /** The animation controller that last retrieved this animator instance. */
604 private PhysicsAnimationController mAssociatedController;
605
Joshua Tsujic1108432019-02-22 16:10:12 -0500606 protected PhysicsPropertyAnimator(View view) {
607 this.mView = view;
Joshua Tsuji442b6272019-02-08 13:23:43 -0500608 }
609
Joshua Tsujic1108432019-02-22 16:10:12 -0500610 /** Animate a property to the given value, then call the optional end actions. */
611 public PhysicsPropertyAnimator property(
612 DynamicAnimation.ViewProperty property, float value, Runnable... endActions) {
613 mAnimatedProperties.put(property, value);
614 mEndActionsForProperty.put(property, endActions);
615 return this;
616 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500617
Joshua Tsujic1108432019-02-22 16:10:12 -0500618 /** Animate the view's alpha value to the provided value. */
619 public PhysicsPropertyAnimator alpha(float alpha, Runnable... endActions) {
620 return property(DynamicAnimation.ALPHA, alpha, endActions);
621 }
622
623 /** Set the view's alpha value to 'from', then animate it to the given value. */
624 public PhysicsPropertyAnimator alpha(float from, float to, Runnable... endActions) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400625 mInitialPropertyValues.put(DynamicAnimation.ALPHA, from);
Joshua Tsujic1108432019-02-22 16:10:12 -0500626 return alpha(to, endActions);
627 }
628
629 /** Animate the view's translationX value to the provided value. */
630 public PhysicsPropertyAnimator translationX(float translationX, Runnable... endActions) {
631 return property(DynamicAnimation.TRANSLATION_X, translationX, endActions);
632 }
633
634 /** Set the view's translationX value to 'from', then animate it to the given value. */
635 public PhysicsPropertyAnimator translationX(
636 float from, float to, Runnable... endActions) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400637 mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_X, from);
Joshua Tsujic1108432019-02-22 16:10:12 -0500638 return translationX(to, endActions);
639 }
640
641 /** Animate the view's translationY value to the provided value. */
642 public PhysicsPropertyAnimator translationY(float translationY, Runnable... endActions) {
643 return property(DynamicAnimation.TRANSLATION_Y, translationY, endActions);
644 }
645
646 /** Set the view's translationY value to 'from', then animate it to the given value. */
647 public PhysicsPropertyAnimator translationY(
648 float from, float to, Runnable... endActions) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400649 mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_Y, from);
Joshua Tsujic1108432019-02-22 16:10:12 -0500650 return translationY(to, endActions);
651 }
652
653 /**
654 * Animate the view's translationX and translationY values, and call the end actions only
655 * once both TRANSLATION_X and TRANSLATION_Y animations have completed.
656 */
657 public PhysicsPropertyAnimator position(
658 float translationX, float translationY, Runnable... endActions) {
659 mPositionEndActions = endActions;
660 translationX(translationX);
661 return translationY(translationY);
662 }
663
664 /** Animate the view's scaleX value to the provided value. */
665 public PhysicsPropertyAnimator scaleX(float scaleX, Runnable... endActions) {
666 return property(DynamicAnimation.SCALE_X, scaleX, endActions);
667 }
668
669 /** Set the view's scaleX value to 'from', then animate it to the given value. */
670 public PhysicsPropertyAnimator scaleX(float from, float to, Runnable... endActions) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400671 mInitialPropertyValues.put(DynamicAnimation.SCALE_X, from);
Joshua Tsujic1108432019-02-22 16:10:12 -0500672 return scaleX(to, endActions);
673 }
674
675 /** Animate the view's scaleY value to the provided value. */
676 public PhysicsPropertyAnimator scaleY(float scaleY, Runnable... endActions) {
677 return property(DynamicAnimation.SCALE_Y, scaleY, endActions);
678 }
679
680 /** Set the view's scaleY value to 'from', then animate it to the given value. */
681 public PhysicsPropertyAnimator scaleY(float from, float to, Runnable... endActions) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400682 mInitialPropertyValues.put(DynamicAnimation.SCALE_Y, from);
Joshua Tsujic1108432019-02-22 16:10:12 -0500683 return scaleY(to, endActions);
684 }
685
686 /** Set the start velocity to use for all property animations. */
687 public PhysicsPropertyAnimator withStartVelocity(float startVel) {
688 mDefaultStartVelocity = startVel;
689 return this;
690 }
691
692 /**
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400693 * Set the damping ratio to use for this animation. If not supplied, will default to the
694 * value from {@link PhysicsAnimationController#getSpringForce}.
695 */
696 public PhysicsPropertyAnimator withDampingRatio(float dampingRatio) {
697 mDampingRatio = dampingRatio;
698 return this;
699 }
700
701 /**
702 * Set the stiffness to use for this animation. If not supplied, will default to the
703 * value from {@link PhysicsAnimationController#getSpringForce}.
704 */
705 public PhysicsPropertyAnimator withStiffness(float stiffness) {
706 mStiffness = stiffness;
707 return this;
708 }
709
710 /**
Joshua Tsujic1108432019-02-22 16:10:12 -0500711 * Set the start velocities to use for TRANSLATION_X and TRANSLATION_Y animations. This
712 * overrides any value set via {@link #withStartVelocity(float)} for those properties.
713 */
714 public PhysicsPropertyAnimator withPositionStartVelocities(float velX, float velY) {
715 mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_X, velX);
716 mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_Y, velY);
717 return this;
718 }
719
720 /** Set a delay, in milliseconds, before kicking off the animations. */
721 public PhysicsPropertyAnimator withStartDelay(long startDelay) {
722 mStartDelay = startDelay;
723 return this;
724 }
725
726 /**
727 * Start the animations, and call the optional end actions once all animations for every
728 * animated property on every child (including chained animations) have ended.
729 */
730 public void start(Runnable... after) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400731 if (!isActiveController(mAssociatedController)) {
732 Log.w(TAG, "Only the active animation controller is allowed to start animations. "
733 + "Use PhysicsAnimationLayout#setActiveController to set the active "
734 + "animation controller.");
735 return;
736 }
737
Joshua Tsujic1108432019-02-22 16:10:12 -0500738 final Set<DynamicAnimation.ViewProperty> properties = getAnimatedProperties();
739
740 // If there are end actions, set an end listener on the layout for all the properties
741 // we're about to animate.
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400742 if (after != null && after.length > 0) {
Joshua Tsujic1108432019-02-22 16:10:12 -0500743 final DynamicAnimation.ViewProperty[] propertiesArray =
744 properties.toArray(new DynamicAnimation.ViewProperty[0]);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400745 setEndActionForMultipleProperties(() -> {
746 for (Runnable callback : after) {
747 callback.run();
748 }
749 }, propertiesArray);
Joshua Tsujic1108432019-02-22 16:10:12 -0500750 }
751
752 // If we used position-specific end actions, we'll need to listen for both TRANSLATION_X
753 // and TRANSLATION_Y animations ending, and call them once both have finished.
754 if (mPositionEndActions != null) {
755 final SpringAnimation translationXAnim =
756 getAnimationFromView(DynamicAnimation.TRANSLATION_X, mView);
757 final SpringAnimation translationYAnim =
758 getAnimationFromView(DynamicAnimation.TRANSLATION_Y, mView);
759 final Runnable waitForBothXAndY = () -> {
760 if (!translationXAnim.isRunning() && !translationYAnim.isRunning()) {
761 if (mPositionEndActions != null) {
762 for (Runnable callback : mPositionEndActions) {
763 callback.run();
764 }
765 }
766
767 mPositionEndActions = null;
768 }
769 };
770
771 mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_X,
772 new Runnable[]{waitForBothXAndY});
773 mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Y,
774 new Runnable[]{waitForBothXAndY});
775 }
776
777 // Actually start the animations.
778 for (DynamicAnimation.ViewProperty property : properties) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400779 if (mInitialPropertyValues.containsKey(property)) {
780 property.setValue(mView, mInitialPropertyValues.get(property));
781 }
782
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400783 final SpringForce defaultSpringForce = mController.getSpringForce(property, mView);
Joshua Tsujic1108432019-02-22 16:10:12 -0500784 animateValueForChild(
785 property,
786 mView,
787 mAnimatedProperties.get(property),
788 mPositionStartVelocities.getOrDefault(property, mDefaultStartVelocity),
789 mStartDelay,
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400790 mStiffness >= 0 ? mStiffness : defaultSpringForce.getStiffness(),
791 mDampingRatio >= 0 ? mDampingRatio : defaultSpringForce.getDampingRatio(),
Joshua Tsujic1108432019-02-22 16:10:12 -0500792 mEndActionsForProperty.get(property));
793 }
794
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400795 clearAnimator();
Joshua Tsujic1108432019-02-22 16:10:12 -0500796 }
797
798 /** Returns the set of properties that will animate once {@link #start} is called. */
799 protected Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
800 return mAnimatedProperties.keySet();
801 }
802
803 /**
804 * Animates the property of the given child view, then runs the callback provided when the
805 * animation ends.
806 */
807 protected void animateValueForChild(
808 DynamicAnimation.ViewProperty property,
809 View view,
810 float value,
811 float startVel,
812 long startDelay,
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400813 float stiffness,
814 float dampingRatio,
Joshua Tsujic1108432019-02-22 16:10:12 -0500815 Runnable[] afterCallbacks) {
816 if (view != null) {
817 final SpringAnimation animation =
818 (SpringAnimation) view.getTag(getTagIdForProperty(property));
819 if (afterCallbacks != null) {
820 animation.addEndListener(new OneTimeEndListener() {
821 @Override
822 public void onAnimationEnd(DynamicAnimation animation, boolean canceled,
823 float value, float velocity) {
824 super.onAnimationEnd(animation, canceled, value, velocity);
825 for (Runnable runnable : afterCallbacks) {
826 runnable.run();
827 }
828 }
829 });
830 }
831
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400832 final SpringForce animationSpring = animation.getSpring();
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400833
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400834 if (animationSpring == null) {
835 return;
Joshua Tsujic1108432019-02-22 16:10:12 -0500836 }
837
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400838 final Runnable configureAndStartAnimation = () -> {
839 animationSpring.setStiffness(stiffness);
840 animationSpring.setDampingRatio(dampingRatio);
841
842 if (startVel > -Float.MAX_VALUE) {
843 animation.setStartVelocity(startVel);
844 }
845
846 animationSpring.setFinalPosition(value);
847 animation.start();
848 };
849
Joshua Tsujic1108432019-02-22 16:10:12 -0500850 if (startDelay > 0) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400851 postDelayed(configureAndStartAnimation, startDelay);
Joshua Tsujic1108432019-02-22 16:10:12 -0500852 } else {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400853 configureAndStartAnimation.run();
Joshua Tsuji442b6272019-02-08 13:23:43 -0500854 }
855 }
856 }
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400857
858 private void clearAnimator() {
859 mInitialPropertyValues.clear();
860 mAnimatedProperties.clear();
861 mPositionStartVelocities.clear();
862 mDefaultStartVelocity = -Float.MAX_VALUE;
863 mStartDelay = 0;
864 mStiffness = -1;
865 mDampingRatio = -1;
866 mEndActionsForProperty.clear();
867 }
868
869 /**
870 * Sets the controller that last retrieved this animator instance, so that we can prevent
871 * {@link #start} from actually starting animations if called by a non-active controller.
872 */
873 private void setAssociatedController(PhysicsAnimationController controller) {
874 mAssociatedController = controller;
875 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500876 }
Tiger Huang2b210c22019-03-18 21:21:26 +0800877
878 @Override
879 protected boolean canReceivePointerEvents() {
880 return false;
881 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800882}