| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.bubbles.animation; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ObjectAnimator; |
| import android.animation.TimeInterpolator; |
| import android.content.Context; |
| import android.graphics.Path; |
| import android.graphics.PointF; |
| import android.util.FloatProperty; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.FrameLayout; |
| |
| import androidx.annotation.Nullable; |
| import androidx.dynamicanimation.animation.DynamicAnimation; |
| import androidx.dynamicanimation.animation.SpringAnimation; |
| import androidx.dynamicanimation.animation.SpringForce; |
| |
| import com.android.systemui.R; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Layout that constructs physics-based animations for each of its children, which behave according |
| * to settings provided by a {@link PhysicsAnimationController} instance. |
| * |
| * See physics-animation-layout.md. |
| */ |
| public class PhysicsAnimationLayout extends FrameLayout { |
| private static final String TAG = "Bubbs.PAL"; |
| |
| /** |
| * Controls the construction, configuration, and use of the physics animations supplied by this |
| * layout. |
| */ |
| abstract static class PhysicsAnimationController { |
| |
| /** Configures a given {@link PhysicsPropertyAnimator} for a view at the given index. */ |
| interface ChildAnimationConfigurator { |
| |
| /** |
| * Called to configure the animator for the view at the given index. |
| * |
| * This method should make use of methods such as |
| * {@link PhysicsPropertyAnimator#translationX} and |
| * {@link PhysicsPropertyAnimator#withStartDelay} to configure the animation. |
| * |
| * Implementations should not call {@link PhysicsPropertyAnimator#start}, this will |
| * happen elsewhere after configuration is complete. |
| */ |
| void configureAnimationForChildAtIndex(int index, PhysicsPropertyAnimator animation); |
| } |
| |
| /** |
| * Returned by {@link #animationsForChildrenFromIndex} to allow starting multiple animations |
| * on multiple child views at the same time. |
| */ |
| interface MultiAnimationStarter { |
| |
| /** |
| * Start all animations and call the given end actions once all animations have |
| * completed. |
| */ |
| void startAll(Runnable... endActions); |
| } |
| |
| /** |
| * Constant to return from {@link #getNextAnimationInChain} if the animation should not be |
| * chained at all. |
| */ |
| protected static final int NONE = -1; |
| |
| /** Set of properties for which the layout should construct physics animations. */ |
| abstract Set<DynamicAnimation.ViewProperty> getAnimatedProperties(); |
| |
| /** |
| * Returns the index of the next animation after the given index in the animation chain, or |
| * {@link #NONE} if it should not be chained, or if the chain should end at the given index. |
| * |
| * If a next index is returned, an update listener will be added to the animation at the |
| * given index that dispatches value updates to the animation at the next index. This |
| * creates a 'following' effect. |
| * |
| * Typical implementations of this method will return either index + 1, or index - 1, to |
| * create forward or backward chains between adjacent child views, but this is not required. |
| */ |
| abstract int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index); |
| |
| /** |
| * Offsets to be added to the value that chained animations of the given property dispatch |
| * to subsequent child animations. |
| * |
| * This is used for things like maintaining the 'stack' effect in Bubbles, where bubbles |
| * stack off to the left or right side slightly. |
| */ |
| abstract float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property); |
| |
| /** |
| * Returns the SpringForce to be used for the given child view's property animation. Despite |
| * these usually being similar or identical across properties and views, {@link SpringForce} |
| * also contains the SpringAnimation's final position, so we have to construct a new one for |
| * each animation rather than using a constant. |
| */ |
| abstract SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view); |
| |
| /** |
| * Called when a new child is added at the specified index. Controllers can use this |
| * opportunity to animate in the new view. |
| */ |
| abstract void onChildAdded(View child, int index); |
| |
| /** |
| * Called with a child view that has been removed from the layout, from the given index. The |
| * passed view has been removed from the layout and added back as a transient view, which |
| * renders normally, but is not part of the normal view hierarchy and will not be considered |
| * by getChildAt() and getChildCount(). |
| * |
| * The controller can perform animations on the child (either manually, or by using |
| * {@link #animationForChild(View)}), and then call finishRemoval when complete. |
| * |
| * finishRemoval must be called by implementations of this method, or transient views will |
| * never be removed. |
| */ |
| abstract void onChildRemoved(View child, int index, Runnable finishRemoval); |
| |
| /** Called when a child view has been reordered in the view hierachy. */ |
| abstract void onChildReordered(View child, int oldIndex, int newIndex); |
| |
| /** |
| * Called when the controller is set as the active animation controller for the given |
| * layout. Once active, the controller can start animations using the animator instances |
| * returned by {@link #animationForChild}. |
| * |
| * While all animations started by the previous controller will be cancelled, the new |
| * controller should not make any assumptions about the state of the layout or its children. |
| * Their translation, alpha, scale, etc. values may have been changed by the previous |
| * controller and should be reset here if relevant. |
| */ |
| abstract void onActiveControllerForLayout(PhysicsAnimationLayout layout); |
| |
| protected PhysicsAnimationLayout mLayout; |
| |
| PhysicsAnimationController() { } |
| |
| /** Whether this controller is the currently active controller for its associated layout. */ |
| protected boolean isActiveController() { |
| return mLayout != null && this == mLayout.mController; |
| } |
| |
| protected void setLayout(PhysicsAnimationLayout layout) { |
| this.mLayout = layout; |
| onActiveControllerForLayout(layout); |
| } |
| |
| protected PhysicsAnimationLayout getLayout() { |
| return mLayout; |
| } |
| |
| /** |
| * Returns a {@link PhysicsPropertyAnimator} instance for the given child view. |
| */ |
| protected PhysicsPropertyAnimator animationForChild(View child) { |
| PhysicsPropertyAnimator animator = |
| (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag); |
| |
| if (animator == null) { |
| animator = mLayout.new PhysicsPropertyAnimator(child); |
| child.setTag(R.id.physics_animator_tag, animator); |
| } |
| |
| animator.clearAnimator(); |
| animator.setAssociatedController(this); |
| |
| return animator; |
| } |
| |
| /** Returns a {@link PhysicsPropertyAnimator} instance for child at the given index. */ |
| protected PhysicsPropertyAnimator animationForChildAtIndex(int index) { |
| return animationForChild(mLayout.getChildAt(index)); |
| } |
| |
| /** |
| * Returns a {@link MultiAnimationStarter} whose startAll method will start the physics |
| * animations for all children from startIndex onward. The provided configurator will be |
| * called with each child's {@link PhysicsPropertyAnimator}, where it can set up each |
| * animation appropriately. |
| */ |
| protected MultiAnimationStarter animationsForChildrenFromIndex( |
| int startIndex, ChildAnimationConfigurator configurator) { |
| final Set<DynamicAnimation.ViewProperty> allAnimatedProperties = new HashSet<>(); |
| final List<PhysicsPropertyAnimator> allChildAnims = new ArrayList<>(); |
| |
| // Retrieve the animator for each child, ask the configurator to configure it, then save |
| // it and the properties it chose to animate. |
| for (int i = startIndex; i < mLayout.getChildCount(); i++) { |
| final PhysicsPropertyAnimator anim = animationForChildAtIndex(i); |
| configurator.configureAnimationForChildAtIndex(i, anim); |
| allAnimatedProperties.addAll(anim.getAnimatedProperties()); |
| allChildAnims.add(anim); |
| } |
| |
| // Return a MultiAnimationStarter that will start all of the child animations, and also |
| // add a multiple property end listener to the layout that will call the end action |
| // provided to startAll() once all animations on the animated properties complete. |
| return (endActions) -> { |
| final Runnable runAllEndActions = () -> { |
| for (Runnable action : endActions) { |
| action.run(); |
| } |
| }; |
| |
| // If there aren't any children to animate, just run the end actions. |
| if (mLayout.getChildCount() == 0) { |
| runAllEndActions.run(); |
| return; |
| } |
| |
| if (endActions != null) { |
| setEndActionForMultipleProperties( |
| runAllEndActions, |
| allAnimatedProperties.toArray( |
| new DynamicAnimation.ViewProperty[0])); |
| } |
| |
| for (PhysicsPropertyAnimator childAnim : allChildAnims) { |
| childAnim.start(); |
| } |
| }; |
| } |
| |
| /** |
| * Sets an end action that will be run when all child animations for a given property have |
| * stopped running. |
| */ |
| protected void setEndActionForProperty( |
| Runnable action, DynamicAnimation.ViewProperty property) { |
| mLayout.mEndActionForProperty.put(property, action); |
| } |
| |
| /** |
| * Sets an end action that will be run when all child animations for all of the given |
| * properties have stopped running. |
| */ |
| protected void setEndActionForMultipleProperties( |
| Runnable action, DynamicAnimation.ViewProperty... properties) { |
| final Runnable checkIfAllFinished = () -> { |
| if (!mLayout.arePropertiesAnimating(properties)) { |
| action.run(); |
| |
| for (DynamicAnimation.ViewProperty property : properties) { |
| removeEndActionForProperty(property); |
| } |
| } |
| }; |
| |
| for (DynamicAnimation.ViewProperty property : properties) { |
| setEndActionForProperty(checkIfAllFinished, property); |
| } |
| } |
| |
| /** |
| * Removes the end listener that would have been called when all child animations for a |
| * given property stopped running. |
| */ |
| protected void removeEndActionForProperty(DynamicAnimation.ViewProperty property) { |
| mLayout.mEndActionForProperty.remove(property); |
| } |
| } |
| |
| /** |
| * End actions that are called when every child's animation of the given property has finished. |
| */ |
| protected final HashMap<DynamicAnimation.ViewProperty, Runnable> mEndActionForProperty = |
| new HashMap<>(); |
| |
| /** The currently active animation controller. */ |
| @Nullable protected PhysicsAnimationController mController; |
| |
| public PhysicsAnimationLayout(Context context) { |
| super(context); |
| } |
| |
| /** |
| * Sets the animation controller and constructs or reconfigures the layout's physics animations |
| * to meet the controller's specifications. |
| */ |
| public void setActiveController(PhysicsAnimationController controller) { |
| cancelAllAnimations(); |
| mEndActionForProperty.clear(); |
| |
| this.mController = controller; |
| mController.setLayout(this); |
| |
| // Set up animations for this controller's animated properties. |
| for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { |
| setUpAnimationsForProperty(property); |
| } |
| } |
| |
| @Override |
| public void addView(View child, int index, ViewGroup.LayoutParams params) { |
| addViewInternal(child, index, params, false /* isReorder */); |
| } |
| |
| @Override |
| public void removeView(View view) { |
| if (mController != null) { |
| final int index = indexOfChild(view); |
| |
| // Remove the view and add it back as a transient view so we can animate it out. |
| super.removeView(view); |
| addTransientView(view, index); |
| |
| // Tell the controller to animate this view out, and call the callback when it's |
| // finished. |
| mController.onChildRemoved(view, index, () -> { |
| // The controller says it's done with the transient view, cancel animations in case |
| // any are still running and then remove it. |
| cancelAnimationsOnView(view); |
| removeTransientView(view); |
| }); |
| } else { |
| // Without a controller, nobody will animate this view out, so it gets an unceremonious |
| // departure. |
| super.removeView(view); |
| } |
| } |
| |
| @Override |
| public void removeViewAt(int index) { |
| removeView(getChildAt(index)); |
| } |
| |
| /** Immediately re-orders the view to the given index. */ |
| public void reorderView(View view, int index) { |
| final int oldIndex = indexOfChild(view); |
| |
| super.removeView(view); |
| addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */); |
| |
| if (mController != null) { |
| mController.onChildReordered(view, oldIndex, index); |
| } |
| } |
| |
| /** Checks whether any animations of the given properties are still running. */ |
| public boolean arePropertiesAnimating(DynamicAnimation.ViewProperty... properties) { |
| for (int i = 0; i < getChildCount(); i++) { |
| if (arePropertiesAnimatingOnView(getChildAt(i), properties)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** Checks whether any animations of the given properties are running on the given view. */ |
| public boolean arePropertiesAnimatingOnView( |
| View view, DynamicAnimation.ViewProperty... properties) { |
| final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); |
| for (DynamicAnimation.ViewProperty property : properties) { |
| final SpringAnimation animation = getAnimationFromView(property, view); |
| if (animation != null && animation.isRunning()) { |
| return true; |
| } |
| |
| // If the target animator is running, its update listener will trigger the translation |
| // physics animations at some point. We should consider the translation properties to be |
| // be animating in this case, even if the physics animations haven't been started yet. |
| final boolean isTranslation = |
| property.equals(DynamicAnimation.TRANSLATION_X) |
| || property.equals(DynamicAnimation.TRANSLATION_Y); |
| if (isTranslation && targetAnimator != null && targetAnimator.isRunning()) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** Cancels all animations that are running on all child views, for all properties. */ |
| public void cancelAllAnimations() { |
| if (mController == null) { |
| return; |
| } |
| |
| cancelAllAnimationsOfProperties( |
| mController.getAnimatedProperties().toArray(new DynamicAnimation.ViewProperty[]{})); |
| } |
| |
| /** Cancels all animations that are running on all child views, for the given properties. */ |
| public void cancelAllAnimationsOfProperties(DynamicAnimation.ViewProperty... properties) { |
| if (mController == null) { |
| return; |
| } |
| |
| for (int i = 0; i < getChildCount(); i++) { |
| for (DynamicAnimation.ViewProperty property : properties) { |
| final DynamicAnimation anim = getAnimationAtIndex(property, i); |
| if (anim != null) { |
| anim.cancel(); |
| } |
| } |
| } |
| } |
| |
| /** Cancels all of the physics animations running on the given view. */ |
| public void cancelAnimationsOnView(View view) { |
| // If present, cancel the target animator so it doesn't restart the translation physics |
| // animations. |
| final ObjectAnimator targetAnimator = getTargetAnimatorFromView(view); |
| if (targetAnimator != null) { |
| targetAnimator.cancel(); |
| } |
| |
| // Cancel physics animations on the view. |
| for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { |
| getAnimationFromView(property, view).cancel(); |
| } |
| } |
| |
| protected boolean isActiveController(PhysicsAnimationController controller) { |
| return mController == controller; |
| } |
| |
| /** Whether the first child would be left of center if translated to the given x value. */ |
| protected boolean isFirstChildXLeftOfCenter(float x) { |
| if (getChildCount() > 0) { |
| return x + (getChildAt(0).getWidth() / 2) < getWidth() / 2; |
| } else { |
| return false; // If there's no first child, really anything is correct, right? |
| } |
| } |
| |
| /** ViewProperty's toString is useless, this returns a readable name for debug logging. */ |
| protected static String getReadablePropertyName(DynamicAnimation.ViewProperty property) { |
| if (property.equals(DynamicAnimation.TRANSLATION_X)) { |
| return "TRANSLATION_X"; |
| } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { |
| return "TRANSLATION_Y"; |
| } else if (property.equals(DynamicAnimation.SCALE_X)) { |
| return "SCALE_X"; |
| } else if (property.equals(DynamicAnimation.SCALE_Y)) { |
| return "SCALE_Y"; |
| } else if (property.equals(DynamicAnimation.ALPHA)) { |
| return "ALPHA"; |
| } else { |
| return "Unknown animation property."; |
| } |
| } |
| |
| /** |
| * Adds a view to the layout. If this addition is not the result of a call to |
| * {@link #reorderView}, this will also notify the controller via |
| * {@link PhysicsAnimationController#onChildAdded} and set up animations for the view. |
| */ |
| private void addViewInternal( |
| View child, int index, ViewGroup.LayoutParams params, boolean isReorder) { |
| super.addView(child, index, params); |
| |
| // Set up animations for the new view, if the controller is set. If it isn't set, we'll be |
| // setting up animations for all children when setActiveController is called. |
| if (mController != null && !isReorder) { |
| for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { |
| setUpAnimationForChild(property, child, index); |
| } |
| |
| mController.onChildAdded(child, index); |
| } |
| } |
| |
| /** |
| * Retrieves the animation of the given property from the view at the given index via the view |
| * tag system. |
| */ |
| private SpringAnimation getAnimationAtIndex( |
| DynamicAnimation.ViewProperty property, int index) { |
| return getAnimationFromView(property, getChildAt(index)); |
| } |
| |
| /** Retrieves the animation of the given property from the view via the view tag system. */ |
| private SpringAnimation getAnimationFromView( |
| DynamicAnimation.ViewProperty property, View view) { |
| return (SpringAnimation) view.getTag(getTagIdForProperty(property)); |
| } |
| |
| /** Retrieves the target animator from the view via the view tag system. */ |
| @Nullable private ObjectAnimator getTargetAnimatorFromView(View view) { |
| return (ObjectAnimator) view.getTag(R.id.target_animator_tag); |
| } |
| |
| /** Sets up SpringAnimations of the given property for each child view in the layout. */ |
| private void setUpAnimationsForProperty(DynamicAnimation.ViewProperty property) { |
| for (int i = 0; i < getChildCount(); i++) { |
| setUpAnimationForChild(property, getChildAt(i), i); |
| } |
| } |
| |
| /** Constructs a SpringAnimation of the given property for a child view. */ |
| private void setUpAnimationForChild( |
| DynamicAnimation.ViewProperty property, View child, int index) { |
| SpringAnimation newAnim = new SpringAnimation(child, property); |
| newAnim.addUpdateListener((animation, value, velocity) -> { |
| final int indexOfChild = indexOfChild(child); |
| final int nextAnimInChain = mController.getNextAnimationInChain(property, indexOfChild); |
| |
| if (nextAnimInChain == PhysicsAnimationController.NONE || indexOfChild < 0) { |
| return; |
| } |
| |
| final float offset = mController.getOffsetForChainedPropertyAnimation(property); |
| if (nextAnimInChain < getChildCount()) { |
| getAnimationAtIndex(property, nextAnimInChain) |
| .animateToFinalPosition(value + offset); |
| } |
| }); |
| |
| newAnim.setSpring(mController.getSpringForce(property, child)); |
| newAnim.addEndListener(new AllAnimationsForPropertyFinishedEndListener(property)); |
| child.setTag(getTagIdForProperty(property), newAnim); |
| } |
| |
| /** Return a stable ID to use as a tag key for the given property's animations. */ |
| private int getTagIdForProperty(DynamicAnimation.ViewProperty property) { |
| if (property.equals(DynamicAnimation.TRANSLATION_X)) { |
| return R.id.translation_x_dynamicanimation_tag; |
| } else if (property.equals(DynamicAnimation.TRANSLATION_Y)) { |
| return R.id.translation_y_dynamicanimation_tag; |
| } else if (property.equals(DynamicAnimation.SCALE_X)) { |
| return R.id.scale_x_dynamicanimation_tag; |
| } else if (property.equals(DynamicAnimation.SCALE_Y)) { |
| return R.id.scale_y_dynamicanimation_tag; |
| } else if (property.equals(DynamicAnimation.ALPHA)) { |
| return R.id.alpha_dynamicanimation_tag; |
| } |
| |
| return -1; |
| } |
| |
| /** |
| * End listener that is added to each individual DynamicAnimation, which dispatches to a single |
| * listener when every other animation of the given property is no longer running. |
| * |
| * This is required since chained DynamicAnimations can stop and start again due to changes in |
| * upstream animations. This means that adding an end listener to just the last animation is not |
| * sufficient. By firing only when every other animation on the property has stopped running, we |
| * ensure that no animation will be restarted after the single end listener is called. |
| */ |
| protected class AllAnimationsForPropertyFinishedEndListener |
| implements DynamicAnimation.OnAnimationEndListener { |
| private DynamicAnimation.ViewProperty mProperty; |
| |
| AllAnimationsForPropertyFinishedEndListener(DynamicAnimation.ViewProperty property) { |
| this.mProperty = property; |
| } |
| |
| @Override |
| public void onAnimationEnd( |
| DynamicAnimation anim, boolean canceled, float value, float velocity) { |
| if (!arePropertiesAnimating(mProperty)) { |
| if (mEndActionForProperty.containsKey(mProperty)) { |
| final Runnable callback = mEndActionForProperty.get(mProperty); |
| |
| if (callback != null) { |
| callback.run(); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Animator class returned by {@link PhysicsAnimationController#animationForChild}, to allow |
| * controllers to animate child views using physics animations. |
| * |
| * See docs/physics-animation-layout.md for documentation and examples. |
| */ |
| protected class PhysicsPropertyAnimator { |
| /** The view whose properties this animator animates. */ |
| private View mView; |
| |
| /** Start velocity to use for all property animations. */ |
| private float mDefaultStartVelocity = -Float.MAX_VALUE; |
| |
| /** Start delay to use when start is called. */ |
| private long mStartDelay = 0; |
| |
| /** Damping ratio to use for the animations. */ |
| private float mDampingRatio = -1; |
| |
| /** Stiffness to use for the animations. */ |
| private float mStiffness = -1; |
| |
| /** End actions to call when animations for the given property complete. */ |
| private Map<DynamicAnimation.ViewProperty, Runnable[]> mEndActionsForProperty = |
| new HashMap<>(); |
| |
| /** |
| * Start velocities to use for TRANSLATION_X and TRANSLATION_Y, since these are often |
| * provided by VelocityTrackers and differ from each other. |
| */ |
| private Map<DynamicAnimation.ViewProperty, Float> mPositionStartVelocities = |
| new HashMap<>(); |
| |
| /** |
| * End actions to call when both TRANSLATION_X and TRANSLATION_Y animations have completed, |
| * if {@link #position} was used to animate TRANSLATION_X and TRANSLATION_Y simultaneously. |
| */ |
| @Nullable private Runnable[] mPositionEndActions; |
| |
| /** |
| * All of the properties that have been set and will animate when {@link #start} is called. |
| */ |
| private Map<DynamicAnimation.ViewProperty, Float> mAnimatedProperties = new HashMap<>(); |
| |
| /** |
| * All of the initial property values that have been set. These values will be instantly set |
| * when {@link #start} is called, just before the animation begins. |
| */ |
| private Map<DynamicAnimation.ViewProperty, Float> mInitialPropertyValues = new HashMap<>(); |
| |
| /** The animation controller that last retrieved this animator instance. */ |
| private PhysicsAnimationController mAssociatedController; |
| |
| /** |
| * Animator used to traverse the path provided to {@link #followAnimatedTargetAlongPath}. As |
| * the path is traversed, the view's translation spring animation final positions are |
| * updated such that the view 'follows' the current position on the path. |
| */ |
| @Nullable private ObjectAnimator mPathAnimator; |
| |
| /** Current position on the path. This is animated by {@link #mPathAnimator}. */ |
| private PointF mCurrentPointOnPath = new PointF(); |
| |
| /** |
| * FloatProperty instances that can be passed to {@link ObjectAnimator} to animate the value |
| * of {@link #mCurrentPointOnPath}. |
| */ |
| private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathXProperty = |
| new FloatProperty<PhysicsPropertyAnimator>("PathX") { |
| @Override |
| public void setValue(PhysicsPropertyAnimator object, float value) { |
| mCurrentPointOnPath.x = value; |
| } |
| |
| @Override |
| public Float get(PhysicsPropertyAnimator object) { |
| return mCurrentPointOnPath.x; |
| } |
| }; |
| |
| private final FloatProperty<PhysicsPropertyAnimator> mCurrentPointOnPathYProperty = |
| new FloatProperty<PhysicsPropertyAnimator>("PathY") { |
| @Override |
| public void setValue(PhysicsPropertyAnimator object, float value) { |
| mCurrentPointOnPath.y = value; |
| } |
| |
| @Override |
| public Float get(PhysicsPropertyAnimator object) { |
| return mCurrentPointOnPath.y; |
| } |
| }; |
| |
| protected PhysicsPropertyAnimator(View view) { |
| this.mView = view; |
| } |
| |
| /** Animate a property to the given value, then call the optional end actions. */ |
| public PhysicsPropertyAnimator property( |
| DynamicAnimation.ViewProperty property, float value, Runnable... endActions) { |
| mAnimatedProperties.put(property, value); |
| mEndActionsForProperty.put(property, endActions); |
| return this; |
| } |
| |
| /** Animate the view's alpha value to the provided value. */ |
| public PhysicsPropertyAnimator alpha(float alpha, Runnable... endActions) { |
| return property(DynamicAnimation.ALPHA, alpha, endActions); |
| } |
| |
| /** Set the view's alpha value to 'from', then animate it to the given value. */ |
| public PhysicsPropertyAnimator alpha(float from, float to, Runnable... endActions) { |
| mInitialPropertyValues.put(DynamicAnimation.ALPHA, from); |
| return alpha(to, endActions); |
| } |
| |
| /** Animate the view's translationX value to the provided value. */ |
| public PhysicsPropertyAnimator translationX(float translationX, Runnable... endActions) { |
| mPathAnimator = null; // We aren't using the path anymore if we're translating. |
| return property(DynamicAnimation.TRANSLATION_X, translationX, endActions); |
| } |
| |
| /** Set the view's translationX value to 'from', then animate it to the given value. */ |
| public PhysicsPropertyAnimator translationX( |
| float from, float to, Runnable... endActions) { |
| mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_X, from); |
| return translationX(to, endActions); |
| } |
| |
| /** Animate the view's translationY value to the provided value. */ |
| public PhysicsPropertyAnimator translationY(float translationY, Runnable... endActions) { |
| mPathAnimator = null; // We aren't using the path anymore if we're translating. |
| return property(DynamicAnimation.TRANSLATION_Y, translationY, endActions); |
| } |
| |
| /** Set the view's translationY value to 'from', then animate it to the given value. */ |
| public PhysicsPropertyAnimator translationY( |
| float from, float to, Runnable... endActions) { |
| mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_Y, from); |
| return translationY(to, endActions); |
| } |
| |
| /** |
| * Animate the view's translationX and translationY values, and call the end actions only |
| * once both TRANSLATION_X and TRANSLATION_Y animations have completed. |
| */ |
| public PhysicsPropertyAnimator position( |
| float translationX, float translationY, Runnable... endActions) { |
| mPositionEndActions = endActions; |
| translationX(translationX); |
| return translationY(translationY); |
| } |
| |
| /** |
| * Animates a 'target' point that moves along the given path, using the provided duration |
| * and interpolator to animate the target. The view itself is animated using physics-based |
| * animations, whose final positions are updated to the target position as it animates. This |
| * results in the view 'following' the target in a realistic way. |
| * |
| * This method will override earlier calls to {@link #translationX}, {@link #translationY}, |
| * or {@link #position}, ultimately animating the view's position to the final point on the |
| * given path. |
| * |
| * Any provided end listeners will be called when the physics-based animations kicked off by |
| * the moving target have completed - not when the target animation completes. |
| */ |
| public PhysicsPropertyAnimator followAnimatedTargetAlongPath( |
| Path path, |
| int targetAnimDuration, |
| TimeInterpolator targetAnimInterpolator, |
| Runnable... endActions) { |
| mPathAnimator = ObjectAnimator.ofFloat( |
| this, mCurrentPointOnPathXProperty, mCurrentPointOnPathYProperty, path); |
| mPathAnimator.setDuration(targetAnimDuration); |
| mPathAnimator.setInterpolator(targetAnimInterpolator); |
| |
| mPositionEndActions = endActions; |
| |
| // Remove translation related values since we're going to ignore them and follow the |
| // path instead. |
| clearTranslationValues(); |
| return this; |
| } |
| |
| private void clearTranslationValues() { |
| mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_X); |
| mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Y); |
| mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_X); |
| mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Y); |
| mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_X); |
| mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Y); |
| } |
| |
| /** Animate the view's scaleX value to the provided value. */ |
| public PhysicsPropertyAnimator scaleX(float scaleX, Runnable... endActions) { |
| return property(DynamicAnimation.SCALE_X, scaleX, endActions); |
| } |
| |
| /** Set the view's scaleX value to 'from', then animate it to the given value. */ |
| public PhysicsPropertyAnimator scaleX(float from, float to, Runnable... endActions) { |
| mInitialPropertyValues.put(DynamicAnimation.SCALE_X, from); |
| return scaleX(to, endActions); |
| } |
| |
| /** Animate the view's scaleY value to the provided value. */ |
| public PhysicsPropertyAnimator scaleY(float scaleY, Runnable... endActions) { |
| return property(DynamicAnimation.SCALE_Y, scaleY, endActions); |
| } |
| |
| /** Set the view's scaleY value to 'from', then animate it to the given value. */ |
| public PhysicsPropertyAnimator scaleY(float from, float to, Runnable... endActions) { |
| mInitialPropertyValues.put(DynamicAnimation.SCALE_Y, from); |
| return scaleY(to, endActions); |
| } |
| |
| /** Set the start velocity to use for all property animations. */ |
| public PhysicsPropertyAnimator withStartVelocity(float startVel) { |
| mDefaultStartVelocity = startVel; |
| return this; |
| } |
| |
| /** |
| * Set the damping ratio to use for this animation. If not supplied, will default to the |
| * value from {@link PhysicsAnimationController#getSpringForce}. |
| */ |
| public PhysicsPropertyAnimator withDampingRatio(float dampingRatio) { |
| mDampingRatio = dampingRatio; |
| return this; |
| } |
| |
| /** |
| * Set the stiffness to use for this animation. If not supplied, will default to the |
| * value from {@link PhysicsAnimationController#getSpringForce}. |
| */ |
| public PhysicsPropertyAnimator withStiffness(float stiffness) { |
| mStiffness = stiffness; |
| return this; |
| } |
| |
| /** |
| * Set the start velocities to use for TRANSLATION_X and TRANSLATION_Y animations. This |
| * overrides any value set via {@link #withStartVelocity(float)} for those properties. |
| */ |
| public PhysicsPropertyAnimator withPositionStartVelocities(float velX, float velY) { |
| mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_X, velX); |
| mPositionStartVelocities.put(DynamicAnimation.TRANSLATION_Y, velY); |
| return this; |
| } |
| |
| /** Set a delay, in milliseconds, before kicking off the animations. */ |
| public PhysicsPropertyAnimator withStartDelay(long startDelay) { |
| mStartDelay = startDelay; |
| return this; |
| } |
| |
| /** |
| * Start the animations, and call the optional end actions once all animations for every |
| * animated property on every child (including chained animations) have ended. |
| */ |
| public void start(Runnable... after) { |
| if (!isActiveController(mAssociatedController)) { |
| Log.w(TAG, "Only the active animation controller is allowed to start animations. " |
| + "Use PhysicsAnimationLayout#setActiveController to set the active " |
| + "animation controller."); |
| return; |
| } |
| |
| final Set<DynamicAnimation.ViewProperty> properties = getAnimatedProperties(); |
| |
| // If there are end actions, set an end listener on the layout for all the properties |
| // we're about to animate. |
| if (after != null && after.length > 0) { |
| final DynamicAnimation.ViewProperty[] propertiesArray = |
| properties.toArray(new DynamicAnimation.ViewProperty[0]); |
| mAssociatedController.setEndActionForMultipleProperties(() -> { |
| for (Runnable callback : after) { |
| callback.run(); |
| } |
| }, propertiesArray); |
| } |
| |
| // If we used position-specific end actions, we'll need to listen for both TRANSLATION_X |
| // and TRANSLATION_Y animations ending, and call them once both have finished. |
| if (mPositionEndActions != null) { |
| final SpringAnimation translationXAnim = |
| getAnimationFromView(DynamicAnimation.TRANSLATION_X, mView); |
| final SpringAnimation translationYAnim = |
| getAnimationFromView(DynamicAnimation.TRANSLATION_Y, mView); |
| final Runnable waitForBothXAndY = () -> { |
| if (!translationXAnim.isRunning() && !translationYAnim.isRunning()) { |
| if (mPositionEndActions != null) { |
| for (Runnable callback : mPositionEndActions) { |
| callback.run(); |
| } |
| } |
| |
| mPositionEndActions = null; |
| } |
| }; |
| |
| mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_X, |
| new Runnable[]{waitForBothXAndY}); |
| mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Y, |
| new Runnable[]{waitForBothXAndY}); |
| } |
| |
| if (mPathAnimator != null) { |
| startPathAnimation(); |
| } |
| |
| // Actually start the animations. |
| for (DynamicAnimation.ViewProperty property : properties) { |
| // Don't start translation animations if we're using a path animator, the update |
| // listeners added to that animator will take care of that. |
| if (mPathAnimator != null |
| && (property.equals(DynamicAnimation.TRANSLATION_X) |
| || property.equals(DynamicAnimation.TRANSLATION_Y))) { |
| return; |
| } |
| |
| if (mInitialPropertyValues.containsKey(property)) { |
| property.setValue(mView, mInitialPropertyValues.get(property)); |
| } |
| |
| final SpringForce defaultSpringForce = mController.getSpringForce(property, mView); |
| animateValueForChild( |
| property, |
| mView, |
| mAnimatedProperties.get(property), |
| mPositionStartVelocities.getOrDefault(property, mDefaultStartVelocity), |
| mStartDelay, |
| mStiffness >= 0 ? mStiffness : defaultSpringForce.getStiffness(), |
| mDampingRatio >= 0 ? mDampingRatio : defaultSpringForce.getDampingRatio(), |
| mEndActionsForProperty.get(property)); |
| } |
| |
| clearAnimator(); |
| } |
| |
| /** Returns the set of properties that will animate once {@link #start} is called. */ |
| protected Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { |
| final HashSet<DynamicAnimation.ViewProperty> animatedProperties = new HashSet<>( |
| mAnimatedProperties.keySet()); |
| |
| // If we're using a path animator, it'll kick off translation animations. |
| if (mPathAnimator != null) { |
| animatedProperties.add(DynamicAnimation.TRANSLATION_X); |
| animatedProperties.add(DynamicAnimation.TRANSLATION_Y); |
| } |
| |
| return animatedProperties; |
| } |
| |
| /** |
| * Animates the property of the given child view, then runs the callback provided when the |
| * animation ends. |
| */ |
| protected void animateValueForChild( |
| DynamicAnimation.ViewProperty property, |
| View view, |
| float value, |
| float startVel, |
| long startDelay, |
| float stiffness, |
| float dampingRatio, |
| Runnable... afterCallbacks) { |
| if (view != null) { |
| final SpringAnimation animation = |
| (SpringAnimation) view.getTag(getTagIdForProperty(property)); |
| |
| // If the animation is null, the view was probably removed from the layout before |
| // the animation started. |
| if (animation == null) { |
| return; |
| } |
| |
| if (afterCallbacks != null) { |
| animation.addEndListener(new OneTimeEndListener() { |
| @Override |
| public void onAnimationEnd(DynamicAnimation animation, boolean canceled, |
| float value, float velocity) { |
| super.onAnimationEnd(animation, canceled, value, velocity); |
| for (Runnable runnable : afterCallbacks) { |
| runnable.run(); |
| } |
| } |
| }); |
| } |
| |
| final SpringForce animationSpring = animation.getSpring(); |
| |
| if (animationSpring == null) { |
| return; |
| } |
| |
| final Runnable configureAndStartAnimation = () -> { |
| animationSpring.setStiffness(stiffness); |
| animationSpring.setDampingRatio(dampingRatio); |
| |
| if (startVel > -Float.MAX_VALUE) { |
| animation.setStartVelocity(startVel); |
| } |
| |
| animationSpring.setFinalPosition(value); |
| animation.start(); |
| }; |
| |
| if (startDelay > 0) { |
| postDelayed(configureAndStartAnimation, startDelay); |
| } else { |
| configureAndStartAnimation.run(); |
| } |
| } |
| } |
| |
| /** |
| * Updates the final position of a view's animation, without changing any of the animation's |
| * other settings. Calling this before an initial call to {@link #animateValueForChild} will |
| * work, but result in unknown values for stiffness, etc. and is not recommended. |
| */ |
| private void updateValueForChild( |
| DynamicAnimation.ViewProperty property, View view, float position) { |
| if (view != null) { |
| final SpringAnimation animation = |
| (SpringAnimation) view.getTag(getTagIdForProperty(property)); |
| final SpringForce animationSpring = animation.getSpring(); |
| |
| if (animationSpring == null) { |
| return; |
| } |
| |
| animationSpring.setFinalPosition(position); |
| animation.start(); |
| } |
| } |
| |
| /** |
| * Configures the path animator to respect the settings passed into the animation builder |
| * and adds update listeners that update the translation physics animations. Then, starts |
| * the path animation. |
| */ |
| protected void startPathAnimation() { |
| final SpringForce defaultSpringForceX = mController.getSpringForce( |
| DynamicAnimation.TRANSLATION_X, mView); |
| final SpringForce defaultSpringForceY = mController.getSpringForce( |
| DynamicAnimation.TRANSLATION_Y, mView); |
| |
| if (mStartDelay > 0) { |
| mPathAnimator.setStartDelay(mStartDelay); |
| } |
| |
| final Runnable updatePhysicsAnims = () -> { |
| updateValueForChild( |
| DynamicAnimation.TRANSLATION_X, mView, mCurrentPointOnPath.x); |
| updateValueForChild( |
| DynamicAnimation.TRANSLATION_Y, mView, mCurrentPointOnPath.y); |
| }; |
| |
| mPathAnimator.addUpdateListener(pathAnim -> updatePhysicsAnims.run()); |
| mPathAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| animateValueForChild( |
| DynamicAnimation.TRANSLATION_X, |
| mView, |
| mCurrentPointOnPath.x, |
| mDefaultStartVelocity, |
| 0 /* startDelay */, |
| mStiffness >= 0 ? mStiffness : defaultSpringForceX.getStiffness(), |
| mDampingRatio >= 0 |
| ? mDampingRatio |
| : defaultSpringForceX.getDampingRatio()); |
| |
| animateValueForChild( |
| DynamicAnimation.TRANSLATION_Y, |
| mView, |
| mCurrentPointOnPath.y, |
| mDefaultStartVelocity, |
| 0 /* startDelay */, |
| mStiffness >= 0 ? mStiffness : defaultSpringForceY.getStiffness(), |
| mDampingRatio >= 0 |
| ? mDampingRatio |
| : defaultSpringForceY.getDampingRatio()); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| updatePhysicsAnims.run(); |
| } |
| }); |
| |
| // If there's a target animator saved for the view, make sure it's not running. |
| final ObjectAnimator targetAnimator = getTargetAnimatorFromView(mView); |
| if (targetAnimator != null) { |
| targetAnimator.cancel(); |
| } |
| |
| mView.setTag(R.id.target_animator_tag, mPathAnimator); |
| mPathAnimator.start(); |
| } |
| |
| private void clearAnimator() { |
| mInitialPropertyValues.clear(); |
| mAnimatedProperties.clear(); |
| mPositionStartVelocities.clear(); |
| mDefaultStartVelocity = -Float.MAX_VALUE; |
| mStartDelay = 0; |
| mStiffness = -1; |
| mDampingRatio = -1; |
| mEndActionsForProperty.clear(); |
| mPathAnimator = null; |
| mPositionEndActions = null; |
| } |
| |
| /** |
| * Sets the controller that last retrieved this animator instance, so that we can prevent |
| * {@link #start} from actually starting animations if called by a non-active controller. |
| */ |
| private void setAssociatedController(PhysicsAnimationController controller) { |
| mAssociatedController = controller; |
| } |
| } |
| |
| @Override |
| protected boolean canReceivePointerEvents() { |
| return false; |
| } |
| } |