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.content.Context;
21 import android.util.Log;
22 import android.view.ViewPropertyAnimator;
23 import android.view.animation.Interpolator;
24 import android.view.animation.PathInterpolator;
25 
26 import com.android.systemui.Interpolators;
27 import com.android.systemui.statusbar.notification.NotificationUtils;
28 import com.android.systemui.statusbar.phone.StatusBar;
29 
30 /**
31  * Utility class to calculate general fling animation when the finger is released.
32  */
33 public class FlingAnimationUtils {
34 
35     private static final String TAG = "FlingAnimationUtils";
36 
37     private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f;
38     private static final float LINEAR_OUT_SLOW_IN_X2_MAX = 0.68f;
39     private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f;
40     private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f;
41     private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f;
42     private static final float MIN_VELOCITY_DP_PER_SECOND = 250;
43     private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000;
44 
45     private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 0.75f;
46     private final float mSpeedUpFactor;
47     private final float mY2;
48 
49     private float mMinVelocityPxPerSecond;
50     private float mMaxLengthSeconds;
51     private float mHighVelocityPxPerSecond;
52     private float mLinearOutSlowInX2;
53 
54     private AnimatorProperties mAnimatorProperties = new AnimatorProperties();
55     private PathInterpolator mInterpolator;
56     private float mCachedStartGradient = -1;
57     private float mCachedVelocityFactor = -1;
58 
FlingAnimationUtils(Context ctx, float maxLengthSeconds)59     public FlingAnimationUtils(Context ctx, float maxLengthSeconds) {
60         this(ctx, maxLengthSeconds, 0.0f);
61     }
62 
63     /**
64      * @param maxLengthSeconds the longest duration an animation can become in seconds
65      * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
66      *                      the end of the animation. 0 means it's at the beginning and no
67      *                      acceleration will take place.
68      */
FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor)69     public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor) {
70         this(ctx, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f);
71     }
72 
73     /**
74      * @param maxLengthSeconds the longest duration an animation can become in seconds
75      * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
76      *                      the end of the animation. 0 means it's at the beginning and no
77      *                      acceleration will take place.
78      * @param x2 the x value to take for the second point of the bezier spline. If a value below 0
79      *           is provided, the value is automatically calculated.
80      * @param y2 the y value to take for the second point of the bezier spline
81      */
FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor, float x2, float y2)82     public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor, float x2,
83             float y2) {
84         mMaxLengthSeconds = maxLengthSeconds;
85         mSpeedUpFactor = speedUpFactor;
86         if (x2 < 0) {
87             mLinearOutSlowInX2 = NotificationUtils.interpolate(LINEAR_OUT_SLOW_IN_X2,
88                     LINEAR_OUT_SLOW_IN_X2_MAX,
89                     mSpeedUpFactor);
90         } else {
91             mLinearOutSlowInX2 = x2;
92         }
93         mY2 = y2;
94 
95         mMinVelocityPxPerSecond
96                 = MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
97         mHighVelocityPxPerSecond
98                 = HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
99     }
100 
101     /**
102      * Applies the interpolator and length to the animator, such that the fling animation is
103      * consistent with the finger motion.
104      *
105      * @param animator the animator to apply
106      * @param currValue the current value
107      * @param endValue the end value of the animator
108      * @param velocity the current velocity of the motion
109      */
apply(Animator animator, float currValue, float endValue, float velocity)110     public void apply(Animator animator, float currValue, float endValue, float velocity) {
111         apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
112     }
113 
114     /**
115      * Applies the interpolator and length to the animator, such that the fling animation is
116      * consistent with the finger motion.
117      *
118      * @param animator the animator to apply
119      * @param currValue the current value
120      * @param endValue the end value of the animator
121      * @param velocity the current velocity of the motion
122      */
apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity)123     public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
124             float velocity) {
125         apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
126     }
127 
128     /**
129      * Applies the interpolator and length to the animator, such that the fling animation is
130      * consistent with the finger motion.
131      *
132      * @param animator the animator to apply
133      * @param currValue the current value
134      * @param endValue the end value of the animator
135      * @param velocity the current velocity of the motion
136      * @param maxDistance the maximum distance for this interaction; the maximum animation length
137      *                    gets multiplied by the ratio between the actual distance and this value
138      */
apply(Animator animator, float currValue, float endValue, float velocity, float maxDistance)139     public void apply(Animator animator, float currValue, float endValue, float velocity,
140             float maxDistance) {
141         AnimatorProperties properties = getProperties(currValue, endValue, velocity,
142                 maxDistance);
143         animator.setDuration(properties.duration);
144         animator.setInterpolator(properties.interpolator);
145     }
146 
147     /**
148      * Applies the interpolator and length to the animator, such that the fling animation is
149      * consistent with the finger motion.
150      *
151      * @param animator the animator to apply
152      * @param currValue the current value
153      * @param endValue the end value of the animator
154      * @param velocity the current velocity of the motion
155      * @param maxDistance the maximum distance for this interaction; the maximum animation length
156      *                    gets multiplied by the ratio between the actual distance and this value
157      */
apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance)158     public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
159             float velocity, float maxDistance) {
160         AnimatorProperties properties = getProperties(currValue, endValue, velocity,
161                 maxDistance);
162         animator.setDuration(properties.duration);
163         animator.setInterpolator(properties.interpolator);
164     }
165 
getProperties(float currValue, float endValue, float velocity, float maxDistance)166     private AnimatorProperties getProperties(float currValue,
167             float endValue, float velocity, float maxDistance) {
168         float maxLengthSeconds = (float) (mMaxLengthSeconds
169                 * Math.sqrt(Math.abs(endValue - currValue) / maxDistance));
170         float diff = Math.abs(endValue - currValue);
171         float velAbs = Math.abs(velocity);
172         float velocityFactor = mSpeedUpFactor == 0.0f
173                 ? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f);
174         float startGradient = NotificationUtils.interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT,
175                 mY2 / mLinearOutSlowInX2, velocityFactor);
176         float durationSeconds = startGradient * diff / velAbs;
177         Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor);
178         if (durationSeconds <= maxLengthSeconds) {
179             mAnimatorProperties.interpolator = slowInInterpolator;
180         } else if (velAbs >= mMinVelocityPxPerSecond) {
181 
182             // Cross fade between fast-out-slow-in and linear interpolator with current velocity.
183             durationSeconds = maxLengthSeconds;
184             VelocityInterpolator velocityInterpolator
185                     = new VelocityInterpolator(durationSeconds, velAbs, diff);
186             InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
187                     velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN);
188             mAnimatorProperties.interpolator = superInterpolator;
189         } else {
190 
191             // Just use a normal interpolator which doesn't take the velocity into account.
192             durationSeconds = maxLengthSeconds;
193             mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN;
194         }
195         mAnimatorProperties.duration = (long) (durationSeconds * 1000);
196         return mAnimatorProperties;
197     }
198 
getInterpolator(float startGradient, float velocityFactor)199     private Interpolator getInterpolator(float startGradient, float velocityFactor) {
200         if (Float.isNaN(velocityFactor)) {
201             Log.e(TAG, "Invalid velocity factor", new Throwable());
202             return Interpolators.LINEAR_OUT_SLOW_IN;
203         }
204         if (startGradient != mCachedStartGradient
205                 || velocityFactor != mCachedVelocityFactor) {
206             float speedup = mSpeedUpFactor * (1.0f - velocityFactor);
207             float x1 = speedup;
208             float y1 = speedup * startGradient;
209             float x2 = mLinearOutSlowInX2;
210             float y2 = mY2;
211             try {
212                 mInterpolator = new PathInterpolator(x1, y1, x2, y2);
213             } catch (IllegalArgumentException e) {
214                 throw new IllegalArgumentException("Illegal path with "
215                         + "x1=" + x1 + " y1=" + y1 + " x2=" + x2 + " y2=" + y2, e);
216             }
217             mCachedStartGradient = startGradient;
218             mCachedVelocityFactor = velocityFactor;
219         }
220         return mInterpolator;
221     }
222 
223     /**
224      * Applies the interpolator and length to the animator, such that the fling animation is
225      * consistent with the finger motion for the case when the animation is making something
226      * disappear.
227      *
228      * @param animator the animator to apply
229      * @param currValue the current value
230      * @param endValue the end value of the animator
231      * @param velocity the current velocity of the motion
232      * @param maxDistance the maximum distance for this interaction; the maximum animation length
233      *                    gets multiplied by the ratio between the actual distance and this value
234      */
applyDismissing(Animator animator, float currValue, float endValue, float velocity, float maxDistance)235     public void applyDismissing(Animator animator, float currValue, float endValue,
236             float velocity, float maxDistance) {
237         AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
238                 maxDistance);
239         animator.setDuration(properties.duration);
240         animator.setInterpolator(properties.interpolator);
241     }
242 
243     /**
244      * Applies the interpolator and length to the animator, such that the fling animation is
245      * consistent with the finger motion for the case when the animation is making something
246      * disappear.
247      *
248      * @param animator the animator to apply
249      * @param currValue the current value
250      * @param endValue the end value of the animator
251      * @param velocity the current velocity of the motion
252      * @param maxDistance the maximum distance for this interaction; the maximum animation length
253      *                    gets multiplied by the ratio between the actual distance and this value
254      */
applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance)255     public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue,
256             float velocity, float maxDistance) {
257         AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
258                 maxDistance);
259         animator.setDuration(properties.duration);
260         animator.setInterpolator(properties.interpolator);
261     }
262 
getDismissingProperties(float currValue, float endValue, float velocity, float maxDistance)263     private AnimatorProperties getDismissingProperties(float currValue, float endValue,
264             float velocity, float maxDistance) {
265         float maxLengthSeconds = (float) (mMaxLengthSeconds
266                 * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f));
267         float diff = Math.abs(endValue - currValue);
268         float velAbs = Math.abs(velocity);
269         float y2 = calculateLinearOutFasterInY2(velAbs);
270 
271         float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2;
272         Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2);
273         float durationSeconds = startGradient * diff / velAbs;
274         if (durationSeconds <= maxLengthSeconds) {
275             mAnimatorProperties.interpolator = mLinearOutFasterIn;
276         } else if (velAbs >= mMinVelocityPxPerSecond) {
277 
278             // Cross fade between linear-out-faster-in and linear interpolator with current
279             // velocity.
280             durationSeconds = maxLengthSeconds;
281             VelocityInterpolator velocityInterpolator
282                     = new VelocityInterpolator(durationSeconds, velAbs, diff);
283             InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
284                     velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN);
285             mAnimatorProperties.interpolator = superInterpolator;
286         } else {
287 
288             // Just use a normal interpolator which doesn't take the velocity into account.
289             durationSeconds = maxLengthSeconds;
290             mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN;
291         }
292         mAnimatorProperties.duration = (long) (durationSeconds * 1000);
293         return mAnimatorProperties;
294     }
295 
296     /**
297      * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
298      * velocity. The faster the velocity, the more "linear" the interpolator gets.
299      *
300      * @param velocity the velocity of the gesture.
301      * @return the y2 control point for a cubic bezier path interpolator
302      */
calculateLinearOutFasterInY2(float velocity)303     private float calculateLinearOutFasterInY2(float velocity) {
304         float t = (velocity - mMinVelocityPxPerSecond)
305                 / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond);
306         t = Math.max(0, Math.min(1, t));
307         return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
308     }
309 
310     /**
311      * @return the minimum velocity a gesture needs to have to be considered a fling
312      */
getMinVelocityPxPerSecond()313     public float getMinVelocityPxPerSecond() {
314         return mMinVelocityPxPerSecond;
315     }
316 
317     /**
318      * An interpolator which interpolates two interpolators with an interpolator.
319      */
320     private static final class InterpolatorInterpolator implements Interpolator {
321 
322         private Interpolator mInterpolator1;
323         private Interpolator mInterpolator2;
324         private Interpolator mCrossfader;
325 
InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2, Interpolator crossfader)326         InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2,
327                 Interpolator crossfader) {
328             mInterpolator1 = interpolator1;
329             mInterpolator2 = interpolator2;
330             mCrossfader = crossfader;
331         }
332 
333         @Override
getInterpolation(float input)334         public float getInterpolation(float input) {
335             float t = mCrossfader.getInterpolation(input);
336             return (1 - t) * mInterpolator1.getInterpolation(input)
337                     + t * mInterpolator2.getInterpolation(input);
338         }
339     }
340 
341     /**
342      * An interpolator which interpolates with a fixed velocity.
343      */
344     private static final class VelocityInterpolator implements Interpolator {
345 
346         private float mDurationSeconds;
347         private float mVelocity;
348         private float mDiff;
349 
VelocityInterpolator(float durationSeconds, float velocity, float diff)350         private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
351             mDurationSeconds = durationSeconds;
352             mVelocity = velocity;
353             mDiff = diff;
354         }
355 
356         @Override
getInterpolation(float input)357         public float getInterpolation(float input) {
358             float time = input * mDurationSeconds;
359             return time * mVelocity / mDiff;
360         }
361     }
362 
363     private static class AnimatorProperties {
364         Interpolator interpolator;
365         long duration;
366     }
367 
368 }
369