FlingAnimationUtils.java revision 4c6969a512cd70831249ec1d07691f16fe5465f5
1/*
2 * Copyright (C) 2014 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 com.android.systemui.statusbar;
18
19import android.animation.ValueAnimator;
20import android.content.Context;
21import android.view.ViewPropertyAnimator;
22import android.view.animation.AnimationUtils;
23import android.view.animation.Interpolator;
24import android.view.animation.PathInterpolator;
25
26/**
27 * Utility class to calculate general fling animation when the finger is released.
28 */
29public class FlingAnimationUtils {
30
31    private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f;
32    private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.7f;
33    private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 1f;
34    private static final float MIN_VELOCITY_DP_PER_SECOND = 250;
35    private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000;
36
37    /**
38     * Crazy math. http://en.wikipedia.org/wiki/B%C3%A9zier_curve
39     */
40    private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 1.0f / LINEAR_OUT_SLOW_IN_X2;
41
42    private Interpolator mLinearOutSlowIn;
43    private Interpolator mFastOutSlowIn;
44    private Interpolator mFastOutLinearIn;
45
46    private float mMinVelocityPxPerSecond;
47    private float mMaxLengthSeconds;
48    private float mHighVelocityPxPerSecond;
49
50    private AnimatorProperties mAnimatorProperties = new AnimatorProperties();
51
52    public FlingAnimationUtils(Context ctx, float maxLengthSeconds) {
53        mMaxLengthSeconds = maxLengthSeconds;
54        mLinearOutSlowIn = new PathInterpolator(0, 0, LINEAR_OUT_SLOW_IN_X2, 1);
55        mFastOutSlowIn
56                = AnimationUtils.loadInterpolator(ctx, android.R.interpolator.fast_out_slow_in);
57        mFastOutLinearIn
58                = AnimationUtils.loadInterpolator(ctx, android.R.interpolator.fast_out_linear_in);
59        mMinVelocityPxPerSecond
60                = MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
61        mHighVelocityPxPerSecond
62                = HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
63    }
64
65    /**
66     * Applies the interpolator and length to the animator, such that the fling animation is
67     * consistent with the finger motion.
68     *
69     * @param animator the animator to apply
70     * @param currValue the current value
71     * @param endValue the end value of the animator
72     * @param velocity the current velocity of the motion
73     */
74    public void apply(ValueAnimator animator, float currValue, float endValue, float velocity) {
75        apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
76    }
77
78    /**
79     * Applies the interpolator and length to the animator, such that the fling animation is
80     * consistent with the finger motion.
81     *
82     * @param animator the animator to apply
83     * @param currValue the current value
84     * @param endValue the end value of the animator
85     * @param velocity the current velocity of the motion
86     */
87    public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
88            float velocity) {
89        apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
90    }
91
92    /**
93     * Applies the interpolator and length to the animator, such that the fling animation is
94     * consistent with the finger motion.
95     *
96     * @param animator the animator to apply
97     * @param currValue the current value
98     * @param endValue the end value of the animator
99     * @param velocity the current velocity of the motion
100     * @param maxDistance the maximum distance for this interaction; the maximum animation length
101     *                    gets multiplied by the ratio between the actual distance and this value
102     */
103    public void apply(ValueAnimator animator, float currValue, float endValue, float velocity,
104            float maxDistance) {
105        AnimatorProperties properties = getProperties(currValue, endValue, velocity,
106                maxDistance);
107        animator.setDuration(properties.duration);
108        animator.setInterpolator(properties.interpolator);
109    }
110
111    /**
112     * Applies the interpolator and length to the animator, such that the fling animation is
113     * consistent with the finger motion.
114     *
115     * @param animator the animator to apply
116     * @param currValue the current value
117     * @param endValue the end value of the animator
118     * @param velocity the current velocity of the motion
119     * @param maxDistance the maximum distance for this interaction; the maximum animation length
120     *                    gets multiplied by the ratio between the actual distance and this value
121     */
122    public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
123            float velocity, float maxDistance) {
124        AnimatorProperties properties = getProperties(currValue, endValue, velocity,
125                maxDistance);
126        animator.setDuration(properties.duration);
127        animator.setInterpolator(properties.interpolator);
128    }
129
130    private AnimatorProperties getProperties(float currValue,
131            float endValue, float velocity, float maxDistance) {
132        float maxLengthSeconds = (float) (mMaxLengthSeconds
133                * Math.sqrt(Math.abs(endValue - currValue) / maxDistance));
134        float diff = Math.abs(endValue - currValue);
135        float velAbs = Math.abs(velocity);
136        float durationSeconds = LINEAR_OUT_SLOW_IN_START_GRADIENT * diff / velAbs;
137        if (durationSeconds <= maxLengthSeconds) {
138            mAnimatorProperties.interpolator = mLinearOutSlowIn;
139        } else if (velAbs >= mMinVelocityPxPerSecond) {
140
141            // Cross fade between fast-out-slow-in and linear interpolator with current velocity.
142            durationSeconds = maxLengthSeconds;
143            VelocityInterpolator velocityInterpolator
144                    = new VelocityInterpolator(durationSeconds, velAbs, diff);
145            InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
146                    velocityInterpolator, mLinearOutSlowIn, mLinearOutSlowIn);
147            mAnimatorProperties.interpolator = superInterpolator;
148        } else {
149
150            // Just use a normal interpolator which doesn't take the velocity into account.
151            durationSeconds = maxLengthSeconds;
152            mAnimatorProperties.interpolator = mFastOutSlowIn;
153        }
154        mAnimatorProperties.duration = (long) (durationSeconds * 1000);
155        return mAnimatorProperties;
156    }
157
158    /**
159     * Applies the interpolator and length to the animator, such that the fling animation is
160     * consistent with the finger motion for the case when the animation is making something
161     * disappear.
162     *
163     * @param animator the animator to apply
164     * @param currValue the current value
165     * @param endValue the end value of the animator
166     * @param velocity the current velocity of the motion
167     * @param maxDistance the maximum distance for this interaction; the maximum animation length
168     *                    gets multiplied by the ratio between the actual distance and this value
169     */
170    public void applyDismissing(ValueAnimator animator, float currValue, float endValue,
171            float velocity, float maxDistance) {
172        AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
173                maxDistance);
174        animator.setDuration(properties.duration);
175        animator.setInterpolator(properties.interpolator);
176    }
177
178    /**
179     * Applies the interpolator and length to the animator, such that the fling animation is
180     * consistent with the finger motion for the case when the animation is making something
181     * disappear.
182     *
183     * @param animator the animator to apply
184     * @param currValue the current value
185     * @param endValue the end value of the animator
186     * @param velocity the current velocity of the motion
187     * @param maxDistance the maximum distance for this interaction; the maximum animation length
188     *                    gets multiplied by the ratio between the actual distance and this value
189     */
190    public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue,
191            float velocity, float maxDistance) {
192        AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
193                maxDistance);
194        animator.setDuration(properties.duration);
195        animator.setInterpolator(properties.interpolator);
196    }
197
198    private AnimatorProperties getDismissingProperties(float currValue, float endValue,
199            float velocity, float maxDistance) {
200        float maxLengthSeconds = (float) (mMaxLengthSeconds
201                * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f));
202        float diff = Math.abs(endValue - currValue);
203        float velAbs = Math.abs(velocity);
204        float y2 = calculateLinearOutFasterInY2(velAbs);
205
206        // The gradient at the start of the curve is just y2.
207        float startGradient = y2;
208        Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, 1, y2);
209        float durationSeconds = startGradient * diff / velAbs;
210        if (durationSeconds <= maxLengthSeconds) {
211            mAnimatorProperties.interpolator = mLinearOutFasterIn;
212        } else if (velAbs >= mMinVelocityPxPerSecond) {
213
214            // Cross fade between linear-out-faster-in and linear interpolator with current
215            // velocity.
216            durationSeconds = maxLengthSeconds;
217            VelocityInterpolator velocityInterpolator
218                    = new VelocityInterpolator(durationSeconds, velAbs, diff);
219            InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
220                    velocityInterpolator, mLinearOutFasterIn, mLinearOutSlowIn);
221            mAnimatorProperties.interpolator = superInterpolator;
222        } else {
223
224            // Just use a normal interpolator which doesn't take the velocity into account.
225            durationSeconds = maxLengthSeconds;
226            mAnimatorProperties.interpolator = mFastOutLinearIn;
227        }
228        mAnimatorProperties.duration = (long) (durationSeconds * 1000);
229        return mAnimatorProperties;
230    }
231
232    /**
233     * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
234     * velocity. The faster the velocity, the more "linear" the interpolator gets.
235     *
236     * @param velocity the velocity of the gesture.
237     * @return the y2 control point for a cubic bezier path interpolator
238     */
239    private float calculateLinearOutFasterInY2(float velocity) {
240        float t = (velocity - mMinVelocityPxPerSecond)
241                / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond);
242        t = Math.max(0, Math.min(1, t));
243        return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
244    }
245
246    /**
247     * @return the minimum velocity a gesture needs to have to be considered a fling
248     */
249    public float getMinVelocityPxPerSecond() {
250        return mMinVelocityPxPerSecond;
251    }
252
253    /**
254     * An interpolator which interpolates two interpolators with an interpolator.
255     */
256    private static final class InterpolatorInterpolator implements Interpolator {
257
258        private Interpolator mInterpolator1;
259        private Interpolator mInterpolator2;
260        private Interpolator mCrossfader;
261
262        InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2,
263                Interpolator crossfader) {
264            mInterpolator1 = interpolator1;
265            mInterpolator2 = interpolator2;
266            mCrossfader = crossfader;
267        }
268
269        @Override
270        public float getInterpolation(float input) {
271            float t = mCrossfader.getInterpolation(input);
272            return (1 - t) * mInterpolator1.getInterpolation(input)
273                    + t * mInterpolator2.getInterpolation(input);
274        }
275    }
276
277    /**
278     * An interpolator which interpolates with a fixed velocity.
279     */
280    private static final class VelocityInterpolator implements Interpolator {
281
282        private float mDurationSeconds;
283        private float mVelocity;
284        private float mDiff;
285
286        private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
287            mDurationSeconds = durationSeconds;
288            mVelocity = velocity;
289            mDiff = diff;
290        }
291
292        @Override
293        public float getInterpolation(float input) {
294            float time = input * mDurationSeconds;
295            return time * mVelocity / mDiff;
296        }
297    }
298
299    private static class AnimatorProperties {
300        Interpolator interpolator;
301        long duration;
302    }
303
304}
305