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.util.AndroidRuntimeException;
20 import android.util.FloatProperty;
21 
22 /**
23  * SpringAnimation is an animation that is driven by a {@link SpringForce}. The spring force defines
24  * the spring's stiffness, damping ratio, as well as the rest position. Once the SpringAnimation is
25  * started, on each frame the spring force will update the animation's value and velocity.
26  * The animation will continue to run until the spring force reaches equilibrium. If the spring used
27  * in the animation is undamped, the animation will never reach equilibrium. Instead, it will
28  * oscillate forever.
29  *
30  * <div class="special reference">
31  * <h3>Developer Guides</h3>
32  * </div>
33  *
34  * <p>To create a simple {@link SpringAnimation} that uses the default {@link SpringForce}:</p>
35  * <pre class="prettyprint">
36  * // Create an animation to animate view's X property, set the rest position of the
37  * // default spring to 0, and start the animation with a starting velocity of 5000 (pixel/s).
38  * final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.X, 0)
39  *         .setStartVelocity(5000);
40  * anim.start();
41  * </pre>
42  *
43  * <p>Alternatively, a {@link SpringAnimation} can take a pre-configured {@link SpringForce}, and
44  * use that to drive the animation. </p>
45  * <pre class="prettyprint">
46  * // Create a low stiffness, low bounce spring at position 0.
47  * SpringForce spring = new SpringForce(0)
48  *         .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
49  *         .setStiffness(SpringForce.STIFFNESS_LOW);
50  * // Create an animation to animate view's scaleY property, and start the animation using
51  * // the spring above and a starting value of 0.5. Additionally, constrain the range of value for
52  * // the animation to be non-negative, effectively preventing any spring overshoot.
53  * final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.SCALE_Y)
54  *         .setMinValue(0).setSpring(spring).setStartValue(1);
55  * anim.start();
56  * </pre>
57  */
58 public final class SpringAnimation extends DynamicAnimation<SpringAnimation> {
59 
60     private SpringForce mSpring = null;
61     private float mPendingPosition = UNSET;
62     private static final float UNSET = Float.MAX_VALUE;
63     private boolean mEndRequested = false;
64 
65     /**
66      * <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During
67      * the animation, the {@link FloatValueHolder} instance will be updated via
68      * {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
69      * animation value via {@link FloatValueHolder#getValue()}.
70      *
71      * <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
72      * {@link FloatValueHolder#setValue(float)} outside of the animation during an
73      * animation run will not have any effect on the on-going animation.
74      *
75      * @param floatValueHolder the property to be animated
76      */
SpringAnimation(FloatValueHolder floatValueHolder)77     public SpringAnimation(FloatValueHolder floatValueHolder) {
78         super(floatValueHolder);
79     }
80 
81     /**
82      * <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During
83      * the animation, the {@link FloatValueHolder} instance will be updated via
84      * {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
85      * animation value via {@link FloatValueHolder#getValue()}.
86      *
87      * A Spring will be created with the given final position and default stiffness and damping
88      * ratio. This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
89      *
90      * <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
91      * {@link FloatValueHolder#setValue(float)} outside of the animation during an
92      * animation run will not have any effect on the on-going animation.
93      *
94      * @param floatValueHolder the property to be animated
95      * @param finalPosition the final position of the spring to be created.
96      */
SpringAnimation(FloatValueHolder floatValueHolder, float finalPosition)97     public SpringAnimation(FloatValueHolder floatValueHolder, float finalPosition) {
98         super(floatValueHolder);
99         mSpring = new SpringForce(finalPosition);
100     }
101 
102     /**
103      * This creates a SpringAnimation that animates the property of the given object.
104      * Note, a spring will need to setup through {@link #setSpring(SpringForce)} before
105      * the animation starts.
106      *
107      * @param object the Object whose property will be animated
108      * @param property the property to be animated
109      * @param <K> the class on which the Property is declared
110      */
SpringAnimation(K object, FloatProperty<K> property)111     public <K> SpringAnimation(K object, FloatProperty<K> property) {
112         super(object, property);
113     }
114 
115     /**
116      * This creates a SpringAnimation that animates the property of the given object. A Spring will
117      * be created with the given final position and default stiffness and damping ratio.
118      * This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
119      *
120      * @param object the Object whose property will be animated
121      * @param property the property to be animated
122      * @param finalPosition the final position of the spring to be created.
123      * @param <K> the class on which the Property is declared
124      */
SpringAnimation(K object, FloatProperty<K> property, float finalPosition)125     public <K> SpringAnimation(K object, FloatProperty<K> property,
126             float finalPosition) {
127         super(object, property);
128         mSpring = new SpringForce(finalPosition);
129     }
130 
131     /**
132      * Returns the spring that the animation uses for animations.
133      *
134      * @return the spring that the animation uses for animations
135      */
getSpring()136     public SpringForce getSpring() {
137         return mSpring;
138     }
139 
140     /**
141      * Uses the given spring as the force that drives this animation. If this spring force has its
142      * parameters re-configured during the animation, the new configuration will be reflected in the
143      * animation immediately.
144      *
145      * @param force a pre-defined spring force that drives the animation
146      * @return the animation that the spring force is set on
147      */
setSpring(SpringForce force)148     public SpringAnimation setSpring(SpringForce force) {
149         mSpring = force;
150         return this;
151     }
152 
153     @Override
start()154     public void start() {
155         sanityCheck();
156         mSpring.setValueThreshold(getValueThreshold());
157         super.start();
158     }
159 
160     /**
161      * Updates the final position of the spring.
162      * <p/>
163      * When the animation is running, calling this method would assume the position change of the
164      * spring as a continuous movement since last frame, which yields more accurate results than
165      * changing the spring position directly through {@link SpringForce#setFinalPosition(float)}.
166      * <p/>
167      * If the animation hasn't started, calling this method will change the spring position, and
168      * immediately start the animation.
169      *
170      * @param finalPosition rest position of the spring
171      */
animateToFinalPosition(float finalPosition)172     public void animateToFinalPosition(float finalPosition) {
173         if (isRunning()) {
174             mPendingPosition = finalPosition;
175         } else {
176             if (mSpring == null) {
177                 mSpring = new SpringForce(finalPosition);
178             }
179             mSpring.setFinalPosition(finalPosition);
180             start();
181         }
182     }
183 
184     /**
185      * Cancels the on-going animation. If the animation hasn't started, no op. Note that this method
186      * should only be called on main thread.
187      *
188      * @throws AndroidRuntimeException if this method is not called on the main thread
189      */
190     @Override
cancel()191     public void cancel() {
192         super.cancel();
193         if (mPendingPosition != UNSET) {
194             if (mSpring == null) {
195                 mSpring = new SpringForce(mPendingPosition);
196             } else {
197                 mSpring.setFinalPosition(mPendingPosition);
198             }
199             mPendingPosition = UNSET;
200         }
201     }
202 
203     /**
204      * Skips to the end of the animation. If the spring is undamped, an
205      * {@link IllegalStateException} will be thrown, as the animation would never reach to an end.
206      * It is recommended to check {@link #canSkipToEnd()} before calling this method. If animation
207      * is not running, no-op.
208      *
209      * Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
210      * is created on the same thread as the first call to start/cancel an animation. All the
211      * subsequent animation lifecycle manipulations need to be on that same thread, until the
212      * AnimationHandler is reset (using [setAnimationHandler]).
213      *
214      * @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0)
215      * @throws AndroidRuntimeException if this method is not called on the same thread as the
216      * animation handler
217      */
skipToEnd()218     public void skipToEnd() {
219         if (!canSkipToEnd()) {
220             throw new UnsupportedOperationException("Spring animations can only come to an end"
221                     + " when there is damping");
222         }
223         if (!isCurrentThread()) {
224             throw new AndroidRuntimeException("Animations may only be started on the same thread "
225                     + "as the animation handler");
226         }
227         if (mRunning) {
228             mEndRequested = true;
229         }
230     }
231 
232     /**
233      * Queries whether the spring can eventually come to the rest position.
234      *
235      * @return {@code true} if the spring is damped, otherwise {@code false}
236      */
canSkipToEnd()237     public boolean canSkipToEnd() {
238         return mSpring.mDampingRatio > 0;
239     }
240 
241     /************************ Below are private APIs *************************/
242 
sanityCheck()243     private void sanityCheck() {
244         if (mSpring == null) {
245             throw new UnsupportedOperationException("Incomplete SpringAnimation: Either final"
246                     + " position or a spring force needs to be set.");
247         }
248         double finalPosition = mSpring.getFinalPosition();
249         if (finalPosition > mMaxValue) {
250             throw new UnsupportedOperationException("Final position of the spring cannot be greater"
251                     + " than the max value.");
252         } else if (finalPosition < mMinValue) {
253             throw new UnsupportedOperationException("Final position of the spring cannot be less"
254                     + " than the min value.");
255         }
256     }
257 
258     @Override
updateValueAndVelocity(long deltaT)259     boolean updateValueAndVelocity(long deltaT) {
260         // If user had requested end, then update the value and velocity to end state and consider
261         // animation done.
262         if (mEndRequested) {
263             if (mPendingPosition != UNSET) {
264                 mSpring.setFinalPosition(mPendingPosition);
265                 mPendingPosition = UNSET;
266             }
267             mValue = mSpring.getFinalPosition();
268             mVelocity = 0;
269             mEndRequested = false;
270             return true;
271         }
272 
273         if (mPendingPosition != UNSET) {
274             // Approximate by considering half of the time spring position stayed at the old
275             // position, half of the time it's at the new position.
276             MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2);
277             mSpring.setFinalPosition(mPendingPosition);
278             mPendingPosition = UNSET;
279 
280             massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2);
281             mValue = massState.mValue;
282             mVelocity = massState.mVelocity;
283 
284         } else {
285             MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT);
286             mValue = massState.mValue;
287             mVelocity = massState.mVelocity;
288         }
289 
290         mValue = Math.max(mValue, mMinValue);
291         mValue = Math.min(mValue, mMaxValue);
292 
293         if (isAtEquilibrium(mValue, mVelocity)) {
294             mValue = mSpring.getFinalPosition();
295             mVelocity = 0f;
296             return true;
297         }
298         return false;
299     }
300 
301     @Override
getAcceleration(float value, float velocity)302     float getAcceleration(float value, float velocity) {
303         return mSpring.getAcceleration(value, velocity);
304     }
305 
306     @Override
isAtEquilibrium(float value, float velocity)307     boolean isAtEquilibrium(float value, float velocity) {
308         return mSpring.isAtEquilibrium(value, velocity);
309     }
310 
311     @Override
setValueThreshold(float threshold)312     void setValueThreshold(float threshold) {
313     }
314 }
315