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 android.os.Looper;
20 import android.util.AndroidRuntimeException;
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      * This creates a SpringAnimation that animates the property of the given object.
83      * Note, a spring will need to setup through {@link #setSpring(SpringForce)} before
84      * the animation starts.
85      *
86      * @param object the Object whose property will be animated
87      * @param property the property to be animated
88      * @param <K> the class on which the Property is declared
89      */
SpringAnimation(K object, FloatPropertyCompat<K> property)90     public <K> SpringAnimation(K object, FloatPropertyCompat<K> property) {
91         super(object, property);
92     }
93 
94     /**
95      * This creates a SpringAnimation that animates the property of the given object. A Spring will
96      * be created with the given final position and default stiffness and damping ratio.
97      * This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
98      *
99      * @param object the Object whose property will be animated
100      * @param property the property to be animated
101      * @param finalPosition the final position of the spring to be created.
102      * @param <K> the class on which the Property is declared
103      */
SpringAnimation(K object, FloatPropertyCompat<K> property, float finalPosition)104     public <K> SpringAnimation(K object, FloatPropertyCompat<K> property,
105             float finalPosition) {
106         super(object, property);
107         mSpring = new SpringForce(finalPosition);
108     }
109 
110     /**
111      * Returns the spring that the animation uses for animations.
112      *
113      * @return the spring that the animation uses for animations
114      */
getSpring()115     public SpringForce getSpring() {
116         return mSpring;
117     }
118 
119     /**
120      * Uses the given spring as the force that drives this animation. If this spring force has its
121      * parameters re-configured during the animation, the new configuration will be reflected in the
122      * animation immediately.
123      *
124      * @param force a pre-defined spring force that drives the animation
125      * @return the animation that the spring force is set on
126      */
setSpring(SpringForce force)127     public SpringAnimation setSpring(SpringForce force) {
128         mSpring = force;
129         return this;
130     }
131 
132     @Override
start()133     public void start() {
134         sanityCheck();
135         mSpring.setValueThreshold(getValueThreshold());
136         super.start();
137     }
138 
139     /**
140      * Updates the final position of the spring.
141      * <p/>
142      * When the animation is running, calling this method would assume the position change of the
143      * spring as a continuous movement since last frame, which yields more accurate results than
144      * changing the spring position directly through {@link SpringForce#setFinalPosition(float)}.
145      * <p/>
146      * If the animation hasn't started, calling this method will change the spring position, and
147      * immediately start the animation.
148      *
149      * @param finalPosition rest position of the spring
150      */
animateToFinalPosition(float finalPosition)151     public void animateToFinalPosition(float finalPosition) {
152         if (isRunning()) {
153             mPendingPosition = finalPosition;
154         } else {
155             if (mSpring == null) {
156                 mSpring = new SpringForce(finalPosition);
157             }
158             mSpring.setFinalPosition(finalPosition);
159             start();
160         }
161     }
162 
163     /**
164      * Skips to the end of the animation. If the spring is undamped, an
165      * {@link IllegalStateException} will be thrown, as the animation would never reach to an end.
166      * It is recommended to check {@link #canSkipToEnd()} before calling this method. This method
167      * should only be called on main thread. If animation is not running, no-op.
168      *
169      * @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0)
170      * @throws AndroidRuntimeException if this method is not called on the main thread
171      */
skipToEnd()172     public void skipToEnd() {
173         if (!canSkipToEnd()) {
174             throw new UnsupportedOperationException("Spring animations can only come to an end"
175                     + " when there is damping");
176         }
177         if (Looper.myLooper() != Looper.getMainLooper()) {
178             throw new AndroidRuntimeException("Animations may only be started on the main thread");
179         }
180         if (mRunning) {
181             mEndRequested = true;
182         }
183     }
184 
185     /**
186      * Queries whether the spring can eventually come to the rest position.
187      *
188      * @return {@code true} if the spring is damped, otherwise {@code false}
189      */
canSkipToEnd()190     public boolean canSkipToEnd() {
191         return mSpring.mDampingRatio > 0;
192     }
193 
194     /************************ Below are private APIs *************************/
195 
sanityCheck()196     private void sanityCheck() {
197         if (mSpring == null) {
198             throw new UnsupportedOperationException("Incomplete SpringAnimation: Either final"
199                     + " position or a spring force needs to be set.");
200         }
201         double finalPosition = mSpring.getFinalPosition();
202         if (finalPosition > mMaxValue) {
203             throw new UnsupportedOperationException("Final position of the spring cannot be greater"
204                     + " than the max value.");
205         } else if (finalPosition < mMinValue) {
206             throw new UnsupportedOperationException("Final position of the spring cannot be less"
207                     + " than the min value.");
208         }
209     }
210 
211     @Override
updateValueAndVelocity(long deltaT)212     boolean updateValueAndVelocity(long deltaT) {
213         // If user had requested end, then update the value and velocity to end state and consider
214         // animation done.
215         if (mEndRequested) {
216             if (mPendingPosition != UNSET) {
217                 mSpring.setFinalPosition(mPendingPosition);
218                 mPendingPosition = UNSET;
219             }
220             mValue = mSpring.getFinalPosition();
221             mVelocity = 0;
222             mEndRequested = false;
223             return true;
224         }
225 
226         if (mPendingPosition != UNSET) {
227             double lastPosition = mSpring.getFinalPosition();
228             // Approximate by considering half of the time spring position stayed at the old
229             // position, half of the time it's at the new position.
230             MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2);
231             mSpring.setFinalPosition(mPendingPosition);
232             mPendingPosition = UNSET;
233 
234             massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2);
235             mValue = massState.mValue;
236             mVelocity = massState.mVelocity;
237 
238         } else {
239             MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT);
240             mValue = massState.mValue;
241             mVelocity = massState.mVelocity;
242         }
243 
244         mValue = Math.max(mValue, mMinValue);
245         mValue = Math.min(mValue, mMaxValue);
246 
247         if (isAtEquilibrium(mValue, mVelocity)) {
248             mValue = mSpring.getFinalPosition();
249             mVelocity = 0f;
250             return true;
251         }
252         return false;
253     }
254 
255     @Override
getAcceleration(float value, float velocity)256     float getAcceleration(float value, float velocity) {
257         return mSpring.getAcceleration(value, velocity);
258     }
259 
260     @Override
isAtEquilibrium(float value, float velocity)261     boolean isAtEquilibrium(float value, float velocity) {
262         return mSpring.isAtEquilibrium(value, velocity);
263     }
264 
265     @Override
setValueThreshold(float threshold)266     void setValueThreshold(float threshold) {
267     }
268 }
269