Merge "Add AndroidX SpringAnimation library to core." into tm-qpr-dev

This commit is contained in:
Shan Huang
2022-10-18 01:06:42 +00:00
committed by Android (Google) Code Review
6 changed files with 1545 additions and 1 deletions

View File

@@ -432,8 +432,9 @@ public class AnimationHandler {
/**
* Callbacks that receives notifications for animation timing and frame commit timing.
* @hide
*/
interface AnimationFrameCallback {
public interface AnimationFrameCallback {
/**
* Run animation based on the frame time.
* @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time

View File

@@ -0,0 +1,815 @@
/*
* Copyright (C) 2022 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.internal.dynamicanimation.animation;
import android.animation.AnimationHandler;
import android.animation.ValueAnimator;
import android.annotation.FloatRange;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.os.Looper;
import android.util.AndroidRuntimeException;
import android.util.FloatProperty;
import android.view.View;
import java.util.ArrayList;
/**
* This class is the base class of physics-based animations. It manages the animation's
* lifecycle such as {@link #start()} and {@link #cancel()}. This base class also handles the common
* setup for all the subclass animations. For example, DynamicAnimation supports adding
* {@link OnAnimationEndListener} and {@link OnAnimationUpdateListener} so that the important
* animation events can be observed through the callbacks. The start conditions for any subclass of
* DynamicAnimation can be set using {@link #setStartValue(float)} and
* {@link #setStartVelocity(float)}.
*
* @param <T> subclass of DynamicAnimation
*/
public abstract class DynamicAnimation<T extends DynamicAnimation<T>>
implements AnimationHandler.AnimationFrameCallback {
/**
* ViewProperty holds the access of a property of a {@link View}. When an animation is
* created with a {@link ViewProperty} instance, the corresponding property value of the view
* will be updated through this ViewProperty instance.
*/
public abstract static class ViewProperty extends FloatProperty<View> {
private ViewProperty(String name) {
super(name);
}
}
/**
* View's translationX property.
*/
public static final ViewProperty TRANSLATION_X = new ViewProperty("translationX") {
@Override
public void setValue(View view, float value) {
view.setTranslationX(value);
}
@Override
public Float get(View view) {
return view.getTranslationX();
}
};
/**
* View's translationY property.
*/
public static final ViewProperty TRANSLATION_Y = new ViewProperty("translationY") {
@Override
public void setValue(View view, float value) {
view.setTranslationY(value);
}
@Override
public Float get(View view) {
return view.getTranslationY();
}
};
/**
* View's translationZ property.
*/
public static final ViewProperty TRANSLATION_Z = new ViewProperty("translationZ") {
@Override
public void setValue(View view, float value) {
view.setTranslationZ(value);
}
@Override
public Float get(View view) {
return view.getTranslationZ();
}
};
/**
* View's scaleX property.
*/
public static final ViewProperty SCALE_X = new ViewProperty("scaleX") {
@Override
public void setValue(View view, float value) {
view.setScaleX(value);
}
@Override
public Float get(View view) {
return view.getScaleX();
}
};
/**
* View's scaleY property.
*/
public static final ViewProperty SCALE_Y = new ViewProperty("scaleY") {
@Override
public void setValue(View view, float value) {
view.setScaleY(value);
}
@Override
public Float get(View view) {
return view.getScaleY();
}
};
/**
* View's rotation property.
*/
public static final ViewProperty ROTATION = new ViewProperty("rotation") {
@Override
public void setValue(View view, float value) {
view.setRotation(value);
}
@Override
public Float get(View view) {
return view.getRotation();
}
};
/**
* View's rotationX property.
*/
public static final ViewProperty ROTATION_X = new ViewProperty("rotationX") {
@Override
public void setValue(View view, float value) {
view.setRotationX(value);
}
@Override
public Float get(View view) {
return view.getRotationX();
}
};
/**
* View's rotationY property.
*/
public static final ViewProperty ROTATION_Y = new ViewProperty("rotationY") {
@Override
public void setValue(View view, float value) {
view.setRotationY(value);
}
@Override
public Float get(View view) {
return view.getRotationY();
}
};
/**
* View's x property.
*/
public static final ViewProperty X = new ViewProperty("x") {
@Override
public void setValue(View view, float value) {
view.setX(value);
}
@Override
public Float get(View view) {
return view.getX();
}
};
/**
* View's y property.
*/
public static final ViewProperty Y = new ViewProperty("y") {
@Override
public void setValue(View view, float value) {
view.setY(value);
}
@Override
public Float get(View view) {
return view.getY();
}
};
/**
* View's z property.
*/
public static final ViewProperty Z = new ViewProperty("z") {
@Override
public void setValue(View view, float value) {
view.setZ(value);
}
@Override
public Float get(View view) {
return view.getZ();
}
};
/**
* View's alpha property.
*/
public static final ViewProperty ALPHA = new ViewProperty("alpha") {
@Override
public void setValue(View view, float value) {
view.setAlpha(value);
}
@Override
public Float get(View view) {
return view.getAlpha();
}
};
// Properties below are not RenderThread compatible
/**
* View's scrollX property.
*/
public static final ViewProperty SCROLL_X = new ViewProperty("scrollX") {
@Override
public void setValue(View view, float value) {
view.setScrollX((int) value);
}
@Override
public Float get(View view) {
return (float) view.getScrollX();
}
};
/**
* View's scrollY property.
*/
public static final ViewProperty SCROLL_Y = new ViewProperty("scrollY") {
@Override
public void setValue(View view, float value) {
view.setScrollY((int) value);
}
@Override
public Float get(View view) {
return (float) view.getScrollY();
}
};
/**
* The minimum visible change in pixels that can be visible to users.
*/
@SuppressLint("MinMaxConstant")
public static final float MIN_VISIBLE_CHANGE_PIXELS = 1f;
/**
* The minimum visible change in degrees that can be visible to users.
*/
@SuppressLint("MinMaxConstant")
public static final float MIN_VISIBLE_CHANGE_ROTATION_DEGREES = 1f / 10f;
/**
* The minimum visible change in alpha that can be visible to users.
*/
@SuppressLint("MinMaxConstant")
public static final float MIN_VISIBLE_CHANGE_ALPHA = 1f / 256f;
/**
* The minimum visible change in scale that can be visible to users.
*/
@SuppressLint("MinMaxConstant")
public static final float MIN_VISIBLE_CHANGE_SCALE = 1f / 500f;
// Use the max value of float to indicate an unset state.
private static final float UNSET = Float.MAX_VALUE;
// Multiplier to the min visible change value for value threshold
private static final float THRESHOLD_MULTIPLIER = 0.75f;
// Internal tracking for velocity.
float mVelocity = 0;
// Internal tracking for value.
float mValue = UNSET;
// Tracks whether start value is set. If not, the animation will obtain the value at the time
// of starting through the getter and use that as the starting value of the animation.
boolean mStartValueIsSet = false;
// Target to be animated.
final Object mTarget;
// View property id.
final FloatProperty mProperty;
// Package private tracking of animation lifecycle state. Visible to subclass animations.
boolean mRunning = false;
// Min and max values that defines the range of the animation values.
float mMaxValue = Float.MAX_VALUE;
float mMinValue = -mMaxValue;
// Last frame time. Always gets reset to -1 at the end of the animation.
private long mLastFrameTime = 0;
private float mMinVisibleChange;
// List of end listeners
private final ArrayList<OnAnimationEndListener> mEndListeners = new ArrayList<>();
// List of update listeners
private final ArrayList<OnAnimationUpdateListener> mUpdateListeners = new ArrayList<>();
// Animation handler used to schedule updates for this animation.
private AnimationHandler mAnimationHandler;
// Internal state for value/velocity pair.
static class MassState {
float mValue;
float mVelocity;
}
/**
* Creates a dynamic animation with the given FloatValueHolder instance.
*
* @param floatValueHolder the FloatValueHolder instance to be animated.
*/
DynamicAnimation(final FloatValueHolder floatValueHolder) {
mTarget = null;
mProperty = new FloatProperty("FloatValueHolder") {
@Override
public Float get(Object object) {
return floatValueHolder.getValue();
}
@Override
public void setValue(Object object, float value) {
floatValueHolder.setValue(value);
}
};
mMinVisibleChange = MIN_VISIBLE_CHANGE_PIXELS;
}
/**
* Creates a dynamic animation to animate the given property for the given {@link View}
*
* @param object the Object whose property is to be animated
* @param property the property to be animated
*/
<K> DynamicAnimation(K object, FloatProperty<K> property) {
mTarget = object;
mProperty = property;
if (mProperty == ROTATION || mProperty == ROTATION_X
|| mProperty == ROTATION_Y) {
mMinVisibleChange = MIN_VISIBLE_CHANGE_ROTATION_DEGREES;
} else if (mProperty == ALPHA) {
mMinVisibleChange = MIN_VISIBLE_CHANGE_ALPHA;
} else if (mProperty == SCALE_X || mProperty == SCALE_Y) {
mMinVisibleChange = MIN_VISIBLE_CHANGE_SCALE;
} else {
mMinVisibleChange = MIN_VISIBLE_CHANGE_PIXELS;
}
}
/**
* Sets the start value of the animation. If start value is not set, the animation will get
* the current value for the view's property, and use that as the start value.
*
* @param startValue start value for the animation
* @return the Animation whose start value is being set
*/
@SuppressWarnings("unchecked")
public T setStartValue(float startValue) {
mValue = startValue;
mStartValueIsSet = true;
return (T) this;
}
/**
* Start velocity of the animation. Default velocity is 0. Unit: change in property per
* second (e.g. pixels per second, scale/alpha value change per second).
*
* <p>Note when using a fixed value as the start velocity (as opposed to getting the velocity
* through touch events), it is recommended to define such a value in dp/second and convert it
* to pixel/second based on the density of the screen to achieve a consistent look across
* different screens.
*
* <p>To convert from dp/second to pixel/second:
* <pre class="prettyprint">
* float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond,
* getResources().getDisplayMetrics());
* </pre>
*
* @param startVelocity start velocity of the animation
* @return the Animation whose start velocity is being set
*/
@SuppressWarnings("unchecked")
public T setStartVelocity(float startVelocity) {
mVelocity = startVelocity;
return (T) this;
}
/**
* Sets the max value of the animation. Animations will not animate beyond their max value.
* Whether or not animation will come to an end when max value is reached is dependent on the
* child animation's implementation.
*
* @param max maximum value of the property to be animated
* @return the Animation whose max value is being set
*/
@SuppressWarnings("unchecked")
public T setMaxValue(float max) {
// This max value should be checked and handled in the subclass animations, instead of
// assuming the end of the animations when the max/min value is hit in the base class.
// The reason is that hitting max/min value may just be a transient state, such as during
// the spring oscillation.
mMaxValue = max;
return (T) this;
}
/**
* Sets the min value of the animation. Animations will not animate beyond their min value.
* Whether or not animation will come to an end when min value is reached is dependent on the
* child animation's implementation.
*
* @param min minimum value of the property to be animated
* @return the Animation whose min value is being set
*/
@SuppressWarnings("unchecked")
public T setMinValue(float min) {
mMinValue = min;
return (T) this;
}
/**
* Adds an end listener to the animation for receiving onAnimationEnd callbacks. If the listener
* is {@code null} or has already been added to the list of listeners for the animation, no op.
*
* @param listener the listener to be added
* @return the animation to which the listener is added
*/
@SuppressWarnings("unchecked")
public T addEndListener(OnAnimationEndListener listener) {
if (!mEndListeners.contains(listener)) {
mEndListeners.add(listener);
}
return (T) this;
}
/**
* Removes the end listener from the animation, so as to stop receiving animation end callbacks.
*
* @param listener the listener to be removed
*/
public void removeEndListener(OnAnimationEndListener listener) {
removeEntry(mEndListeners, listener);
}
/**
* Adds an update listener to the animation for receiving per-frame animation update callbacks.
* If the listener is {@code null} or has already been added to the list of listeners for the
* animation, no op.
*
* <p>Note that update listener should only be added before the start of the animation.
*
* @param listener the listener to be added
* @return the animation to which the listener is added
* @throws UnsupportedOperationException if the update listener is added after the animation has
* started
*/
@SuppressWarnings("unchecked")
public T addUpdateListener(OnAnimationUpdateListener listener) {
if (isRunning()) {
// Require update listener to be added before the animation, such as when we start
// the animation, we know whether the animation is RenderThread compatible.
throw new UnsupportedOperationException("Error: Update listeners must be added before"
+ "the animation.");
}
if (!mUpdateListeners.contains(listener)) {
mUpdateListeners.add(listener);
}
return (T) this;
}
/**
* Removes the update listener from the animation, so as to stop receiving animation update
* callbacks.
*
* @param listener the listener to be removed
*/
public void removeUpdateListener(OnAnimationUpdateListener listener) {
removeEntry(mUpdateListeners, listener);
}
/**
* This method sets the minimal change of animation value that is visible to users, which helps
* determine a reasonable threshold for the animation's termination condition. It is critical
* to set the minimal visible change for custom properties (i.e. non-<code>ViewProperty</code>s)
* unless the custom property is in pixels.
*
* <p>For custom properties, this minimum visible change defaults to change in pixel
* (i.e. {@link #MIN_VISIBLE_CHANGE_PIXELS}. It is recommended to adjust this value that is
* reasonable for the property to be animated. A general rule of thumb to calculate such a value
* is: minimum visible change = range of custom property value / corresponding pixel range. For
* example, if the property to be animated is a progress (from 0 to 100) that corresponds to a
* 200-pixel change. Then the min visible change should be 100 / 200. (i.e. 0.5).
*
* <p>It's not necessary to call this method when animating {@link ViewProperty}s, as the
* minimum visible change will be derived from the property. For example, if the property to be
* animated is in pixels (i.e. {@link #TRANSLATION_X}, {@link #TRANSLATION_Y},
* {@link #TRANSLATION_Z}, @{@link #SCROLL_X} or {@link #SCROLL_Y}), the default minimum visible
* change is 1 (pixel). For {@link #ROTATION}, {@link #ROTATION_X} or {@link #ROTATION_Y}, the
* animation will use {@link #MIN_VISIBLE_CHANGE_ROTATION_DEGREES} as the min visible change,
* which is 1/10. Similarly, the minimum visible change for alpha (
* i.e. {@link #MIN_VISIBLE_CHANGE_ALPHA} is defined as 1 / 256.
*
* @param minimumVisibleChange minimum change in property value that is visible to users
* @return the animation whose min visible change is being set
* @throws IllegalArgumentException if the given threshold is not positive
*/
@SuppressWarnings("unchecked")
public T setMinimumVisibleChange(@FloatRange(from = 0.0, fromInclusive = false)
float minimumVisibleChange) {
if (minimumVisibleChange <= 0) {
throw new IllegalArgumentException("Minimum visible change must be positive.");
}
mMinVisibleChange = minimumVisibleChange;
setValueThreshold(minimumVisibleChange * THRESHOLD_MULTIPLIER);
return (T) this;
}
/**
* Returns the minimum change in the animation property that could be visibly different to
* users.
*
* @return minimum change in property value that is visible to users
*/
public float getMinimumVisibleChange() {
return mMinVisibleChange;
}
/**
* Remove {@code null} entries from the list.
*/
private static <T> void removeNullEntries(ArrayList<T> list) {
// Clean up null entries
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i) == null) {
list.remove(i);
}
}
}
/**
* Remove an entry from the list by marking it {@code null} and clean up later.
*/
private static <T> void removeEntry(ArrayList<T> list, T entry) {
int id = list.indexOf(entry);
if (id >= 0) {
list.set(id, null);
}
}
/****************Animation Lifecycle Management***************/
/**
* Starts an animation. If the animation has already been started, no op. Note that calling
* {@link #start()} will not immediately set the property value to start value of the animation.
* The property values will be changed at each animation pulse, which happens before the draw
* pass. As a result, the changes will be reflected in the next frame, the same as if the values
* were set immediately. This method should only be called on main thread.
*
* Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
* is created on the same thread as the first call to start/cancel an animation. All the
* subsequent animation lifecycle manipulations need to be on that same thread, until the
* AnimationHandler is reset (using [setAnimationHandler]).
*
* @throws AndroidRuntimeException if this method is not called on the same thread as the
* animation handler
*/
@MainThread
public void start() {
if (!isCurrentThread()) {
throw new AndroidRuntimeException("Animations may only be started on the same thread "
+ "as the animation handler");
}
if (!mRunning) {
startAnimationInternal();
}
}
boolean isCurrentThread() {
return Thread.currentThread() == Looper.myLooper().getThread();
}
/**
* Cancels the on-going animation. If the animation hasn't started, no op.
*
* Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
* is created on the same thread as the first call to start/cancel an animation. All the
* subsequent animation lifecycle manipulations need to be on that same thread, until the
* AnimationHandler is reset (using [setAnimationHandler]).
*
* @throws AndroidRuntimeException if this method is not called on the same thread as the
* animation handler
*/
@MainThread
public void cancel() {
if (!isCurrentThread()) {
throw new AndroidRuntimeException("Animations may only be canceled from the same "
+ "thread as the animation handler");
}
if (mRunning) {
endAnimationInternal(true);
}
}
/**
* Returns whether the animation is currently running.
*
* @return {@code true} if the animation is currently running, {@code false} otherwise
*/
public boolean isRunning() {
return mRunning;
}
/************************** Private APIs below ********************************/
// This gets called when the animation is started, to finish the setup of the animation
// before the animation pulsing starts.
private void startAnimationInternal() {
if (!mRunning) {
mRunning = true;
if (!mStartValueIsSet) {
mValue = getPropertyValue();
}
// Sanity check:
if (mValue > mMaxValue || mValue < mMinValue) {
throw new IllegalArgumentException("Starting value need to be in between min"
+ " value and max value");
}
getAnimationHandler().addAnimationFrameCallback(this, 0);
}
}
/**
* This gets call on each frame of the animation. Animation value and velocity are updated
* in this method based on the new frame time. The property value of the view being animated
* is then updated. The animation's ending conditions are also checked in this method. Once
* the animation reaches equilibrium, the animation will come to its end, and end listeners
* will be notified, if any.
*/
@Override
public boolean doAnimationFrame(long frameTime) {
if (mLastFrameTime == 0) {
// First frame.
mLastFrameTime = frameTime;
setPropertyValue(mValue);
return false;
}
long deltaT = frameTime - mLastFrameTime;
mLastFrameTime = frameTime;
float durationScale = ValueAnimator.getDurationScale();
deltaT = durationScale == 0.0f ? Integer.MAX_VALUE : (long) (deltaT / durationScale);
boolean finished = updateValueAndVelocity(deltaT);
// Clamp value & velocity.
mValue = Math.min(mValue, mMaxValue);
mValue = Math.max(mValue, mMinValue);
setPropertyValue(mValue);
if (finished) {
endAnimationInternal(false);
}
return finished;
}
@Override
public void commitAnimationFrame(long frameTime) {
doAnimationFrame(frameTime);
}
/**
* Updates the animation state (i.e. value and velocity). This method is package private, so
* subclasses can override this method to calculate the new value and velocity in their custom
* way.
*
* @param deltaT time elapsed in millisecond since last frame
* @return whether the animation has finished
*/
abstract boolean updateValueAndVelocity(long deltaT);
/**
* Internal method to reset the animation states when animation is finished/canceled.
*/
private void endAnimationInternal(boolean canceled) {
mRunning = false;
getAnimationHandler().removeCallback(this);
mLastFrameTime = 0;
mStartValueIsSet = false;
for (int i = 0; i < mEndListeners.size(); i++) {
if (mEndListeners.get(i) != null) {
mEndListeners.get(i).onAnimationEnd(this, canceled, mValue, mVelocity);
}
}
removeNullEntries(mEndListeners);
}
/**
* Updates the property value through the corresponding setter.
*/
@SuppressWarnings("unchecked")
void setPropertyValue(float value) {
mProperty.setValue(mTarget, value);
for (int i = 0; i < mUpdateListeners.size(); i++) {
if (mUpdateListeners.get(i) != null) {
mUpdateListeners.get(i).onAnimationUpdate(this, mValue, mVelocity);
}
}
removeNullEntries(mUpdateListeners);
}
/**
* Returns the default threshold.
*/
float getValueThreshold() {
return mMinVisibleChange * THRESHOLD_MULTIPLIER;
}
/**
* Obtain the property value through the corresponding getter.
*/
@SuppressWarnings("unchecked")
private float getPropertyValue() {
return (Float) mProperty.get(mTarget);
}
/**
* Returns the {@link AnimationHandler} used to schedule updates for this animator.
*
* @return the {@link AnimationHandler} for this animator.
*/
@NonNull
public AnimationHandler getAnimationHandler() {
return mAnimationHandler != null ? mAnimationHandler : AnimationHandler.getInstance();
}
/****************Sub class animations**************/
/**
* Returns the acceleration at the given value with the given velocity.
**/
abstract float getAcceleration(float value, float velocity);
/**
* Returns whether the animation has reached equilibrium.
*/
abstract boolean isAtEquilibrium(float value, float velocity);
/**
* Updates the default value threshold for the animation based on the property to be animated.
*/
abstract void setValueThreshold(float threshold);
/**
* An animation listener that receives end notifications from an animation.
*/
public interface OnAnimationEndListener {
/**
* Notifies the end of an animation. Note that this callback will be invoked not only when
* an animation reach equilibrium, but also when the animation is canceled.
*
* @param animation animation that has ended or was canceled
* @param canceled whether the animation has been canceled
* @param value the final value when the animation stopped
* @param velocity the final velocity when the animation stopped
*/
void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
float velocity);
}
/**
* Implementors of this interface can add themselves as update listeners
* to an <code>DynamicAnimation</code> instance to receive callbacks on every animation
* frame, after the current frame's values have been calculated for that
* <code>DynamicAnimation</code>.
*/
public interface OnAnimationUpdateListener {
/**
* Notifies the occurrence of another frame of the animation.
*
* @param animation animation that the update listener is added to
* @param value the current value of the animation
* @param velocity the current velocity of the animation
*/
void onAnimationUpdate(DynamicAnimation animation, float value, float velocity);
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2022 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.internal.dynamicanimation.animation;
/**
* <p>FloatValueHolder holds a float value. FloatValueHolder provides a setter and a getter (
* i.e. {@link #setValue(float)} and {@link #getValue()}) to access this float value. Animations can
* be performed on a FloatValueHolder instance. During each frame of the animation, the
* FloatValueHolder will have its value updated via {@link #setValue(float)}. The caller can
* obtain the up-to-date animation value via {@link FloatValueHolder#getValue()}.
*
* @see SpringAnimation#SpringAnimation(FloatValueHolder)
*/
public class FloatValueHolder {
private float mValue = 0.0f;
/**
* Constructs a holder for a float value that is initialized to 0.
*/
public FloatValueHolder() {
}
/**
* Constructs a holder for a float value that is initialized to the input value.
*
* @param value the value to initialize the value held in the FloatValueHolder
*/
public FloatValueHolder(float value) {
setValue(value);
}
/**
* Sets the value held in the FloatValueHolder instance.
*
* @param value float value held in the FloatValueHolder instance
*/
public void setValue(float value) {
mValue = value;
}
/**
* Returns the float value held in the FloatValueHolder instance.
*
* @return float value held in the FloatValueHolder instance
*/
public float getValue() {
return mValue;
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2022 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.internal.dynamicanimation.animation;
/**
* Hide this for now, in case we want to change the API.
*/
interface Force {
// Acceleration based on position.
float getAcceleration(float position, float velocity);
boolean isAtEquilibrium(float value, float velocity);
}

View File

@@ -0,0 +1,314 @@
/*
* Copyright (C) 2022 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.internal.dynamicanimation.animation;
import android.util.AndroidRuntimeException;
import android.util.FloatProperty;
/**
* SpringAnimation is an animation that is driven by a {@link SpringForce}. The spring force defines
* the spring's stiffness, damping ratio, as well as the rest position. Once the SpringAnimation is
* started, on each frame the spring force will update the animation's value and velocity.
* The animation will continue to run until the spring force reaches equilibrium. If the spring used
* in the animation is undamped, the animation will never reach equilibrium. Instead, it will
* oscillate forever.
*
* <div class="special reference">
* <h3>Developer Guides</h3>
* </div>
*
* <p>To create a simple {@link SpringAnimation} that uses the default {@link SpringForce}:</p>
* <pre class="prettyprint">
* // Create an animation to animate view's X property, set the rest position of the
* // default spring to 0, and start the animation with a starting velocity of 5000 (pixel/s).
* final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.X, 0)
* .setStartVelocity(5000);
* anim.start();
* </pre>
*
* <p>Alternatively, a {@link SpringAnimation} can take a pre-configured {@link SpringForce}, and
* use that to drive the animation. </p>
* <pre class="prettyprint">
* // Create a low stiffness, low bounce spring at position 0.
* SpringForce spring = new SpringForce(0)
* .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
* .setStiffness(SpringForce.STIFFNESS_LOW);
* // Create an animation to animate view's scaleY property, and start the animation using
* // the spring above and a starting value of 0.5. Additionally, constrain the range of value for
* // the animation to be non-negative, effectively preventing any spring overshoot.
* final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.SCALE_Y)
* .setMinValue(0).setSpring(spring).setStartValue(1);
* anim.start();
* </pre>
*/
public final class SpringAnimation extends DynamicAnimation<SpringAnimation> {
private SpringForce mSpring = null;
private float mPendingPosition = UNSET;
private static final float UNSET = Float.MAX_VALUE;
private boolean mEndRequested = false;
/**
* <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During
* the animation, the {@link FloatValueHolder} instance will be updated via
* {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
* animation value via {@link FloatValueHolder#getValue()}.
*
* <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
* {@link FloatValueHolder#setValue(float)} outside of the animation during an
* animation run will not have any effect on the on-going animation.
*
* @param floatValueHolder the property to be animated
*/
public SpringAnimation(FloatValueHolder floatValueHolder) {
super(floatValueHolder);
}
/**
* <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During
* the animation, the {@link FloatValueHolder} instance will be updated via
* {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
* animation value via {@link FloatValueHolder#getValue()}.
*
* A Spring will be created with the given final position and default stiffness and damping
* ratio. This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
*
* <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
* {@link FloatValueHolder#setValue(float)} outside of the animation during an
* animation run will not have any effect on the on-going animation.
*
* @param floatValueHolder the property to be animated
* @param finalPosition the final position of the spring to be created.
*/
public SpringAnimation(FloatValueHolder floatValueHolder, float finalPosition) {
super(floatValueHolder);
mSpring = new SpringForce(finalPosition);
}
/**
* This creates a SpringAnimation that animates the property of the given object.
* Note, a spring will need to setup through {@link #setSpring(SpringForce)} before
* the animation starts.
*
* @param object the Object whose property will be animated
* @param property the property to be animated
* @param <K> the class on which the Property is declared
*/
public <K> SpringAnimation(K object, FloatProperty<K> property) {
super(object, property);
}
/**
* This creates a SpringAnimation that animates the property of the given object. A Spring will
* be created with the given final position and default stiffness and damping ratio.
* This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
*
* @param object the Object whose property will be animated
* @param property the property to be animated
* @param finalPosition the final position of the spring to be created.
* @param <K> the class on which the Property is declared
*/
public <K> SpringAnimation(K object, FloatProperty<K> property,
float finalPosition) {
super(object, property);
mSpring = new SpringForce(finalPosition);
}
/**
* Returns the spring that the animation uses for animations.
*
* @return the spring that the animation uses for animations
*/
public SpringForce getSpring() {
return mSpring;
}
/**
* Uses the given spring as the force that drives this animation. If this spring force has its
* parameters re-configured during the animation, the new configuration will be reflected in the
* animation immediately.
*
* @param force a pre-defined spring force that drives the animation
* @return the animation that the spring force is set on
*/
public SpringAnimation setSpring(SpringForce force) {
mSpring = force;
return this;
}
@Override
public void start() {
sanityCheck();
mSpring.setValueThreshold(getValueThreshold());
super.start();
}
/**
* Updates the final position of the spring.
* <p/>
* When the animation is running, calling this method would assume the position change of the
* spring as a continuous movement since last frame, which yields more accurate results than
* changing the spring position directly through {@link SpringForce#setFinalPosition(float)}.
* <p/>
* If the animation hasn't started, calling this method will change the spring position, and
* immediately start the animation.
*
* @param finalPosition rest position of the spring
*/
public void animateToFinalPosition(float finalPosition) {
if (isRunning()) {
mPendingPosition = finalPosition;
} else {
if (mSpring == null) {
mSpring = new SpringForce(finalPosition);
}
mSpring.setFinalPosition(finalPosition);
start();
}
}
/**
* Cancels the on-going animation. If the animation hasn't started, no op. Note that this method
* should only be called on main thread.
*
* @throws AndroidRuntimeException if this method is not called on the main thread
*/
@Override
public void cancel() {
super.cancel();
if (mPendingPosition != UNSET) {
if (mSpring == null) {
mSpring = new SpringForce(mPendingPosition);
} else {
mSpring.setFinalPosition(mPendingPosition);
}
mPendingPosition = UNSET;
}
}
/**
* Skips to the end of the animation. If the spring is undamped, an
* {@link IllegalStateException} will be thrown, as the animation would never reach to an end.
* It is recommended to check {@link #canSkipToEnd()} before calling this method. If animation
* is not running, no-op.
*
* Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
* is created on the same thread as the first call to start/cancel an animation. All the
* subsequent animation lifecycle manipulations need to be on that same thread, until the
* AnimationHandler is reset (using [setAnimationHandler]).
*
* @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0)
* @throws AndroidRuntimeException if this method is not called on the same thread as the
* animation handler
*/
public void skipToEnd() {
if (!canSkipToEnd()) {
throw new UnsupportedOperationException("Spring animations can only come to an end"
+ " when there is damping");
}
if (!isCurrentThread()) {
throw new AndroidRuntimeException("Animations may only be started on the same thread "
+ "as the animation handler");
}
if (mRunning) {
mEndRequested = true;
}
}
/**
* Queries whether the spring can eventually come to the rest position.
*
* @return {@code true} if the spring is damped, otherwise {@code false}
*/
public boolean canSkipToEnd() {
return mSpring.mDampingRatio > 0;
}
/************************ Below are private APIs *************************/
private void sanityCheck() {
if (mSpring == null) {
throw new UnsupportedOperationException("Incomplete SpringAnimation: Either final"
+ " position or a spring force needs to be set.");
}
double finalPosition = mSpring.getFinalPosition();
if (finalPosition > mMaxValue) {
throw new UnsupportedOperationException("Final position of the spring cannot be greater"
+ " than the max value.");
} else if (finalPosition < mMinValue) {
throw new UnsupportedOperationException("Final position of the spring cannot be less"
+ " than the min value.");
}
}
@Override
boolean updateValueAndVelocity(long deltaT) {
// If user had requested end, then update the value and velocity to end state and consider
// animation done.
if (mEndRequested) {
if (mPendingPosition != UNSET) {
mSpring.setFinalPosition(mPendingPosition);
mPendingPosition = UNSET;
}
mValue = mSpring.getFinalPosition();
mVelocity = 0;
mEndRequested = false;
return true;
}
if (mPendingPosition != UNSET) {
// Approximate by considering half of the time spring position stayed at the old
// position, half of the time it's at the new position.
MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2);
mSpring.setFinalPosition(mPendingPosition);
mPendingPosition = UNSET;
massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2);
mValue = massState.mValue;
mVelocity = massState.mVelocity;
} else {
MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT);
mValue = massState.mValue;
mVelocity = massState.mVelocity;
}
mValue = Math.max(mValue, mMinValue);
mValue = Math.min(mValue, mMaxValue);
if (isAtEquilibrium(mValue, mVelocity)) {
mValue = mSpring.getFinalPosition();
mVelocity = 0f;
return true;
}
return false;
}
@Override
float getAcceleration(float value, float velocity) {
return mSpring.getAcceleration(value, velocity);
}
@Override
boolean isAtEquilibrium(float value, float velocity) {
return mSpring.isAtEquilibrium(value, velocity);
}
@Override
void setValueThreshold(float threshold) {
}
}

View File

@@ -0,0 +1,323 @@
/*
* Copyright (C) 2022 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.internal.dynamicanimation.animation;
import android.annotation.FloatRange;
/**
* Spring Force defines the characteristics of the spring being used in the animation.
* <p>
* By configuring the stiffness and damping ratio, callers can create a spring with the look and
* feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring
* is, the harder it is to stretch it, the faster it undergoes dampening.
* <p>
* Spring damping ratio describes how oscillations in a system decay after a disturbance.
* When damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position
* without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
* return to equilibrium within the shortest amount of time. When damping ratio is less than 1
* (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without any
* damping (i.e. damping ratio = 0), the mass will oscillate forever.
*/
public final class SpringForce implements Force {
/**
* Stiffness constant for extremely stiff spring.
*/
public static final float STIFFNESS_HIGH = 10_000f;
/**
* Stiffness constant for medium stiff spring. This is the default stiffness for spring force.
*/
public static final float STIFFNESS_MEDIUM = 1500f;
/**
* Stiffness constant for a spring with low stiffness.
*/
public static final float STIFFNESS_LOW = 200f;
/**
* Stiffness constant for a spring with very low stiffness.
*/
public static final float STIFFNESS_VERY_LOW = 50f;
/**
* Damping ratio for a very bouncy spring. Note for under-damped springs
* (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring.
*/
public static final float DAMPING_RATIO_HIGH_BOUNCY = 0.2f;
/**
* Damping ratio for a medium bouncy spring. This is also the default damping ratio for spring
* force. Note for under-damped springs (i.e. damping ratio < 1), the lower the damping ratio,
* the more bouncy the spring.
*/
public static final float DAMPING_RATIO_MEDIUM_BOUNCY = 0.5f;
/**
* Damping ratio for a spring with low bounciness. Note for under-damped springs
* (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness.
*/
public static final float DAMPING_RATIO_LOW_BOUNCY = 0.75f;
/**
* Damping ratio for a spring with no bounciness. This damping ratio will create a critically
* damped spring that returns to equilibrium within the shortest amount of time without
* oscillating.
*/
public static final float DAMPING_RATIO_NO_BOUNCY = 1f;
// This multiplier is used to calculate the velocity threshold given a certain value threshold.
// The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity
// is a reasonable threshold.
private static final double VELOCITY_THRESHOLD_MULTIPLIER = 1000.0 / 16.0;
// Natural frequency
double mNaturalFreq = Math.sqrt(STIFFNESS_MEDIUM);
// Damping ratio.
double mDampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY;
// Value to indicate an unset state.
private static final double UNSET = Double.MAX_VALUE;
// Indicates whether the spring has been initialized
private boolean mInitialized = false;
// Threshold for velocity and value to determine when it's reasonable to assume that the spring
// is approximately at rest.
private double mValueThreshold;
private double mVelocityThreshold;
// Intermediate values to simplify the spring function calculation per frame.
private double mGammaPlus;
private double mGammaMinus;
private double mDampedFreq;
// Final position of the spring. This must be set before the start of the animation.
private double mFinalPosition = UNSET;
// Internal state to hold a value/velocity pair.
private final DynamicAnimation.MassState mMassState = new DynamicAnimation.MassState();
/**
* Creates a spring force. Note that final position of the spring must be set through
* {@link #setFinalPosition(float)} before the spring animation starts.
*/
public SpringForce() {
// No op.
}
/**
* Creates a spring with a given final rest position.
*
* @param finalPosition final position of the spring when it reaches equilibrium
*/
public SpringForce(float finalPosition) {
mFinalPosition = finalPosition;
}
/**
* Sets the stiffness of a spring. The more stiff a spring is, the more force it applies to
* the object attached when the spring is not at the final position. Default stiffness is
* {@link #STIFFNESS_MEDIUM}.
*
* @param stiffness non-negative stiffness constant of a spring
* @return the spring force that the given stiffness is set on
* @throws IllegalArgumentException if the given spring stiffness is not positive
*/
public SpringForce setStiffness(
@FloatRange(from = 0.0, fromInclusive = false) float stiffness) {
if (stiffness <= 0) {
throw new IllegalArgumentException("Spring stiffness constant must be positive.");
}
mNaturalFreq = Math.sqrt(stiffness);
// All the intermediate values need to be recalculated.
mInitialized = false;
return this;
}
/**
* Gets the stiffness of the spring.
*
* @return the stiffness of the spring
*/
public float getStiffness() {
return (float) (mNaturalFreq * mNaturalFreq);
}
/**
* Spring damping ratio describes how oscillations in a system decay after a disturbance.
* <p>
* When damping ratio > 1 (over-damped), the object will quickly return to the rest position
* without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
* return to equilibrium within the shortest amount of time. When damping ratio is less than 1
* (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without
* any damping (i.e. damping ratio = 0), the mass will oscillate forever.
* <p>
* Default damping ratio is {@link #DAMPING_RATIO_MEDIUM_BOUNCY}.
*
* @param dampingRatio damping ratio of the spring, it should be non-negative
* @return the spring force that the given damping ratio is set on
* @throws IllegalArgumentException if the {@param dampingRatio} is negative.
*/
public SpringForce setDampingRatio(@FloatRange(from = 0.0) float dampingRatio) {
if (dampingRatio < 0) {
throw new IllegalArgumentException("Damping ratio must be non-negative");
}
mDampingRatio = dampingRatio;
// All the intermediate values need to be recalculated.
mInitialized = false;
return this;
}
/**
* Returns the damping ratio of the spring.
*
* @return damping ratio of the spring
*/
public float getDampingRatio() {
return (float) mDampingRatio;
}
/**
* Sets the rest position of the spring.
*
* @param finalPosition rest position of the spring
* @return the spring force that the given final position is set on
*/
public SpringForce setFinalPosition(float finalPosition) {
mFinalPosition = finalPosition;
return this;
}
/**
* Returns the rest position of the spring.
*
* @return rest position of the spring
*/
public float getFinalPosition() {
return (float) mFinalPosition;
}
/*********************** Below are private APIs *********************/
@Override
public float getAcceleration(float lastDisplacement, float lastVelocity) {
lastDisplacement -= getFinalPosition();
double k = mNaturalFreq * mNaturalFreq;
double c = 2 * mNaturalFreq * mDampingRatio;
return (float) (-k * lastDisplacement - c * lastVelocity);
}
@Override
public boolean isAtEquilibrium(float value, float velocity) {
if (Math.abs(velocity) < mVelocityThreshold
&& Math.abs(value - getFinalPosition()) < mValueThreshold) {
return true;
}
return false;
}
/**
* Initialize the string by doing the necessary pre-calculation as well as some sanity check
* on the setup.
*
* @throws IllegalStateException if the final position is not yet set by the time the spring
* animation has started
*/
private void init() {
if (mInitialized) {
return;
}
if (mFinalPosition == UNSET) {
throw new IllegalStateException("Error: Final position of the spring must be"
+ " set before the animation starts");
}
if (mDampingRatio > 1) {
// Over damping
mGammaPlus = -mDampingRatio * mNaturalFreq
+ mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
mGammaMinus = -mDampingRatio * mNaturalFreq
- mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
} else if (mDampingRatio >= 0 && mDampingRatio < 1) {
// Under damping
mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
}
mInitialized = true;
}
/**
* Internal only call for Spring to calculate the spring position/velocity using
* an analytical approach.
*/
DynamicAnimation.MassState updateValues(double lastDisplacement, double lastVelocity,
long timeElapsed) {
init();
double deltaT = timeElapsed / 1000d; // unit: seconds
lastDisplacement -= mFinalPosition;
double displacement;
double currentVelocity;
if (mDampingRatio > 1) {
// Overdamped
double coeffA = lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity)
/ (mGammaMinus - mGammaPlus);
double coeffB = (mGammaMinus * lastDisplacement - lastVelocity)
/ (mGammaMinus - mGammaPlus);
displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT)
+ coeffB * Math.pow(Math.E, mGammaPlus * deltaT);
currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT)
+ coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT);
} else if (mDampingRatio == 1) {
// Critically damped
double coeffA = lastDisplacement;
double coeffB = lastVelocity + mNaturalFreq * lastDisplacement;
displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT);
currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT)
* -mNaturalFreq + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT);
} else {
// Underdamped
double cosCoeff = lastDisplacement;
double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq
* lastDisplacement + lastVelocity);
displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
* (cosCoeff * Math.cos(mDampedFreq * deltaT)
+ sinCoeff * Math.sin(mDampedFreq * deltaT));
currentVelocity = displacement * -mNaturalFreq * mDampingRatio
+ Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
* (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT)
+ mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT));
}
mMassState.mValue = (float) (displacement + mFinalPosition);
mMassState.mVelocity = (float) currentVelocity;
return mMassState;
}
/**
* This threshold defines how close the animation value needs to be before the animation can
* finish. This default value is based on the property being animated, e.g. animations on alpha,
* scale, translation or rotation would have different thresholds. This value should be small
* enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that
* animations take seconds to finish.
*
* @param threshold the difference between the animation value and final spring position that
* is allowed to end the animation when velocity is very low
*/
void setValueThreshold(double threshold) {
mValueThreshold = Math.abs(threshold);
mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER;
}
}