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