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