| /* |
| * 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.content.Context; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.FrameLayout; |
| |
| import androidx.dynamicanimation.animation.DynamicAnimation; |
| import androidx.dynamicanimation.animation.SpringAnimation; |
| import androidx.dynamicanimation.animation.SpringForce; |
| |
| import com.android.systemui.R; |
| |
| import java.util.HashMap; |
| import java.util.HashSet; |
| 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 { |
| |
| /** |
| * 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 #animateValueForChild}), 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); |
| |
| protected PhysicsAnimationLayout mLayout; |
| |
| PhysicsAnimationController() { } |
| |
| protected void setLayout(PhysicsAnimationLayout layout) { |
| this.mLayout = layout; |
| } |
| |
| protected PhysicsAnimationLayout getLayout() { |
| return mLayout; |
| } |
| |
| /** |
| * Sets the child's visibility when it moves beyond or within the limits set by a call to |
| * {@link PhysicsAnimationLayout#setMaxRenderedChildren}. This can be overridden to animate |
| * this transition. |
| */ |
| protected void setChildVisibility(View child, int index, int visibility) { |
| child.setVisibility(visibility); |
| } |
| } |
| |
| /** |
| * End listeners that are called when every child's animation of the given property has |
| * finished. |
| */ |
| protected final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation.OnAnimationEndListener> |
| mEndListenerForProperty = new HashMap<>(); |
| |
| /** Set of currently rendered transient views. */ |
| private final Set<View> mTransientViews = new HashSet<>(); |
| |
| /** The currently active animation controller. */ |
| private PhysicsAnimationController mController; |
| |
| /** |
| * The maximum number of children to render and animate at a time. See |
| * {@link #setMaxRenderedChildren}. |
| */ |
| private int mMaxRenderedChildren = 5; |
| |
| public PhysicsAnimationLayout(Context context) { |
| super(context); |
| } |
| |
| /** |
| * The maximum number of children to render and animate at a time. Any child views added beyond |
| * this limit will be set to {@link View#GONE}. If any animations attempt to run on the view, |
| * the corresponding property will be set with no animation. |
| */ |
| public void setMaxRenderedChildren(int max) { |
| this.mMaxRenderedChildren = max; |
| } |
| |
| /** |
| * Sets the animation controller and constructs or reconfigures the layout's physics animations |
| * to meet the controller's specifications. |
| */ |
| public void setController(PhysicsAnimationController controller) { |
| cancelAllAnimations(); |
| mEndListenerForProperty.clear(); |
| |
| this.mController = controller; |
| mController.setLayout(this); |
| |
| // Set up animations for this controller's animated properties. |
| for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { |
| setUpAnimationsForProperty(property); |
| } |
| } |
| |
| /** |
| * Sets an end listener that will be called when all child animations for a given property have |
| * stopped running. |
| */ |
| public void setEndListenerForProperty( |
| DynamicAnimation.OnAnimationEndListener listener, |
| DynamicAnimation.ViewProperty property) { |
| mEndListenerForProperty.put(property, listener); |
| } |
| |
| /** |
| * Sets an end listener that will be called whenever any of the given properties' animations |
| * end. For example, setting a listener for TRANSLATION_X and TRANSLATION_Y will result in that |
| * listener being called twice - once when all TRANSLATION_X animations end, and again when all |
| * TRANSLATION_Y animations end. |
| */ |
| public void setEndListenerForProperties( |
| DynamicAnimation.OnAnimationEndListener endListener, |
| DynamicAnimation.ViewProperty... properties) { |
| for (DynamicAnimation.ViewProperty property : properties) { |
| setEndListenerForProperty(endListener, property); |
| } |
| } |
| |
| /** |
| * Removes the end listener that would have been called when all child animations for a given |
| * property stopped running. |
| */ |
| public void removeEndListenerForProperty(DynamicAnimation.ViewProperty property) { |
| mEndListenerForProperty.remove(property); |
| } |
| |
| @Override |
| public void addView(View child, int index, ViewGroup.LayoutParams params) { |
| 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 setController is called. |
| if (mController != null) { |
| for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { |
| setUpAnimationForChild(property, child, index); |
| } |
| |
| mController.onChildAdded(child, index); |
| } |
| |
| setChildrenVisibility(); |
| } |
| |
| @Override |
| public void removeView(View view) { |
| removeViewAndThen(view, /* callback */ null); |
| } |
| |
| @Override |
| public void addTransientView(View view, int index) { |
| super.addTransientView(view, index); |
| mTransientViews.add(view); |
| } |
| |
| @Override |
| public void removeTransientView(View view) { |
| super.removeTransientView(view); |
| mTransientViews.remove(view); |
| } |
| |
| /** Immediately moves the view from wherever it currently is, to the given index. */ |
| public void moveViewTo(View view, int index) { |
| super.removeView(view); |
| addView(view, index); |
| } |
| |
| /** |
| * Let the controller know that this view should be removed, and then call the callback once the |
| * controller has finished any removal animations and the view has actually been removed. |
| */ |
| public void removeViewAndThen(View view, Runnable callback) { |
| 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); |
| |
| setChildrenVisibility(); |
| |
| // Tell the controller to animate this view out, and call the callback when it's |
| // finished. |
| mController.onChildRemoved(view, index, () -> { |
| // Done animating, remove the transient view. |
| removeTransientView(view); |
| |
| if (callback != null) { |
| callback.run(); |
| } |
| }); |
| } else { |
| // Without a controller, nobody will animate this view out, so it gets an unceremonious |
| // departure. |
| super.removeView(view); |
| |
| if (callback != null) { |
| callback.run(); |
| } |
| } |
| } |
| |
| /** Checks whether any animations of the given properties are still running. */ |
| public boolean arePropertiesAnimating(DynamicAnimation.ViewProperty... properties) { |
| for (int i = 0; i < getChildCount(); i++) { |
| for (DynamicAnimation.ViewProperty property : properties) { |
| if (getAnimationAtIndex(property, i).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; |
| } |
| |
| for (int i = 0; i < getChildCount(); i++) { |
| for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { |
| getAnimationAtIndex(property, i).cancel(); |
| } |
| } |
| } |
| |
| /** Cancels all of the physics animations running on the given view. */ |
| public void cancelAnimationsOnView(View view) { |
| for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { |
| getAnimationFromView(property, view).cancel(); |
| } |
| } |
| |
| /** |
| * 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, |
| Runnable after) { |
| if (view != null) { |
| final SpringAnimation animation = |
| (SpringAnimation) view.getTag(getTagIdForProperty(property)); |
| if (after != null) { |
| animation.addEndListener(new OneTimeEndListener() { |
| @Override |
| public void onAnimationEnd(DynamicAnimation animation, boolean canceled, |
| float value, float velocity) { |
| super.onAnimationEnd(animation, canceled, value, velocity); |
| after.run(); |
| } |
| }); |
| } |
| |
| // Set the start velocity if it's something other than the not-set value. |
| if (startVel != Float.MAX_VALUE) { |
| animation.setStartVelocity(startVel); |
| } |
| |
| animation.animateToFinalPosition(value); |
| } |
| } |
| |
| protected void animateValueForChild( |
| DynamicAnimation.ViewProperty property, |
| View view, |
| float value, |
| Runnable after) { |
| animateValueForChild(property, view, value, Float.MAX_VALUE, after); |
| } |
| |
| protected void animateValueForChild( |
| DynamicAnimation.ViewProperty property, |
| View view, |
| float value) { |
| animateValueForChild(property, view, value, Float.MAX_VALUE, /* after */ null); |
| } |
| |
| protected void animateValueForChild( |
| DynamicAnimation.ViewProperty property, |
| View view, |
| float value, |
| float startVel) { |
| animateValueForChild(property, view, value, startVel, /* after */ null); |
| } |
| |
| /** |
| * Animates the property of the child at the given index to the given value, then runs the |
| * callback provided when the animation ends. |
| */ |
| protected void animateValueForChildAtIndex( |
| DynamicAnimation.ViewProperty property, |
| int index, |
| float value, |
| float startVel, |
| Runnable after) { |
| animateValueForChild(property, getChildAt(index), value, startVel, after); |
| } |
| |
| /** Shortcut to animate a value with a callback, but no start velocity. */ |
| protected void animateValueForChildAtIndex( |
| DynamicAnimation.ViewProperty property, |
| int index, |
| float value, |
| Runnable after) { |
| animateValueForChildAtIndex(property, index, value, Float.MAX_VALUE, after); |
| } |
| |
| /** Shortcut to animate a value with a start velocity, but no callback. */ |
| protected void animateValueForChildAtIndex( |
| DynamicAnimation.ViewProperty property, |
| int index, |
| float value, |
| float startVel) { |
| animateValueForChildAtIndex(property, index, value, startVel, /* callback */ null); |
| } |
| |
| /** Shortcut to animate a value without changing the velocity or providing a callback. */ |
| protected void animateValueForChildAtIndex( |
| DynamicAnimation.ViewProperty property, |
| int index, |
| float value) { |
| animateValueForChildAtIndex(property, index, value, Float.MAX_VALUE, /* callback */ null); |
| } |
| |
| /** Shortcut to animate a child view's TRANSLATION_X and TRANSLATION_Y values. */ |
| protected void animatePositionForChildAtIndex(int index, float x, float y) { |
| animateValueForChildAtIndex(DynamicAnimation.TRANSLATION_X, index, x); |
| animateValueForChildAtIndex(DynamicAnimation.TRANSLATION_Y, index, y); |
| } |
| |
| /** 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."; |
| } |
| } |
| |
| /** |
| * 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)); |
| } |
| |
| /** 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 nextAnimInChain = |
| mController.getNextAnimationInChain(property, indexOfChild(child)); |
| |
| // If the controller doesn't want us to chain, or if we're a transient view in the |
| // process of being removed, don't chain. |
| if (nextAnimInChain == PhysicsAnimationController.NONE |
| || mTransientViews.contains(child)) { |
| return; |
| } |
| |
| final int animIndex = indexOfChild(child); |
| final float offset = |
| mController.getOffsetForChainedPropertyAnimation(property); |
| |
| // If this property's animations should be chained, then check to see if there is a |
| // subsequent animation within the rendering limit, and if so, tell it to animate to |
| // this animation's new value (plus the offset). |
| if (nextAnimInChain < Math.min(getChildCount(), mMaxRenderedChildren)) { |
| getAnimationAtIndex(property, animIndex + 1) |
| .animateToFinalPosition(value + offset); |
| } else if (nextAnimInChain < getChildCount()) { |
| // If the next child view is not rendered, update the property directly without |
| // animating it, so that the view is still in the correct state if it later |
| // becomes visible. |
| for (int i = nextAnimInChain; i < getChildCount(); i++) { |
| // 'value' here is the value of the last child within the rendering limit, |
| // not the first child's value - so we want to subtract the last child's |
| // index when calculating the offset. |
| property.setValue(getChildAt(i), value + offset * (i - animIndex)); |
| } |
| } |
| }); |
| |
| newAnim.setSpring(mController.getSpringForce(property, child)); |
| newAnim.addEndListener(new AllAnimationsForPropertyFinishedEndListener(property)); |
| child.setTag(getTagIdForProperty(property), newAnim); |
| } |
| |
| /** Hides children beyond the max rendering count. */ |
| private void setChildrenVisibility() { |
| for (int i = 0; i < getChildCount(); i++) { |
| final int targetVisibility = i < mMaxRenderedChildren ? View.VISIBLE : View.GONE; |
| final View targetView = getChildAt(i); |
| |
| if (targetView.getVisibility() != targetVisibility) { |
| if (mController != null) { |
| mController.setChildVisibility(targetView, i, targetVisibility); |
| } else { |
| targetView.setVisibility(targetVisibility); |
| } |
| } |
| } |
| } |
| |
| /** 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 (mEndListenerForProperty.containsKey(mProperty)) { |
| mEndListenerForProperty.get(mProperty).onAnimationEnd(anim, canceled, value, |
| velocity); |
| } |
| } |
| } |
| } |
| |
| /** |
| * One time end listener that waits for every animation on every given property to finish. At |
| * that point, it calls {@link #onAllAnimationsForPropertiesEnd} and then removes itself as an |
| * end listener from each property. |
| */ |
| public abstract class OneTimeMultiplePropertyEndListener |
| implements DynamicAnimation.OnAnimationEndListener { |
| final DynamicAnimation.ViewProperty[] mViewProperties; |
| |
| OneTimeMultiplePropertyEndListener(DynamicAnimation.ViewProperty... properties) { |
| mViewProperties = properties; |
| } |
| |
| @Override |
| public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, |
| float velocity) { |
| if (!arePropertiesAnimating(mViewProperties)) { |
| onAllAnimationsForPropertiesEnd(); |
| |
| for (DynamicAnimation.ViewProperty property : mViewProperties) { |
| removeEndListenerForProperty(property); |
| } |
| } |
| } |
| |
| /** Called when every animation for every property has finished. */ |
| abstract void onAllAnimationsForPropertiesEnd(); |
| } |
| } |