1 /* 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 17 package com.android.wm.shell.bubbles.animation; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.TimeInterpolator; 23 import android.content.Context; 24 import android.graphics.Path; 25 import android.graphics.PointF; 26 import android.util.FloatProperty; 27 import android.util.Log; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.ViewPropertyAnimator; 31 import android.widget.FrameLayout; 32 33 import androidx.annotation.Nullable; 34 import androidx.dynamicanimation.animation.DynamicAnimation; 35 import androidx.dynamicanimation.animation.SpringAnimation; 36 import androidx.dynamicanimation.animation.SpringForce; 37 38 import com.android.wm.shell.R; 39 40 import java.util.ArrayList; 41 import java.util.HashMap; 42 import java.util.HashSet; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Set; 46 47 /** 48 * Layout that constructs physics-based animations for each of its children, which behave according 49 * to settings provided by a {@link PhysicsAnimationController} instance. 50 * 51 * See physics-animation-layout.md. 52 */ 53 public class PhysicsAnimationLayout extends FrameLayout { 54 private static final String TAG = "Bubbs.PAL"; 55 56 /** 57 * Controls the construction, configuration, and use of the physics animations supplied by this 58 * layout. 59 */ 60 abstract static class PhysicsAnimationController { 61 62 /** Configures a given {@link PhysicsPropertyAnimator} for a view at the given index. */ 63 interface ChildAnimationConfigurator { 64 65 /** 66 * Called to configure the animator for the view at the given index. 67 * 68 * This method should make use of methods such as 69 * {@link PhysicsPropertyAnimator#translationX} and 70 * {@link PhysicsPropertyAnimator#withStartDelay} to configure the animation. 71 * 72 * Implementations should not call {@link PhysicsPropertyAnimator#start}, this will 73 * happen elsewhere after configuration is complete. 74 */ configureAnimationForChildAtIndex(int index, PhysicsPropertyAnimator animation)75 void configureAnimationForChildAtIndex(int index, PhysicsPropertyAnimator animation); 76 } 77 78 /** 79 * Returned by {@link #animationsForChildrenFromIndex} to allow starting multiple animations 80 * on multiple child views at the same time. 81 */ 82 interface MultiAnimationStarter { 83 84 /** 85 * Start all animations and call the given end actions once all animations have 86 * completed. 87 */ startAll(Runnable... endActions)88 void startAll(Runnable... endActions); 89 } 90 91 /** 92 * Constant to return from {@link #getNextAnimationInChain} if the animation should not be 93 * chained at all. 94 */ 95 protected static final int NONE = -1; 96 97 /** Set of properties for which the layout should construct physics animations. */ getAnimatedProperties()98 abstract Set<DynamicAnimation.ViewProperty> getAnimatedProperties(); 99 100 /** 101 * Returns the index of the next animation after the given index in the animation chain, or 102 * {@link #NONE} if it should not be chained, or if the chain should end at the given index. 103 * 104 * If a next index is returned, an update listener will be added to the animation at the 105 * given index that dispatches value updates to the animation at the next index. This 106 * creates a 'following' effect. 107 * 108 * Typical implementations of this method will return either index + 1, or index - 1, to 109 * create forward or backward chains between adjacent child views, but this is not required. 110 */ getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)111 abstract int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index); 112 113 /** 114 * Offsets to be added to the value that chained animations of the given property dispatch 115 * to subsequent child animations. 116 * 117 * This is used for things like maintaining the 'stack' effect in Bubbles, where bubbles 118 * stack off to the left or right side slightly. 119 */ getOffsetForChainedPropertyAnimation( DynamicAnimation.ViewProperty property, int index)120 abstract float getOffsetForChainedPropertyAnimation( 121 DynamicAnimation.ViewProperty property, int index); 122 123 /** 124 * Returns the SpringForce to be used for the given child view's property animation. Despite 125 * these usually being similar or identical across properties and views, {@link SpringForce} 126 * also contains the SpringAnimation's final position, so we have to construct a new one for 127 * each animation rather than using a constant. 128 */ getSpringForce(DynamicAnimation.ViewProperty property, View view)129 abstract SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view); 130 131 /** 132 * Called when a new child is added at the specified index. Controllers can use this 133 * opportunity to animate in the new view. 134 */ onChildAdded(View child, int index)135 abstract void onChildAdded(View child, int index); 136 137 /** 138 * Called with a child view that has been removed from the layout, from the given index. The 139 * passed view has been removed from the layout and added back as a transient view, which 140 * renders normally, but is not part of the normal view hierarchy and will not be considered 141 * by getChildAt() and getChildCount(). 142 * 143 * The controller can perform animations on the child (either manually, or by using 144 * {@link #animationForChild(View)}), and then call finishRemoval when complete. 145 * 146 * finishRemoval must be called by implementations of this method, or transient views will 147 * never be removed. 148 */ onChildRemoved(View child, int index, Runnable finishRemoval)149 abstract void onChildRemoved(View child, int index, Runnable finishRemoval); 150 151 /** Called when a child view has been reordered in the view hierachy. */ onChildReordered(View child, int oldIndex, int newIndex)152 abstract void onChildReordered(View child, int oldIndex, int newIndex); 153 154 /** 155 * Called when the controller is set as the active animation controller for the given 156 * layout. Once active, the controller can start animations using the animator instances 157 * returned by {@link #animationForChild}. 158 * 159 * While all animations started by the previous controller will be cancelled, the new 160 * controller should not make any assumptions about the state of the layout or its children. 161 * Their translation, alpha, scale, etc. values may have been changed by the previous 162 * controller and should be reset here if relevant. 163 */ onActiveControllerForLayout(PhysicsAnimationLayout layout)164 abstract void onActiveControllerForLayout(PhysicsAnimationLayout layout); 165 166 protected PhysicsAnimationLayout mLayout; 167 PhysicsAnimationController()168 PhysicsAnimationController() { } 169 170 /** Whether this controller is the currently active controller for its associated layout. */ isActiveController()171 protected boolean isActiveController() { 172 return mLayout != null && this == mLayout.mController; 173 } 174 setLayout(PhysicsAnimationLayout layout)175 protected void setLayout(PhysicsAnimationLayout layout) { 176 this.mLayout = layout; 177 onActiveControllerForLayout(layout); 178 } 179 getLayout()180 protected PhysicsAnimationLayout getLayout() { 181 return mLayout; 182 } 183 184 /** 185 * Returns a {@link PhysicsPropertyAnimator} instance for the given child view. 186 */ animationForChild(View child)187 protected PhysicsPropertyAnimator animationForChild(View child) { 188 PhysicsPropertyAnimator animator = 189 (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag); 190 191 if (animator == null) { 192 animator = mLayout.new PhysicsPropertyAnimator(child); 193 child.setTag(R.id.physics_animator_tag, animator); 194 } 195 196 animator.clearAnimator(); 197 animator.setAssociatedController(this); 198 199 return animator; 200 } 201 202 /** Returns a {@link PhysicsPropertyAnimator} instance for child at the given index. */ animationForChildAtIndex(int index)203 protected PhysicsPropertyAnimator animationForChildAtIndex(int index) { 204 return animationForChild(mLayout.getChildAt(index)); 205 } 206 207 animationsForChildrenFromIndex( int startIndex, ChildAnimationConfigurator configurator)208 protected MultiAnimationStarter animationsForChildrenFromIndex( 209 int startIndex, ChildAnimationConfigurator configurator) { 210 return animationsForChildrenFromIndex(startIndex, /* fadeChildren= */ false, 211 configurator); 212 } 213 214 /** 215 * Returns a {@link MultiAnimationStarter} whose startAll method will start the physics 216 * animations for all children from startIndex onward. The provided configurator will be 217 * called with each child's {@link PhysicsPropertyAnimator}, where it can set up each 218 * animation appropriately. 219 */ animationsForChildrenFromIndex( int startIndex, boolean fadeChildren, ChildAnimationConfigurator configurator)220 protected MultiAnimationStarter animationsForChildrenFromIndex( 221 int startIndex, boolean fadeChildren, ChildAnimationConfigurator configurator) { 222 final Set<DynamicAnimation.ViewProperty> allAnimatedProperties = new HashSet<>(); 223 final List<PhysicsPropertyAnimator> allChildAnims = new ArrayList<>(); 224 225 // Retrieve the animator for each child, ask the configurator to configure it, then save 226 // it and the properties it chose to animate. 227 for (int i = startIndex; i < mLayout.getChildCount(); i++) { 228 final PhysicsPropertyAnimator anim = fadeChildren 229 ? animationForChildAtIndex(i).alpha(0) 230 : animationForChildAtIndex(i); 231 configurator.configureAnimationForChildAtIndex(i, anim); 232 allAnimatedProperties.addAll(anim.getAnimatedProperties()); 233 allChildAnims.add(anim); 234 } 235 236 // Return a MultiAnimationStarter that will start all of the child animations, and also 237 // add a multiple property end listener to the layout that will call the end action 238 // provided to startAll() once all animations on the animated properties complete. 239 return (endActions) -> { 240 final Runnable runAllEndActions = () -> { 241 for (Runnable action : endActions) { 242 action.run(); 243 } 244 }; 245 246 // If there aren't any children to animate, just run the end actions. 247 if (mLayout.getChildCount() == 0) { 248 runAllEndActions.run(); 249 return; 250 } 251 252 if (endActions != null) { 253 setEndActionForMultipleProperties( 254 runAllEndActions, 255 allAnimatedProperties.toArray( 256 new DynamicAnimation.ViewProperty[0])); 257 } 258 259 for (PhysicsPropertyAnimator childAnim : allChildAnims) { 260 childAnim.start(); 261 } 262 }; 263 } 264 265 /** 266 * Sets an end action that will be run when all child animations for a given property have 267 * stopped running. 268 */ 269 protected void setEndActionForProperty( 270 Runnable action, DynamicAnimation.ViewProperty property) { 271 mLayout.mEndActionForProperty.put(property, action); 272 } 273 274 /** 275 * Sets an end action that will be run when all child animations for all of the given 276 * properties have stopped running. 277 */ 278 protected void setEndActionForMultipleProperties( 279 Runnable action, DynamicAnimation.ViewProperty... properties) { 280 final Runnable checkIfAllFinished = () -> { 281 if (!mLayout.arePropertiesAnimating(properties)) { 282 action.run(); 283 284 for (DynamicAnimation.ViewProperty property : properties) { 285 removeEndActionForProperty(property); 286 } 287 } 288 }; 289 290 for (DynamicAnimation.ViewProperty property : properties) { 291 setEndActionForProperty(checkIfAllFinished, property); 292 } 293 } 294 295 /** 296 * Removes the end listener that would have been called when all child animations for a 297 * given property stopped running. 298 */ 299 protected void removeEndActionForProperty(DynamicAnimation.ViewProperty property) { 300 mLayout.mEndActionForProperty.remove(property); 301 } 302 } 303 304 /** 305 * End actions that are called when every child's animation of the given property has finished. 306 */ 307 protected final HashMap<DynamicAnimation.ViewProperty, Runnable> mEndActionForProperty = 308 new HashMap<>(); 309 310 /** The currently active animation controller. */ 311 @Nullable protected PhysicsAnimationController mController; 312 313 public PhysicsAnimationLayout(Context context) { 314 super(context); 315 } 316 317 /** 318 * Sets the animation controller and constructs or reconfigures the layout's physics animations 319 * to meet the controller's specifications. 320 */ 321 public void setActiveController(PhysicsAnimationController controller) { 322 cancelAllAnimations(); 323 mEndActionForProperty.clear(); 324 325 this.mController = controller; 326 mController.setLayout(this); 327 328 // Set up animations for this controller's animated properties. 329 for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { 330 setUpAnimationsForProperty(property); 331 } 332 } 333 334 @Override 335 public void addView(View child, int index, ViewGroup.LayoutParams params) { 336 addViewInternal(child, index, params, false /* isReorder */); 337 } 338 339 /** Removes the child view immediately. */ 340 public void removeViewNoAnimation(View view) { 341 super.removeView(view); 342 view.setTag(R.id.physics_animator_tag, null); 343 } 344 345 @Override 346 public void removeView(View view) { 347 if (mController != null) { 348 final int index = indexOfChild(view); 349 350 // Remove the view and add it back as a transient view so we can animate it out. 351 super.removeView(view); 352 addTransientView(view, index); 353 354 // Tell the controller to animate this view out, and call the callback when it's 355 // finished. 356 mController.onChildRemoved(view, index, () -> { 357 // The controller says it's done with the transient view, cancel animations in case 358 // any are still running and then remove it. 359 cancelAnimationsOnView(view); 360 removeTransientView(view); 361 }); 362 } else { 363 // Without a controller, nobody will animate this view out, so it gets an unceremonious 364 // departure. 365 super.removeView(view); 366 } 367 } 368 369 @Override 370 public void removeViewAt(int index) { 371 removeView(getChildAt(index)); 372 } 373 374 /** Immediately re-orders the view to the given index. */ 375 public void reorderView(View view, int index) { 376 if (view == null) { 377 return; 378 } 379 final int oldIndex = indexOfChild(view); 380 381 if (oldIndex == index) return; 382 383 super.removeView(view); 384 if (view.getParent() != null) { 385 // View still has a parent. This could have been added as a transient view. 386 // Remove it from transient views. 387 super.removeTransientView(view); 388 } 389 addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */); 390 391 if (mController != null) { 392 mController.onChildReordered(view, oldIndex, index); 393 } 394 } 395 396 /** Checks whether any animations of the given properties are still running. */ 397 public boolean arePropertiesAnimating(DynamicAnimation.ViewProperty... properties) { 398 for (int i = 0; i < getChildCount(); i++) { 399 if (arePropertiesAnimatingOnView(getChildAt(i), properties)) { 400 return true; 401 } 402 } 403 404 return false; 405 } 406 407 /** Checks whether any animations of the given properties are running on the given view. */ 408 public boolean arePropertiesAnimatingOnView( 409 View view, DynamicAnimation.ViewProperty... properties) { 410 final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); 411 for (DynamicAnimation.ViewProperty property : properties) { 412 final SpringAnimation animation = getSpringAnimationFromView(property, view); 413 if (animation != null && animation.isRunning()) { 414 return true; 415 } 416 417 // If the target animator is running, its update listener will trigger the translation 418 // physics animations at some point. We should consider the translation properties to be 419 // be animating in this case, even if the physics animations haven't been started yet. 420 final boolean isTranslation = 421 property.equals(DynamicAnimation.TRANSLATION_X) 422 || property.equals(DynamicAnimation.TRANSLATION_Y) 423 || property.equals(DynamicAnimation.TRANSLATION_Z); 424 if (isTranslation && targetAnimator != null && targetAnimator.isRunning()) { 425 return true; 426 } 427 } 428 429 return false; 430 } 431 432 /** Cancels all animations that are running on all child views, for all properties. */ 433 public void cancelAllAnimations() { 434 if (mController == null) { 435 return; 436 } 437 438 cancelAllAnimationsOfProperties( 439 mController.getAnimatedProperties().toArray(new DynamicAnimation.ViewProperty[]{})); 440 } 441 442 /** Cancels all animations that are running on all child views, for the given properties. */ 443 public void cancelAllAnimationsOfProperties(DynamicAnimation.ViewProperty... properties) { 444 if (mController == null) { 445 return; 446 } 447 448 for (int i = 0; i < getChildCount(); i++) { 449 for (DynamicAnimation.ViewProperty property : properties) { 450 final DynamicAnimation anim = getSpringAnimationAtIndex(property, i); 451 if (anim != null) { 452 anim.cancel(); 453 } 454 } 455 final ViewPropertyAnimator anim = getViewPropertyAnimatorFromView(getChildAt(i)); 456 if (anim != null) { 457 anim.cancel(); 458 } 459 } 460 } 461 462 /** Cancels all of the physics animations running on the given view. */ 463 public void cancelAnimationsOnView(View view) { 464 // If present, cancel the target animator so it doesn't restart the translation physics 465 // animations. 466 final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); 467 if (targetAnimator != null) { 468 targetAnimator.cancel(); 469 } 470 471 // Cancel physics animations on the view. 472 for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { 473 final DynamicAnimation animationFromView = getSpringAnimationFromView(property, view); 474 if (animationFromView != null) { 475 animationFromView.cancel(); 476 } 477 } 478 } 479 480 protected boolean isActiveController(PhysicsAnimationController controller) { 481 return mController == controller; 482 } 483 484 /** Whether the first child would be left of center if translated to the given x value. */ 485 protected boolean isFirstChildXLeftOfCenter(float x) { 486 if (getChildCount() > 0) { 487 return x + (getChildAt(0).getWidth() / 2) < getWidth() / 2; 488 } else { 489 return false; // If there's no first child, really anything is correct, right? 490 } 491 } 492 493 /** ViewProperty's toString is useless, this returns a readable name for debug logging. */ 494 protected static String getReadablePropertyName(DynamicAnimation.ViewProperty property) { 495 if (property.equals(DynamicAnimation.TRANSLATION_X)) { 496 return "TRANSLATION_X"; 497 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { 498 return "TRANSLATION_Y"; 499 } else if (property.equals(DynamicAnimation.TRANSLATION_Z)) { 500 return "TRANSLATION_Z"; 501 } else if (property.equals(DynamicAnimation.SCALE_X)) { 502 return "SCALE_X"; 503 } else if (property.equals(DynamicAnimation.SCALE_Y)) { 504 return "SCALE_Y"; 505 } else if (property.equals(DynamicAnimation.ALPHA)) { 506 return "ALPHA"; 507 } else { 508 return "Unknown animation property."; 509 } 510 } 511 512 /** 513 * Adds a view to the layout. If this addition is not the result of a call to 514 * {@link #reorderView}, this will also notify the controller via 515 * {@link PhysicsAnimationController#onChildAdded} and set up animations for the view. 516 */ 517 private void addViewInternal( 518 View child, int index, ViewGroup.LayoutParams params, boolean isReorder) { 519 super.addView(child, index, params); 520 521 // Set up animations for the new view, if the controller is set. If it isn't set, we'll be 522 // setting up animations for all children when setActiveController is called. 523 if (mController != null && !isReorder) { 524 for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { 525 setUpAnimationForChild(property, child); 526 } 527 528 mController.onChildAdded(child, index); 529 } 530 } 531 532 /** 533 * Retrieves the animation of the given property from the view at the given index via the view 534 * tag system. 535 */ 536 @Nullable private SpringAnimation getSpringAnimationAtIndex( 537 DynamicAnimation.ViewProperty property, int index) { 538 return getSpringAnimationFromView(property, getChildAt(index)); 539 } 540 541 /** 542 * Retrieves the spring animation of the given property from the view via the view tag system. 543 */ 544 @Nullable private SpringAnimation getSpringAnimationFromView( 545 DynamicAnimation.ViewProperty property, View view) { 546 if (view == null) return null; 547 return (SpringAnimation) view.getTag(getTagIdForProperty(property)); 548 } 549 550 /** 551 * Retrieves the view property animation of the given property from the view via the view tag 552 * system. 553 */ 554 @Nullable private ViewPropertyAnimator getViewPropertyAnimatorFromView(View view) { 555 if (view == null) return null; 556 return (ViewPropertyAnimator) view.getTag(R.id.reorder_animator_tag); 557 } 558 559 /** Retrieves the target animator from the view via the view tag system. */ 560 @Nullable private ObjectAnimator getTargetAnimatorFromView(View view) { 561 if (view == null) return null; 562 return (ObjectAnimator) view.getTag(R.id.target_animator_tag); 563 } 564 565 /** Sets up SpringAnimations of the given property for each child view in the layout. */ 566 private void setUpAnimationsForProperty(DynamicAnimation.ViewProperty property) { 567 for (int i = 0; i < getChildCount(); i++) { 568 setUpAnimationForChild(property, getChildAt(i)); 569 } 570 } 571 572 /** Constructs a SpringAnimation of the given property for a child view. */ 573 private void setUpAnimationForChild(DynamicAnimation.ViewProperty property, View child) { 574 SpringAnimation newAnim = new SpringAnimation(child, property); 575 newAnim.addUpdateListener((animation, value, velocity) -> { 576 final int indexOfChild = indexOfChild(child); 577 final int nextAnimInChain = mController.getNextAnimationInChain(property, indexOfChild); 578 if (nextAnimInChain == PhysicsAnimationController.NONE || indexOfChild < 0) { 579 return; 580 } 581 582 final float offset = mController.getOffsetForChainedPropertyAnimation(property, 583 nextAnimInChain); 584 if (nextAnimInChain < getChildCount()) { 585 final SpringAnimation nextAnim = getSpringAnimationAtIndex( 586 property, nextAnimInChain); 587 if (nextAnim != null) { 588 nextAnim.animateToFinalPosition(value + offset); 589 } 590 } 591 }); 592 593 newAnim.setSpring(mController.getSpringForce(property, child)); 594 newAnim.addEndListener(new AllAnimationsForPropertyFinishedEndListener(property)); 595 child.setTag(getTagIdForProperty(property), newAnim); 596 } 597 598 /** Return a stable ID to use as a tag key for the given property's animations. */ 599 private int getTagIdForProperty(DynamicAnimation.ViewProperty property) { 600 if (property.equals(DynamicAnimation.TRANSLATION_X)) { 601 return R.id.translation_x_dynamicanimation_tag; 602 } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { 603 return R.id.translation_y_dynamicanimation_tag; 604 } else if (property.equals(DynamicAnimation.TRANSLATION_Z)) { 605 return R.id.translation_z_dynamicanimation_tag; 606 } else if (property.equals(DynamicAnimation.SCALE_X)) { 607 return R.id.scale_x_dynamicanimation_tag; 608 } else if (property.equals(DynamicAnimation.SCALE_Y)) { 609 return R.id.scale_y_dynamicanimation_tag; 610 } else if (property.equals(DynamicAnimation.ALPHA)) { 611 return R.id.alpha_dynamicanimation_tag; 612 } 613 614 return -1; 615 } 616 617 /** 618 * End listener that is added to each individual DynamicAnimation, which dispatches to a single 619 * listener when every other animation of the given property is no longer running. 620 * 621 * This is required since chained DynamicAnimations can stop and start again due to changes in 622 * upstream animations. This means that adding an end listener to just the last animation is not 623 * sufficient. By firing only when every other animation on the property has stopped running, we 624 * ensure that no animation will be restarted after the single end listener is called. 625 */ 626 protected class AllAnimationsForPropertyFinishedEndListener 627 implements DynamicAnimation.OnAnimationEndListener { 628 private DynamicAnimation.ViewProperty mProperty; 629 630 AllAnimationsForPropertyFinishedEndListener(DynamicAnimation.ViewProperty property) { 631 this.mProperty = property; 632 } 633 634 @Override 635 public void onAnimationEnd( 636 DynamicAnimation anim, boolean canceled, float value, float velocity) { 637 if (!arePropertiesAnimating(mProperty)) { 638 if (mEndActionForProperty.containsKey(mProperty)) { 639 final Runnable callback = mEndActionForProperty.get(mProperty); 640 641 if (callback != null) { 642 callback.run(); 643 } 644 } 645 } 646 } 647 } 648 649 /** 650 * Animator class returned by {@link PhysicsAnimationController#animationForChild}, to allow 651 * controllers to animate child views using physics animations. 652 * 653 * See docs/physics-animation-layout.md for documentation and examples. 654 */ 655 protected class PhysicsPropertyAnimator { 656 /** The view whose properties this animator animates. */ 657 private View mView; 658 659 /** Start velocity to use for all property animations. */ 660 private float mDefaultStartVelocity = -Float.MAX_VALUE; 661 662 /** Start delay to use when start is called. */ 663 private long mStartDelay = 0; 664 665 /** Damping ratio to use for the animations. */ 666 private float mDampingRatio = -1; 667 668 /** Stiffness to use for the animations. */ 669 private float mStiffness = -1; 670 671 /** End actions to call when animations for the given property complete. */ 672 private Map<DynamicAnimation.ViewProperty, Runnable[]> mEndActionsForProperty = 673 new HashMap<>(); 674 675 /** 676 * Start velocities to use for TRANSLATION_X and TRANSLATION_Y, since these are often 677 * provided by VelocityTrackers and differ from each other. 678 */ 679 private Map<DynamicAnimation.ViewProperty, Float> mPositionStartVelocities = 680 new HashMap<>(); 681 682 /** 683 * End actions to call when both TRANSLATION_X and TRANSLATION_Y animations have completed, 684 * if {@link #position} was used to animate TRANSLATION_X and TRANSLATION_Y simultaneously. 685 */ 686 @Nullable private Runnable[] mPositionEndActions; 687 688 /** 689 * All of the properties that have been set and will animate when {@link #start} is called. 690 */ 691 private Map<DynamicAnimation.ViewProperty, Float> mAnimatedProperties = new HashMap<>(); 692 693 /** 694 * All of the initial property values that have been set. These values will be instantly set 695 * when {@link #start} is called, just before the animation begins. 696 */ 697 private Map<DynamicAnimation.ViewProperty, Float> mInitialPropertyValues = new HashMap<>(); 698 699 /** The animation controller that last retrieved this animator instance. */ 700 private PhysicsAnimationController mAssociatedController; 701 702 /** 703 * Animator used to traverse the path provided to {@link #followAnimatedTargetAlongPath}. As 704 * the path is traversed, the view's translation spring animation final positions are 705 * updated such that the view 'follows' the current position on the path. 706 */ 707 @Nullable private ObjectAnimator mPathAnimator; 708 709 /** Current position on the path. This is animated by {@link #mPathAnimator}. */ 710 private PointF mCurrentPointOnPath = new PointF(); 711 712 /** 713 * FloatProperty instances that can be passed to {@link ObjectAnimator} to animate the value 714 * of {@link #mCurrentPointOnPath}. 715 */ 716 private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathXProperty = 717 new FloatProperty<PhysicsPropertyAnimator>("PathX") { 718 @Override 719 public void setValue(PhysicsPropertyAnimator object, float value) { 720 mCurrentPointOnPath.x = value; 721 } 722 723 @Override 724 public Float get(PhysicsPropertyAnimator object) { 725 return mCurrentPointOnPath.x; 726 } 727 }; 728 729 private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathYProperty = 730 new FloatProperty<PhysicsPropertyAnimator>("PathY") { 731 @Override 732 public void setValue(PhysicsPropertyAnimator object, float value) { 733 mCurrentPointOnPath.y = value; 734 } 735 736 @Override 737 public Float get(PhysicsPropertyAnimator object) { 738 return mCurrentPointOnPath.y; 739 } 740 }; 741 742 protected PhysicsPropertyAnimator(View view) { 743 this.mView = view; 744 } 745 746 /** Animate a property to the given value, then call the optional end actions. */ 747 public PhysicsPropertyAnimator property( 748 DynamicAnimation.ViewProperty property, float value, Runnable... endActions) { 749 mAnimatedProperties.put(property, value); 750 mEndActionsForProperty.put(property, endActions); 751 return this; 752 } 753 754 /** Animate the view's alpha value to the provided value. */ 755 public PhysicsPropertyAnimator alpha(float alpha, Runnable... endActions) { 756 return property(DynamicAnimation.ALPHA, alpha, endActions); 757 } 758 759 /** Set the view's alpha value to 'from', then animate it to the given value. */ 760 public PhysicsPropertyAnimator alpha(float from, float to, Runnable... endActions) { 761 mInitialPropertyValues.put(DynamicAnimation.ALPHA, from); 762 return alpha(to, endActions); 763 } 764 765 /** Animate the view's translationX value to the provided value. */ 766 public PhysicsPropertyAnimator translationX(float translationX, Runnable... endActions) { 767 mPathAnimator = null; // We aren't using the path anymore if we're translating. 768 return property(DynamicAnimation.TRANSLATION_X, translationX, endActions); 769 } 770 771 /** Animate the view's translationZ value to the provided value. */ 772 public PhysicsPropertyAnimator translationZ(float translationZ, Runnable... endActions) { 773 mPathAnimator = null; // We aren't using the path anymore if we're translating. 774 return property(DynamicAnimation.TRANSLATION_Z, translationZ, endActions); 775 } 776 777 /** Set the view's translationX value to 'from', then animate it to the given value. */ 778 public PhysicsPropertyAnimator translationX( 779 float from, float to, Runnable... endActions) { 780 mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_X, from); 781 return translationX(to, endActions); 782 } 783 784 /** Animate the view's translationY value to the provided value. */ 785 public PhysicsPropertyAnimator translationY(float translationY, Runnable... endActions) { 786 mPathAnimator = null; // We aren't using the path anymore if we're translating. 787 return property(DynamicAnimation.TRANSLATION_Y, translationY, endActions); 788 } 789 790 /** Set the view's translationY value to 'from', then animate it to the given value. */ 791 public PhysicsPropertyAnimator translationY( 792 float from, float to, Runnable... endActions) { 793 mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_Y, from); 794 return translationY(to, endActions); 795 } 796 797 /** 798 * Animate the view's translationX and translationY values, and call the end actions only 799 * once both TRANSLATION_X, TRANSLATION_Y and TRANSLATION_Z animations have completed. 800 */ 801 public PhysicsPropertyAnimator position(float translationX, float translationY, 802 float translationZ, Runnable... endActions) { 803 mPositionEndActions = endActions; 804 translationX(translationX); 805 translationY(translationY); 806 return translationZ(translationZ); 807 } 808 809 /** 810 * Animates a 'target' point that moves along the given path, using the provided duration 811 * and interpolator to animate the target. The view itself is animated using physics-based 812 * animations, whose final positions are updated to the target position as it animates. This 813 * results in the view 'following' the target in a realistic way. 814 * 815 * This method will override earlier calls to {@link #translationX}, {@link #translationY}, 816 * or {@link #position}, ultimately animating the view's position to the final point on the 817 * given path. 818 * 819 * @param pathAnimEndActions End actions to run after the animator that moves the target 820 * along the path ends. The views following the target may still 821 * be moving. 822 */ 823 public PhysicsPropertyAnimator followAnimatedTargetAlongPath( 824 Path path, 825 int targetAnimDuration, 826 TimeInterpolator targetAnimInterpolator, 827 Runnable... pathAnimEndActions) { 828 if (mPathAnimator != null) { 829 mPathAnimator.cancel(); 830 } 831 832 mPathAnimator = ObjectAnimator.ofFloat( 833 this, mCurrentPointOnPathXProperty, mCurrentPointOnPathYProperty, path); 834 835 if (pathAnimEndActions != null) { 836 mPathAnimator.addListener(new AnimatorListenerAdapter() { 837 @Override 838 public void onAnimationEnd(Animator animation) { 839 for (Runnable action : pathAnimEndActions) { 840 if (action != null) { 841 action.run(); 842 } 843 } 844 } 845 }); 846 } 847 848 mPathAnimator.setDuration(targetAnimDuration); 849 mPathAnimator.setInterpolator(targetAnimInterpolator); 850 851 // Remove translation related values since we're going to ignore them and follow the 852 // path instead. 853 clearTranslationValues(); 854 return this; 855 } 856 857 private void clearTranslationValues() { 858 mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_X); 859 mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Y); 860 mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Z); 861 mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_X); 862 mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Y); 863 mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Z); 864 mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_X); 865 mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Y); 866 mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Z); 867 } 868 869 /** Animate the view's scaleX value to the provided value. */ 870 public PhysicsPropertyAnimator scaleX(float scaleX, Runnable... endActions) { 871 return property(DynamicAnimation.SCALE_X, scaleX, endActions); 872 } 873 874 /** Set the view's scaleX value to 'from', then animate it to the given value. */ 875 public PhysicsPropertyAnimator scaleX(float from, float to, Runnable... endActions) { 876 mInitialPropertyValues.put(DynamicAnimation.SCALE_X, from); 877 return scaleX(to, endActions); 878 } 879 880 /** Animate the view's scaleY value to the provided value. */ 881 public PhysicsPropertyAnimator scaleY(float scaleY, Runnable... endActions) { 882 return property(DynamicAnimation.SCALE_Y, scaleY, endActions); 883 } 884 885 /** Set the view's scaleY value to 'from', then animate it to the given value. */ 886 public PhysicsPropertyAnimator scaleY(float from, float to, Runnable... endActions) { 887 mInitialPropertyValues.put(DynamicAnimation.SCALE_Y, from); 888 return scaleY(to, endActions); 889 } 890 891 /** Set the start velocity to use for all property animations. */ 892 public PhysicsPropertyAnimator withStartVelocity(float startVel) { 893 mDefaultStartVelocity = startVel; 894 return this; 895 } 896 897 /** 898 * Set the damping ratio to use for this animation. If not supplied, will default to the 899 * value from {@link PhysicsAnimationController#getSpringForce}. 900 */ 901 public PhysicsPropertyAnimator withDampingRatio(float dampingRatio) { 902 mDampingRatio = dampingRatio; 903 return this; 904 } 905 906 /** 907 * Set the stiffness to use for this animation. If not supplied, will default to the 908 * value from {@link PhysicsAnimationController#getSpringForce}. 909 */ 910 public PhysicsPropertyAnimator withStiffness(float stiffness) { 911 mStiffness = stiffness; 912 return this; 913 } 914 915 /** 916 * Set the start velocities to use for TRANSLATION_X and TRANSLATION_Y animations. This 917 * overrides any value set via {@link #withStartVelocity(float)} for those properties. 918 */ 919 public PhysicsPropertyAnimator withPositionStartVelocities(float velX, float velY) { 920 mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_X, velX); 921 mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_Y, velY); 922 return this; 923 } 924 925 /** Set a delay, in milliseconds, before kicking off the animations. */ 926 public PhysicsPropertyAnimator withStartDelay(long startDelay) { 927 mStartDelay = startDelay; 928 return this; 929 } 930 931 /** 932 * Start the animations, and call the optional end actions once all animations for every 933 * animated property on every child (including chained animations) have ended. 934 */ 935 public void start(Runnable... after) { 936 if (!isActiveController(mAssociatedController)) { 937 Log.w(TAG, "Only the active animation controller is allowed to start animations. " 938 + "Use PhysicsAnimationLayout#setActiveController to set the active " 939 + "animation controller."); 940 return; 941 } 942 943 final Set<DynamicAnimation.ViewProperty> properties = getAnimatedProperties(); 944 945 // If there are end actions, set an end listener on the layout for all the properties 946 // we're about to animate. 947 if (after != null && after.length > 0) { 948 final DynamicAnimation.ViewProperty[] propertiesArray = 949 properties.toArray(new DynamicAnimation.ViewProperty[0]); 950 mAssociatedController.setEndActionForMultipleProperties(() -> { 951 for (Runnable callback : after) { 952 callback.run(); 953 } 954 }, propertiesArray); 955 } 956 957 // If we used position-specific end actions, we'll need to listen for TRANSLATION_X 958 // TRANSLATION_Y and TRANSLATION_Z animations ending, and call them once both have 959 // finished. 960 if (mPositionEndActions != null) { 961 final SpringAnimation translationXAnim = 962 getSpringAnimationFromView(DynamicAnimation.TRANSLATION_X, mView); 963 final SpringAnimation translationYAnim = 964 getSpringAnimationFromView(DynamicAnimation.TRANSLATION_Y, mView); 965 final SpringAnimation translationZAnim = 966 getSpringAnimationFromView(DynamicAnimation.TRANSLATION_Z, mView); 967 final Runnable waitForXYZ = () -> { 968 if (!translationXAnim.isRunning() && !translationYAnim.isRunning() 969 && !translationZAnim.isRunning()) { 970 if (mPositionEndActions != null) { 971 for (Runnable callback : mPositionEndActions) { 972 callback.run(); 973 } 974 } 975 976 mPositionEndActions = null; 977 } 978 }; 979 980 mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_X, 981 new Runnable[]{waitForXYZ}); 982 mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Y, 983 new Runnable[]{waitForXYZ}); 984 mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Z, 985 new Runnable[]{waitForXYZ}); 986 } 987 988 if (mPathAnimator != null) { 989 startPathAnimation(); 990 } 991 992 // Actually start the animations. 993 for (DynamicAnimation.ViewProperty property : properties) { 994 // Don't start translation animations if we're using a path animator, the update 995 // listeners added to that animator will take care of that. 996 boolean isTranslationProperty = property.equals(DynamicAnimation.TRANSLATION_X) 997 || property.equals(DynamicAnimation.TRANSLATION_Y) 998 || property.equals(DynamicAnimation.TRANSLATION_Z); 999 if (mPathAnimator != null && isTranslationProperty) { 1000 return; 1001 } 1002 1003 if (mInitialPropertyValues.containsKey(property)) { 1004 property.setValue(mView, mInitialPropertyValues.get(property)); 1005 } 1006 1007 final SpringForce defaultSpringForce = mController.getSpringForce(property, mView); 1008 animateValueForChild( 1009 property, 1010 mView, 1011 mAnimatedProperties.get(property), 1012 mPositionStartVelocities.getOrDefault(property, mDefaultStartVelocity), 1013 mStartDelay, 1014 mStiffness >= 0 ? mStiffness : defaultSpringForce.getStiffness(), 1015 mDampingRatio >= 0 ? mDampingRatio : defaultSpringForce.getDampingRatio(), 1016 mEndActionsForProperty.get(property)); 1017 } 1018 1019 clearAnimator(); 1020 } 1021 1022 /** Returns the set of properties that will animate once {@link #start} is called. */ 1023 protected Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { 1024 final HashSet<DynamicAnimation.ViewProperty> animatedProperties = new HashSet<>( 1025 mAnimatedProperties.keySet()); 1026 1027 // If we're using a path animator, it'll kick off translation animations. 1028 if (mPathAnimator != null) { 1029 animatedProperties.add(DynamicAnimation.TRANSLATION_X); 1030 animatedProperties.add(DynamicAnimation.TRANSLATION_Y); 1031 animatedProperties.add(DynamicAnimation.TRANSLATION_Z); 1032 } 1033 1034 return animatedProperties; 1035 } 1036 1037 /** 1038 * Animates the property of the given child view, then runs the callback provided when the 1039 * animation ends. 1040 */ 1041 protected void animateValueForChild( 1042 DynamicAnimation.ViewProperty property, 1043 View view, 1044 float value, 1045 float startVel, 1046 long startDelay, 1047 float stiffness, 1048 float dampingRatio, 1049 Runnable... afterCallbacks) { 1050 if (view != null) { 1051 final SpringAnimation animation = 1052 (SpringAnimation) view.getTag(getTagIdForProperty(property)); 1053 1054 // If the animation is null, the view was probably removed from the layout before 1055 // the animation started. 1056 if (animation == null) { 1057 return; 1058 } 1059 1060 if (afterCallbacks != null) { 1061 animation.addEndListener(new OneTimeEndListener() { 1062 @Override 1063 public void onAnimationEnd(DynamicAnimation animation, boolean canceled, 1064 float value, float velocity) { 1065 super.onAnimationEnd(animation, canceled, value, velocity); 1066 for (Runnable runnable : afterCallbacks) { 1067 runnable.run(); 1068 } 1069 } 1070 }); 1071 } 1072 1073 final SpringForce animationSpring = animation.getSpring(); 1074 1075 if (animationSpring == null) { 1076 return; 1077 } 1078 1079 final Runnable configureAndStartAnimation = () -> { 1080 animationSpring.setStiffness(stiffness); 1081 animationSpring.setDampingRatio(dampingRatio); 1082 1083 if (startVel > -Float.MAX_VALUE) { 1084 animation.setStartVelocity(startVel); 1085 } 1086 1087 animationSpring.setFinalPosition(value); 1088 animation.start(); 1089 }; 1090 1091 if (startDelay > 0) { 1092 postDelayed(configureAndStartAnimation, startDelay); 1093 } else { 1094 configureAndStartAnimation.run(); 1095 } 1096 } 1097 } 1098 1099 /** 1100 * Updates the final position of a view's animation, without changing any of the animation's 1101 * other settings. Calling this before an initial call to {@link #animateValueForChild} will 1102 * work, but result in unknown values for stiffness, etc. and is not recommended. 1103 */ 1104 private void updateValueForChild( 1105 DynamicAnimation.ViewProperty property, View view, float position) { 1106 if (view != null) { 1107 final SpringAnimation animation = 1108 (SpringAnimation) view.getTag(getTagIdForProperty(property)); 1109 1110 if (animation == null) { 1111 return; 1112 } 1113 1114 final SpringForce animationSpring = animation.getSpring(); 1115 1116 if (animationSpring == null) { 1117 return; 1118 } 1119 1120 animationSpring.setFinalPosition(position); 1121 animation.start(); 1122 } 1123 } 1124 1125 /** 1126 * Configures the path animator to respect the settings passed into the animation builder 1127 * and adds update listeners that update the translation physics animations. Then, starts 1128 * the path animation. 1129 */ 1130 protected void startPathAnimation() { 1131 final SpringForce defaultSpringForceX = mController.getSpringForce( 1132 DynamicAnimation.TRANSLATION_X, mView); 1133 final SpringForce defaultSpringForceY = mController.getSpringForce( 1134 DynamicAnimation.TRANSLATION_Y, mView); 1135 1136 if (mStartDelay > 0) { 1137 mPathAnimator.setStartDelay(mStartDelay); 1138 } 1139 1140 final Runnable updatePhysicsAnims = () -> { 1141 updateValueForChild( 1142 DynamicAnimation.TRANSLATION_X, mView, mCurrentPointOnPath.x); 1143 updateValueForChild( 1144 DynamicAnimation.TRANSLATION_Y, mView, mCurrentPointOnPath.y); 1145 }; 1146 1147 mPathAnimator.addUpdateListener(pathAnim -> updatePhysicsAnims.run()); 1148 mPathAnimator.addListener(new AnimatorListenerAdapter() { 1149 @Override 1150 public void onAnimationStart(Animator animation) { 1151 animateValueForChild( 1152 DynamicAnimation.TRANSLATION_X, 1153 mView, 1154 mCurrentPointOnPath.x, 1155 mDefaultStartVelocity, 1156 0 /* startDelay */, 1157 mStiffness >= 0 ? mStiffness : defaultSpringForceX.getStiffness(), 1158 mDampingRatio >= 0 1159 ? mDampingRatio 1160 : defaultSpringForceX.getDampingRatio()); 1161 1162 animateValueForChild( 1163 DynamicAnimation.TRANSLATION_Y, 1164 mView, 1165 mCurrentPointOnPath.y, 1166 mDefaultStartVelocity, 1167 0 /* startDelay */, 1168 mStiffness >= 0 ? mStiffness : defaultSpringForceY.getStiffness(), 1169 mDampingRatio >= 0 1170 ? mDampingRatio 1171 : defaultSpringForceY.getDampingRatio()); 1172 } 1173 1174 @Override 1175 public void onAnimationEnd(Animator animation) { 1176 updatePhysicsAnims.run(); 1177 } 1178 }); 1179 1180 // If there's a target animator saved for the view, make sure it's not running. 1181 final ObjectAnimator targetAnimator = getTargetAnimatorFromView(mView); 1182 if (targetAnimator != null) { 1183 targetAnimator.cancel(); 1184 } 1185 1186 mView.setTag(R.id.target_animator_tag, mPathAnimator); 1187 mPathAnimator.start(); 1188 } 1189 1190 private void clearAnimator() { 1191 mInitialPropertyValues.clear(); 1192 mAnimatedProperties.clear(); 1193 mPositionStartVelocities.clear(); 1194 mDefaultStartVelocity = -Float.MAX_VALUE; 1195 mStartDelay = 0; 1196 mStiffness = -1; 1197 mDampingRatio = -1; 1198 mEndActionsForProperty.clear(); 1199 mPathAnimator = null; 1200 mPositionEndActions = null; 1201 } 1202 1203 /** 1204 * Sets the controller that last retrieved this animator instance, so that we can prevent 1205 * {@link #start} from actually starting animations if called by a non-active controller. 1206 */ 1207 private void setAssociatedController(PhysicsAnimationController controller) { 1208 mAssociatedController = controller; 1209 } 1210 } 1211 } 1212