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