blob: 8e92827e686015cf75dd7d5a4544a9f9387f78d2 [file] [log] [blame]
/*
* Copyright (C) 2017 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 androidx.dynamicanimation.animation;
import android.os.Looper;
import android.util.AndroidRuntimeException;
import android.view.View;
import androidx.annotation.FloatRange;
import androidx.annotation.RestrictTo;
import androidx.core.view.ViewCompat;
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 FloatPropertyCompat<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 getValue(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 getValue(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) {
ViewCompat.setTranslationZ(view, value);
}
@Override
public float getValue(View view) {
return ViewCompat.getTranslationZ(view);
}
};
/**
* 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 getValue(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 getValue(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 getValue(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 getValue(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 getValue(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 getValue(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 getValue(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) {
ViewCompat.setZ(view, value);
}
@Override
public float getValue(View view) {
return ViewCompat.getZ(view);
}
};
/**
* 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 getValue(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 getValue(View view) {
return 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 getValue(View view) {
return view.getScrollY();
}
};
/**
* The minimum visible change in pixels that can be visible to users.
*/
public static final float MIN_VISIBLE_CHANGE_PIXELS = 1f;
/**
* The minimum visible change in degrees that can be visible to users.
*/
public static final float MIN_VISIBLE_CHANGE_ROTATION_DEGREES = 1f / 10f;
/**
* The minimum visible change in alpha that can be visible to users.
*/
public static final float MIN_VISIBLE_CHANGE_ALPHA = 1f / 256f;
/**
* The minimum visible change in scale that can be visible to users.
*/
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 FloatPropertyCompat 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<>();
// 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 FloatPropertyCompat("FloatValueHolder") {
@Override
public float getValue(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, FloatPropertyCompat<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_ALPHA;
} 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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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.
*
* @throws AndroidRuntimeException if this method is not called on the main thread
*/
public void start() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new AndroidRuntimeException("Animations may only be started on the main thread");
}
if (!mRunning) {
startAnimationInternal();
}
}
/**
* 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
*/
public void cancel() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new AndroidRuntimeException("Animations may only be canceled on the main thread");
}
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");
}
AnimationHandler.getInstance().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.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public boolean doAnimationFrame(long frameTime) {
if (mLastFrameTime == 0) {
// First frame.
mLastFrameTime = frameTime;
setPropertyValue(mValue);
return false;
}
long deltaT = frameTime - mLastFrameTime;
mLastFrameTime = frameTime;
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;
}
/**
* 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;
AnimationHandler.getInstance().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.
*/
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.
*/
private float getPropertyValue() {
return mProperty.getValue(mTarget);
}
/****************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);
}
}