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 
17 package com.android.systemui.statusbar;
18 
19 import android.animation.Animator;
20 import android.animation.ValueAnimator;
21 import android.content.Context;
22 import android.view.ViewPropertyAnimator;
23 import android.view.animation.AnimationUtils;
24 import android.view.animation.Interpolator;
25 import android.view.animation.PathInterpolator;
26 
27 /**
28  * Utility class to calculate general fling animation when the finger is released.
29  */
30 public 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 
FlingAnimationUtils(Context ctx, float maxLengthSeconds)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      */
apply(Animator animator, float currValue, float endValue, float velocity)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      */
apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity)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      */
apply(Animator animator, float currValue, float endValue, float velocity, float maxDistance)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      */
apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance)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 
getProperties(float currValue, float endValue, float velocity, float maxDistance)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      */
applyDismissing(Animator animator, float currValue, float endValue, float velocity, float maxDistance)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      */
applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance)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 
getDismissingProperties(float currValue, float endValue, float velocity, float maxDistance)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      */
calculateLinearOutFasterInY2(float velocity)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      */
getMinVelocityPxPerSecond()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 
InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2, Interpolator crossfader)263         InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2,
264                 Interpolator crossfader) {
265             mInterpolator1 = interpolator1;
266             mInterpolator2 = interpolator2;
267             mCrossfader = crossfader;
268         }
269 
270         @Override
getInterpolation(float input)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 
VelocityInterpolator(float durationSeconds, float velocity, float diff)287         private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
288             mDurationSeconds = durationSeconds;
289             mVelocity = velocity;
290             mDiff = diff;
291         }
292 
293         @Override
getInterpolation(float input)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