/* * 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.support.annotation.FloatRange; /** * Spring Force defines the characteristics of the spring being used in the animation. *

* 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. *

* 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. *

* 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. *

* 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 *********************/ /** * @hide */ @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); } /** * @hide */ @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; } }