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 android.support.animation;
18
19import android.support.annotation.FloatRange;
20
21/**
22 * <p>Fling animation is an animation that continues an initial momentum (most often from gesture
23 * velocity) and gradually slows down. The fling animation will come to a stop when the velocity of
24 * the animation is below the threshold derived from {@link #setMinimumVisibleChange(float)},
25 * or when the value of the animation has gone beyond the min or max value defined via
26 * {@link DynamicAnimation#setMinValue(float)} or {@link DynamicAnimation#setMaxValue(float)}.
27 * It is recommended to restrict the fling animation with min and/or max value, such that the
28 * animation can end when it goes beyond screen bounds, thus preserving CPU cycles and resources.
29 *
30 * <p>For example, you can create a fling animation that animates the translationX of a view:
31 * <pre class="prettyprint">
32 * FlingAnimation flingAnim = new FlingAnimation(view, DynamicAnimation.TRANSLATION_X)
33 *         // Sets the start velocity to -2000 (pixel/s)
34 *         .setStartVelocity(-2000)
35 *         // Optional but recommended to set a reasonable min and max range for the animation.
36 *         // In this particular case, we set the min and max to -200 and 2000 respectively.
37 *         .setMinValue(-200).setMaxValue(2000);
38 * flingAnim.start();
39 * </pre>
40 */
41public final class FlingAnimation extends DynamicAnimation<FlingAnimation> {
42
43    private final DragForce mFlingForce = new DragForce();
44
45    /**
46     * <p>This creates a FlingAnimation that animates a {@link FloatValueHolder} instance. During
47     * the animation, the {@link FloatValueHolder} instance will be updated via
48     * {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
49     * animation value via {@link FloatValueHolder#getValue()}.
50     *
51     * <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
52     * {@link FloatValueHolder#setValue(float)} outside of the animation during an
53     * animation run will not have any effect on the on-going animation.
54     *
55     * @param floatValueHolder the property to be animated
56     */
57    public FlingAnimation(FloatValueHolder floatValueHolder) {
58        super(floatValueHolder);
59        mFlingForce.setValueThreshold(getValueThreshold());
60    }
61
62    /**
63     * This creates a FlingAnimation that animates the property of the given object.
64     *
65     * @param object the Object whose property will be animated
66     * @param property the property to be animated
67     * @param <K> the class on which the property is declared
68     */
69    public <K> FlingAnimation(K object, FloatPropertyCompat<K> property) {
70        super(object, property);
71        mFlingForce.setValueThreshold(getValueThreshold());
72    }
73
74    /**
75     * Sets the friction for the fling animation. The greater the friction is, the sooner the
76     * animation will slow down. When not set, the friction defaults to 1.
77     *
78     * @param friction the friction used in the animation
79     * @return the animation whose friction will be scaled
80     * @throws IllegalArgumentException if the input friction is not positive
81     */
82    public FlingAnimation setFriction(
83            @FloatRange(from = 0.0, fromInclusive = false) float friction) {
84        if (friction <= 0) {
85            throw new IllegalArgumentException("Friction must be positive");
86        }
87        mFlingForce.setFrictionScalar(friction);
88        return this;
89    }
90
91    /**
92     * Returns the friction being set on the animation via {@link #setFriction(float)}. If the
93     * friction has not been set, the default friction of 1 will be returned.
94     *
95     * @return friction being used in the animation
96     */
97    public float getFriction() {
98        return mFlingForce.getFrictionScalar();
99    }
100
101    /**
102     * Sets the min value of the animation. When a fling animation reaches the min value, the
103     * animation will end immediately. Animations will not animate beyond the min value.
104     *
105     * @param minValue minimum value of the property to be animated
106     * @return the Animation whose min value is being set
107     */
108    @Override
109    public FlingAnimation setMinValue(float minValue) {
110        super.setMinValue(minValue);
111        return this;
112    }
113
114    /**
115     * Sets the max value of the animation. When a fling animation reaches the max value, the
116     * animation will end immediately. Animations will not animate beyond the max value.
117     *
118     * @param maxValue maximum value of the property to be animated
119     * @return the Animation whose max value is being set
120     */
121    @Override
122    public FlingAnimation setMaxValue(float maxValue) {
123        super.setMaxValue(maxValue);
124        return this;
125    }
126
127    /**
128     * Start velocity of the animation. Default velocity is 0. Unit: pixel/second
129     *
130     * <p>A <b>non-zero</b> start velocity is required for a FlingAnimation. If no start velocity is
131     * set through {@link #setStartVelocity(float)}, the start velocity defaults to 0. In that
132     * case, the fling animation will consider itself done in the next frame.
133     *
134     * <p>Note when using a fixed value as the start velocity (as opposed to getting the velocity
135     * through touch events), it is recommended to define such a value in dp/second and convert it
136     * to pixel/second based on the density of the screen to achieve a consistent look across
137     * different screens.
138     *
139     * <p>To convert from dp/second to pixel/second:
140     * <pre class="prettyprint">
141     * float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond,
142     *         getResources().getDisplayMetrics());
143     * </pre>
144     *
145     * @param startVelocity start velocity of the animation in pixel/second
146     * @return the Animation whose start velocity is being set
147     */
148    @Override
149    public FlingAnimation setStartVelocity(float startVelocity) {
150        super.setStartVelocity(startVelocity);
151        return this;
152    }
153
154    @Override
155    boolean updateValueAndVelocity(long deltaT) {
156
157        MassState state = mFlingForce.updateValueAndVelocity(mValue, mVelocity, deltaT);
158        mValue = state.mValue;
159        mVelocity = state.mVelocity;
160
161        // When the animation hits the max/min value, consider animation done.
162        if (mValue < mMinValue) {
163            mValue = mMinValue;
164            return true;
165        }
166        if (mValue > mMaxValue) {
167            mValue = mMaxValue;
168            return true;
169        }
170
171        if (isAtEquilibrium(mValue, mVelocity)) {
172            return true;
173        }
174        return false;
175    }
176
177    @Override
178    float getAcceleration(float value, float velocity) {
179        return mFlingForce.getAcceleration(value, velocity);
180    }
181
182    @Override
183    boolean isAtEquilibrium(float value, float velocity) {
184        return value >= mMaxValue
185                || value <= mMinValue
186                || mFlingForce.isAtEquilibrium(value, velocity);
187    }
188
189    @Override
190    void setValueThreshold(float threshold) {
191        mFlingForce.setValueThreshold(threshold);
192    }
193
194    private static final class DragForce implements Force {
195
196        private static final float DEFAULT_FRICTION = -4.2f;
197
198        // This multiplier is used to calculate the velocity threshold given a certain value
199        // threshold. The idea is that if it takes >= 1 frame to move the value threshold amount,
200        // then the velocity is a reasonable threshold.
201        private static final float VELOCITY_THRESHOLD_MULTIPLIER = 1000f / 16f;
202        private float mFriction = DEFAULT_FRICTION;
203        private float mVelocityThreshold;
204
205        // Internal state to hold a value/velocity pair.
206        private final DynamicAnimation.MassState mMassState = new DynamicAnimation.MassState();
207
208        void setFrictionScalar(float frictionScalar) {
209            mFriction = frictionScalar * DEFAULT_FRICTION;
210        }
211
212        float getFrictionScalar() {
213            return mFriction / DEFAULT_FRICTION;
214        }
215
216        MassState updateValueAndVelocity(float value, float velocity, long deltaT) {
217            mMassState.mVelocity = (float) (velocity * Math.exp((deltaT / 1000f) * mFriction));
218            mMassState.mValue = (float) (value - velocity / mFriction
219                    + velocity / mFriction * Math.exp(mFriction * deltaT / 1000f));
220            if (isAtEquilibrium(mMassState.mValue, mMassState.mVelocity)) {
221                mMassState.mVelocity = 0f;
222            }
223            return mMassState;
224        }
225
226        @Override
227        public float getAcceleration(float position, float velocity) {
228            return velocity * mFriction;
229        }
230
231        @Override
232        public boolean isAtEquilibrium(float value, float velocity) {
233            return Math.abs(velocity) < mVelocityThreshold;
234        }
235
236        void setValueThreshold(float threshold) {
237            mVelocityThreshold = threshold * VELOCITY_THRESHOLD_MULTIPLIER;
238        }
239    }
240
241}
242