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