Home | History | Annotate | Download | only in animation
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.support.animation;
     18 
     19 import android.support.annotation.FloatRange;
     20 
     21 /**
     22  * Spring Force defines the characteristics of the spring being used in the animation.
     23  * <p>
     24  * By configuring the stiffness and damping ratio, callers can create a spring with the look and
     25  * feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring
     26  * is, the harder it is to stretch it, the faster it undergoes dampening.
     27  * <p>
     28  * Spring damping ratio describes how oscillations in a system decay after a disturbance.
     29  * When damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position
     30  * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
     31  * return to equilibrium within the shortest amount of time. When damping ratio is less than 1
     32  * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without any
     33  * damping (i.e. damping ratio = 0), the mass will oscillate forever.
     34  */
     35 public final class SpringForce implements Force {
     36     /**
     37      * Stiffness constant for extremely stiff spring.
     38      */
     39     public static final float STIFFNESS_HIGH = 10_000f;
     40     /**
     41      * Stiffness constant for medium stiff spring. This is the default stiffness for spring force.
     42      */
     43     public static final float STIFFNESS_MEDIUM = 1500f;
     44     /**
     45      * Stiffness constant for a spring with low stiffness.
     46      */
     47     public static final float STIFFNESS_LOW = 200f;
     48     /**
     49      * Stiffness constant for a spring with very low stiffness.
     50      */
     51     public static final float STIFFNESS_VERY_LOW = 50f;
     52 
     53     /**
     54      * Damping ratio for a very bouncy spring. Note for under-damped springs
     55      * (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring.
     56      */
     57     public static final float DAMPING_RATIO_HIGH_BOUNCY = 0.2f;
     58     /**
     59      * Damping ratio for a medium bouncy spring. This is also the default damping ratio for spring
     60      * force. Note for under-damped springs (i.e. damping ratio < 1), the lower the damping ratio,
     61      * the more bouncy the spring.
     62      */
     63     public static final float DAMPING_RATIO_MEDIUM_BOUNCY = 0.5f;
     64     /**
     65      * Damping ratio for a spring with low bounciness. Note for under-damped springs
     66      * (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness.
     67      */
     68     public static final float DAMPING_RATIO_LOW_BOUNCY = 0.75f;
     69     /**
     70      * Damping ratio for a spring with no bounciness. This damping ratio will create a critically
     71      * damped spring that returns to equilibrium within the shortest amount of time without
     72      * oscillating.
     73      */
     74     public static final float DAMPING_RATIO_NO_BOUNCY = 1f;
     75 
     76     // This multiplier is used to calculate the velocity threshold given a certain value threshold.
     77     // The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity
     78     // is a reasonable threshold.
     79     private static final double VELOCITY_THRESHOLD_MULTIPLIER = 1000.0 / 16.0;
     80 
     81     // Natural frequency
     82     double mNaturalFreq = Math.sqrt(STIFFNESS_MEDIUM);
     83     // Damping ratio.
     84     double mDampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY;
     85 
     86     // Value to indicate an unset state.
     87     private static final double UNSET = Double.MAX_VALUE;
     88 
     89     // Indicates whether the spring has been initialized
     90     private boolean mInitialized = false;
     91 
     92     // Threshold for velocity and value to determine when it's reasonable to assume that the spring
     93     // is approximately at rest.
     94     private double mValueThreshold;
     95     private double mVelocityThreshold;
     96 
     97     // Intermediate values to simplify the spring function calculation per frame.
     98     private double mGammaPlus;
     99     private double mGammaMinus;
    100     private double mDampedFreq;
    101 
    102     // Final position of the spring. This must be set before the start of the animation.
    103     private double mFinalPosition = UNSET;
    104 
    105     // Internal state to hold a value/velocity pair.
    106     private final DynamicAnimation.MassState mMassState = new DynamicAnimation.MassState();
    107 
    108     /**
    109      * Creates a spring force. Note that final position of the spring must be set through
    110      * {@link #setFinalPosition(float)} before the spring animation starts.
    111      */
    112     public SpringForce() {
    113         // No op.
    114     }
    115 
    116     /**
    117      * Creates a spring with a given final rest position.
    118      *
    119      * @param finalPosition final position of the spring when it reaches equilibrium
    120      */
    121     public SpringForce(float finalPosition) {
    122         mFinalPosition = finalPosition;
    123     }
    124 
    125     /**
    126      * Sets the stiffness of a spring. The more stiff a spring is, the more force it applies to
    127      * the object attached when the spring is not at the final position. Default stiffness is
    128      * {@link #STIFFNESS_MEDIUM}.
    129      *
    130      * @param stiffness non-negative stiffness constant of a spring
    131      * @return the spring force that the given stiffness is set on
    132      * @throws IllegalArgumentException if the given spring stiffness is not positive
    133      */
    134     public SpringForce setStiffness(
    135             @FloatRange(from = 0.0, fromInclusive = false) float stiffness) {
    136         if (stiffness <= 0) {
    137             throw new IllegalArgumentException("Spring stiffness constant must be positive.");
    138         }
    139         mNaturalFreq = Math.sqrt(stiffness);
    140         // All the intermediate values need to be recalculated.
    141         mInitialized = false;
    142         return this;
    143     }
    144 
    145     /**
    146      * Gets the stiffness of the spring.
    147      *
    148      * @return the stiffness of the spring
    149      */
    150     public float getStiffness() {
    151         return (float) (mNaturalFreq * mNaturalFreq);
    152     }
    153 
    154     /**
    155      * Spring damping ratio describes how oscillations in a system decay after a disturbance.
    156      * <p>
    157      * When damping ratio > 1 (over-damped), the object will quickly return to the rest position
    158      * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
    159      * return to equilibrium within the shortest amount of time. When damping ratio is less than 1
    160      * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without
    161      * any damping (i.e. damping ratio = 0), the mass will oscillate forever.
    162      * <p>
    163      * Default damping ratio is {@link #DAMPING_RATIO_MEDIUM_BOUNCY}.
    164      *
    165      * @param dampingRatio damping ratio of the spring, it should be non-negative
    166      * @return the spring force that the given damping ratio is set on
    167      * @throws IllegalArgumentException if the {@param dampingRatio} is negative.
    168      */
    169     public SpringForce setDampingRatio(@FloatRange(from = 0.0) float dampingRatio) {
    170         if (dampingRatio < 0) {
    171             throw new IllegalArgumentException("Damping ratio must be non-negative");
    172         }
    173         mDampingRatio = dampingRatio;
    174         // All the intermediate values need to be recalculated.
    175         mInitialized = false;
    176         return this;
    177     }
    178 
    179     /**
    180      * Returns the damping ratio of the spring.
    181      *
    182      * @return damping ratio of the spring
    183      */
    184     public float getDampingRatio() {
    185         return (float) mDampingRatio;
    186     }
    187 
    188     /**
    189      * Sets the rest position of the spring.
    190      *
    191      * @param finalPosition rest position of the spring
    192      * @return the spring force that the given final position is set on
    193      */
    194     public SpringForce setFinalPosition(float finalPosition) {
    195         mFinalPosition = finalPosition;
    196         return this;
    197     }
    198 
    199     /**
    200      * Returns the rest position of the spring.
    201      *
    202      * @return rest position of the spring
    203      */
    204     public float getFinalPosition() {
    205         return (float) mFinalPosition;
    206     }
    207 
    208     /*********************** Below are private APIs *********************/
    209 
    210     /**
    211      * @hide
    212      */
    213     @Override
    214     public float getAcceleration(float lastDisplacement, float lastVelocity) {
    215 
    216         lastDisplacement -= getFinalPosition();
    217 
    218         double k = mNaturalFreq * mNaturalFreq;
    219         double c = 2 * mNaturalFreq * mDampingRatio;
    220 
    221         return (float) (-k * lastDisplacement - c * lastVelocity);
    222     }
    223 
    224     /**
    225      * @hide
    226      */
    227     @Override
    228     public boolean isAtEquilibrium(float value, float velocity) {
    229         if (Math.abs(velocity) < mVelocityThreshold
    230                 && Math.abs(value - getFinalPosition()) < mValueThreshold) {
    231             return true;
    232         }
    233         return false;
    234     }
    235 
    236     /**
    237      * Initialize the string by doing the necessary pre-calculation as well as some sanity check
    238      * on the setup.
    239      *
    240      * @throws IllegalStateException if the final position is not yet set by the time the spring
    241      *                               animation has started
    242      */
    243     private void init() {
    244         if (mInitialized) {
    245             return;
    246         }
    247 
    248         if (mFinalPosition == UNSET) {
    249             throw new IllegalStateException("Error: Final position of the spring must be"
    250                     + " set before the animation starts");
    251         }
    252 
    253         if (mDampingRatio > 1) {
    254             // Over damping
    255             mGammaPlus = -mDampingRatio * mNaturalFreq
    256                     + mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
    257             mGammaMinus = -mDampingRatio * mNaturalFreq
    258                     - mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
    259         } else if (mDampingRatio >= 0 && mDampingRatio < 1) {
    260             // Under damping
    261             mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
    262         }
    263 
    264         mInitialized = true;
    265     }
    266 
    267     /**
    268      * Internal only call for Spring to calculate the spring position/velocity using
    269      * an analytical approach.
    270      */
    271     DynamicAnimation.MassState updateValues(double lastDisplacement, double lastVelocity,
    272             long timeElapsed) {
    273         init();
    274 
    275         double deltaT = timeElapsed / 1000d; // unit: seconds
    276         lastDisplacement -= mFinalPosition;
    277         double displacement;
    278         double currentVelocity;
    279         if (mDampingRatio > 1) {
    280             // Overdamped
    281             double coeffA =  lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity)
    282                     / (mGammaMinus - mGammaPlus);
    283             double coeffB =  (mGammaMinus * lastDisplacement - lastVelocity)
    284                     / (mGammaMinus - mGammaPlus);
    285             displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT)
    286                     + coeffB * Math.pow(Math.E, mGammaPlus * deltaT);
    287             currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT)
    288                     + coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT);
    289         } else if (mDampingRatio == 1) {
    290             // Critically damped
    291             double coeffA = lastDisplacement;
    292             double coeffB = lastVelocity + mNaturalFreq * lastDisplacement;
    293             displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT);
    294             currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT)
    295                     * (-mNaturalFreq) + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT);
    296         } else {
    297             // Underdamped
    298             double cosCoeff = lastDisplacement;
    299             double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq
    300                     * lastDisplacement + lastVelocity);
    301             displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
    302                     * (cosCoeff * Math.cos(mDampedFreq * deltaT)
    303                     + sinCoeff * Math.sin(mDampedFreq * deltaT));
    304             currentVelocity = displacement * (-mNaturalFreq) * mDampingRatio
    305                     + Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
    306                     * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT)
    307                     + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT));
    308         }
    309 
    310         mMassState.mValue = (float) (displacement + mFinalPosition);
    311         mMassState.mVelocity = (float) currentVelocity;
    312         return mMassState;
    313     }
    314 
    315     /**
    316      * This threshold defines how close the animation value needs to be before the animation can
    317      * finish. This default value is based on the property being animated, e.g. animations on alpha,
    318      * scale, translation or rotation would have different thresholds. This value should be small
    319      * enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that
    320      * animations take seconds to finish.
    321      *
    322      * @param threshold the difference between the animation value and final spring position that
    323      *                  is allowed to end the animation when velocity is very low
    324      */
    325     void setValueThreshold(double threshold) {
    326         mValueThreshold = Math.abs(threshold);
    327         mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER;
    328     }
    329 }
    330