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