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