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