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