1 /*
2  * Copyright (C) 2022 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 com.android.internal.dynamicanimation.animation;
18 
19 import android.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      */
SpringForce()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      */
SpringForce(float finalPosition)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      */
setStiffness( @loatRangefrom = 0.0, fromInclusive = false) float stiffness)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      */
getStiffness()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      */
setDampingRatio(@loatRangefrom = 0.0) float dampingRatio)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      */
getDampingRatio()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      */
setFinalPosition(float finalPosition)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      */
getFinalPosition()204     public float getFinalPosition() {
205         return (float) mFinalPosition;
206     }
207 
208     /*********************** Below are private APIs *********************/
209 
210     @Override
getAcceleration(float lastDisplacement, float lastVelocity)211     public float getAcceleration(float lastDisplacement, float lastVelocity) {
212 
213         lastDisplacement -= getFinalPosition();
214 
215         double k = mNaturalFreq * mNaturalFreq;
216         double c = 2 * mNaturalFreq * mDampingRatio;
217 
218         return (float) (-k * lastDisplacement - c * lastVelocity);
219     }
220 
221     @Override
isAtEquilibrium(float value, float velocity)222     public boolean isAtEquilibrium(float value, float velocity) {
223         if (Math.abs(velocity) < mVelocityThreshold
224                 && Math.abs(value - getFinalPosition()) < mValueThreshold) {
225             return true;
226         }
227         return false;
228     }
229 
230     /**
231      * Initialize the string by doing the necessary pre-calculation as well as some sanity check
232      * on the setup.
233      *
234      * @throws IllegalStateException if the final position is not yet set by the time the spring
235      *                               animation has started
236      */
init()237     private void init() {
238         if (mInitialized) {
239             return;
240         }
241 
242         if (mFinalPosition == UNSET) {
243             throw new IllegalStateException("Error: Final position of the spring must be"
244                     + " set before the animation starts");
245         }
246 
247         if (mDampingRatio > 1) {
248             // Over damping
249             mGammaPlus = -mDampingRatio * mNaturalFreq
250                     + mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
251             mGammaMinus = -mDampingRatio * mNaturalFreq
252                     - mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
253         } else if (mDampingRatio >= 0 && mDampingRatio < 1) {
254             // Under damping
255             mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
256         }
257 
258         mInitialized = true;
259     }
260 
261     /**
262      * Internal only call for Spring to calculate the spring position/velocity using
263      * an analytical approach.
264      */
updateValues(double lastDisplacement, double lastVelocity, long timeElapsed)265     DynamicAnimation.MassState updateValues(double lastDisplacement, double lastVelocity,
266             long timeElapsed) {
267         init();
268 
269         double deltaT = timeElapsed / 1000d; // unit: seconds
270         lastDisplacement -= mFinalPosition;
271         double displacement;
272         double currentVelocity;
273         if (mDampingRatio > 1) {
274             // Overdamped
275             double coeffA =  lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity)
276                     / (mGammaMinus - mGammaPlus);
277             double coeffB =  (mGammaMinus * lastDisplacement - lastVelocity)
278                     / (mGammaMinus - mGammaPlus);
279             displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT)
280                     + coeffB * Math.pow(Math.E, mGammaPlus * deltaT);
281             currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT)
282                     + coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT);
283         } else if (mDampingRatio == 1) {
284             // Critically damped
285             double coeffA = lastDisplacement;
286             double coeffB = lastVelocity + mNaturalFreq * lastDisplacement;
287             displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT);
288             currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT)
289                     * -mNaturalFreq + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT);
290         } else {
291             // Underdamped
292             double cosCoeff = lastDisplacement;
293             double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq
294                     * lastDisplacement + lastVelocity);
295             displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
296                     * (cosCoeff * Math.cos(mDampedFreq * deltaT)
297                     + sinCoeff * Math.sin(mDampedFreq * deltaT));
298             currentVelocity = displacement * -mNaturalFreq * mDampingRatio
299                     + Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
300                     * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT)
301                     + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT));
302         }
303 
304         mMassState.mValue = (float) (displacement + mFinalPosition);
305         mMassState.mVelocity = (float) currentVelocity;
306         return mMassState;
307     }
308 
309     /**
310      * This threshold defines how close the animation value needs to be before the animation can
311      * finish. This default value is based on the property being animated, e.g. animations on alpha,
312      * scale, translation or rotation would have different thresholds. This value should be small
313      * enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that
314      * animations take seconds to finish.
315      *
316      * @param threshold the difference between the animation value and final spring position that
317      *                  is allowed to end the animation when velocity is very low
318      */
setValueThreshold(double threshold)319     void setValueThreshold(double threshold) {
320         mValueThreshold = Math.abs(threshold);
321         mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER;
322     }
323 }
324