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