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
17package androidx.dynamicanimation.animation;
18
19import androidx.annotation.FloatRange;
20import 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 */
36public 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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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     */
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     */
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     */
328    void setValueThreshold(double threshold) {
329        mValueThreshold = Math.abs(threshold);
330        mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER;
331    }
332}
333