blob: 5a9a4327ce72810933c95e62f1ef7567e7fef79d [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 android.support.animation;
import android.os.Looper;
import android.util.AndroidRuntimeException;
import android.view.View;
/**
* 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);
}
/**
* 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, FloatPropertyCompat<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, FloatPropertyCompat<K> property,
float finalPosition) {
super(object, property);
mSpring = new SpringForce(finalPosition);
}
/**
* @deprecated This API is being replaced with
* {@link #SpringAnimation(Object, FloatPropertyCompat)}.
*
* <p><b>Note: </b> Migration to the new API should require no modification to callers of this
* deprecated API. The new API's parameters are the base class of the original's parameters and
* therefore is compatible to calls to this deprecated method.
*/
@Deprecated
public SpringAnimation(View v, ViewProperty property) {
super(v, property);
}
/**
* @deprecated This API is being replaced with
* {@link #SpringAnimation(Object, FloatPropertyCompat, float)}.
*
* <p><b>Note: </b> Migration to the new API should require no modification to callers of this
* deprecated API. The new API's parameters are the base class of the original's parameters and
* therefore is compatible to calls to this deprecated method.
*/
@Deprecated
public SpringAnimation(View v, ViewProperty property, float finalPosition) {
super(v, 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();
}
}
/**
* 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. This method
* should only be called on main thread. If animation is not running, no-op.
*
* @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0)
* @throws AndroidRuntimeException if this method is not called on the main thread
*/
public void skipToEnd() {
if (!canSkipToEnd()) {
throw new UnsupportedOperationException("Spring animations can only come to an end"
+ " when there is damping");
}
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new AndroidRuntimeException("Animations may only be started on the main thread");
}
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) {
double lastPosition = mSpring.getFinalPosition();
// 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) {
}
}