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