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 android.os.Looper;
20import 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 */
58public 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     */
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     */
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     */
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     */
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     */
127    public SpringAnimation setSpring(SpringForce force) {
128        mSpring = force;
129        return this;
130    }
131
132    @Override
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     */
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     */
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     */
190    public boolean canSkipToEnd() {
191        return mSpring.mDampingRatio > 0;
192    }
193
194    /************************ Below are private APIs *************************/
195
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
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
256    float getAcceleration(float value, float velocity) {
257        return mSpring.getAcceleration(value, velocity);
258    }
259
260    @Override
261    boolean isAtEquilibrium(float value, float velocity) {
262        return mSpring.isAtEquilibrium(value, velocity);
263    }
264
265    @Override
266    void setValueThreshold(float threshold) {
267    }
268}
269