• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.wm.shell.animation;
18 
19 import android.animation.Animator;
20 import android.util.DisplayMetrics;
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.wm.shell.shared.animation.Interpolators;
27 
28 import javax.inject.Inject;
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(DisplayMetrics displayMetrics, float maxLengthSeconds)59     public FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds) {
60         this(displayMetrics, 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(DisplayMetrics displayMetrics, float maxLengthSeconds, float speedUpFactor)69     public FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds,
70             float speedUpFactor) {
71         this(displayMetrics, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f);
72     }
73 
74     /**
75      * @param maxLengthSeconds the longest duration an animation can become in seconds
76      * @param speedUpFactor    a factor from 0 to 1 how much the slow down should be shifted towards
77      *                         the end of the animation. 0 means it's at the beginning and no
78      *                         acceleration will take place.
79      * @param x2               the x value to take for the second point of the bezier spline. If a
80      *                         value below 0 is provided, the value is automatically calculated.
81      * @param y2               the y value to take for the second point of the bezier spline
82      */
FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds, float speedUpFactor, float x2, float y2)83     public FlingAnimationUtils(DisplayMetrics displayMetrics, float maxLengthSeconds,
84             float speedUpFactor, float x2, float y2) {
85         mMaxLengthSeconds = maxLengthSeconds;
86         mSpeedUpFactor = speedUpFactor;
87         if (x2 < 0) {
88             mLinearOutSlowInX2 = interpolate(LINEAR_OUT_SLOW_IN_X2,
89                     LINEAR_OUT_SLOW_IN_X2_MAX,
90                     mSpeedUpFactor);
91         } else {
92             mLinearOutSlowInX2 = x2;
93         }
94         mY2 = y2;
95 
96         mMinVelocityPxPerSecond = MIN_VELOCITY_DP_PER_SECOND * displayMetrics.density;
97         mHighVelocityPxPerSecond = HIGH_VELOCITY_DP_PER_SECOND * displayMetrics.density;
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(androidx.core.animation.Animator animator, float currValue, float endValue, float velocity)122     public void apply(androidx.core.animation.Animator animator,
123             float currValue, float endValue, 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      */
apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity)136     public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
137             float velocity) {
138         apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
139     }
140 
141     /**
142      * Applies the interpolator and length to the animator, such that the fling animation is
143      * consistent with the finger motion.
144      *
145      * @param animator    the animator to apply
146      * @param currValue   the current value
147      * @param endValue    the end value of the animator
148      * @param velocity    the current velocity of the motion
149      * @param maxDistance the maximum distance for this interaction; the maximum animation length
150      *                    gets multiplied by the ratio between the actual distance and this value
151      */
apply(Animator animator, float currValue, float endValue, float velocity, float maxDistance)152     public void apply(Animator animator, float currValue, float endValue, float velocity,
153             float maxDistance) {
154         AnimatorProperties properties = getProperties(currValue, endValue, velocity,
155                 maxDistance);
156         animator.setDuration(properties.mDuration);
157         animator.setInterpolator(properties.mInterpolator);
158     }
159 
160     /**
161      * Applies the interpolator and length to the animator, such that the fling animation is
162      * consistent with the finger motion.
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      */
apply(androidx.core.animation.Animator animator, float currValue, float endValue, float velocity, float maxDistance)171     public void apply(androidx.core.animation.Animator animator,
172             float currValue, float endValue, float velocity, float maxDistance) {
173         AnimatorProperties properties = getProperties(currValue, endValue, velocity, maxDistance);
174         animator.setDuration(properties.mDuration);
175         animator.setInterpolator(properties.getInterpolator());
176     }
177 
178     /**
179      * Applies the interpolator and length to the animator, such that the fling animation is
180      * consistent with the finger motion.
181      *
182      * @param animator    the animator to apply
183      * @param currValue   the current value
184      * @param endValue    the end value of the animator
185      * @param velocity    the current velocity of the motion
186      * @param maxDistance the maximum distance for this interaction; the maximum animation length
187      *                    gets multiplied by the ratio between the actual distance and this value
188      */
apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance)189     public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
190             float velocity, float maxDistance) {
191         AnimatorProperties properties = getProperties(currValue, endValue, velocity,
192                 maxDistance);
193         animator.setDuration(properties.mDuration);
194         animator.setInterpolator(properties.mInterpolator);
195     }
196 
getProperties(float currValue, float endValue, float velocity, float maxDistance)197     private AnimatorProperties getProperties(float currValue,
198             float endValue, float velocity, float maxDistance) {
199         float maxLengthSeconds = (float) (mMaxLengthSeconds
200                 * Math.sqrt(Math.abs(endValue - currValue) / maxDistance));
201         float diff = Math.abs(endValue - currValue);
202         float velAbs = Math.abs(velocity);
203         float velocityFactor = mSpeedUpFactor == 0.0f
204                 ? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f);
205         float startGradient = interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT,
206                 mY2 / mLinearOutSlowInX2, velocityFactor);
207         float durationSeconds = startGradient * diff / velAbs;
208         Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor);
209         if (durationSeconds <= maxLengthSeconds) {
210             mAnimatorProperties.mInterpolator = slowInInterpolator;
211         } else if (velAbs >= mMinVelocityPxPerSecond) {
212 
213             // Cross fade between fast-out-slow-in and linear interpolator with current velocity.
214             durationSeconds = maxLengthSeconds;
215             VelocityInterpolator velocityInterpolator = new VelocityInterpolator(
216                     durationSeconds, velAbs, diff);
217             InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
218                     velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN);
219             mAnimatorProperties.mInterpolator = superInterpolator;
220         } else {
221 
222             // Just use a normal interpolator which doesn't take the velocity into account.
223             durationSeconds = maxLengthSeconds;
224             mAnimatorProperties.mInterpolator = Interpolators.FAST_OUT_SLOW_IN;
225         }
226         mAnimatorProperties.mDuration = (long) (durationSeconds * 1000);
227         return mAnimatorProperties;
228     }
229 
getInterpolator(float startGradient, float velocityFactor)230     private Interpolator getInterpolator(float startGradient, float velocityFactor) {
231         if (Float.isNaN(velocityFactor)) {
232             Log.e(TAG, "Invalid velocity factor", new Throwable());
233             return Interpolators.LINEAR_OUT_SLOW_IN;
234         }
235         if (startGradient != mCachedStartGradient
236                 || velocityFactor != mCachedVelocityFactor) {
237             float speedup = mSpeedUpFactor * (1.0f - velocityFactor);
238             float x1 = speedup;
239             float y1 = speedup * startGradient;
240             float x2 = mLinearOutSlowInX2;
241             float y2 = mY2;
242             try {
243                 mInterpolator = new PathInterpolator(x1, y1, x2, y2);
244             } catch (IllegalArgumentException e) {
245                 throw new IllegalArgumentException("Illegal path with "
246                         + "x1=" + x1 + " y1=" + y1 + " x2=" + x2 + " y2=" + y2, e);
247             }
248             mCachedStartGradient = startGradient;
249             mCachedVelocityFactor = velocityFactor;
250         }
251         return mInterpolator;
252     }
253 
254     /**
255      * Applies the interpolator and length to the animator, such that the fling animation is
256      * consistent with the finger motion for the case when the animation is making something
257      * disappear.
258      *
259      * @param animator    the animator to apply
260      * @param currValue   the current value
261      * @param endValue    the end value of the animator
262      * @param velocity    the current velocity of the motion
263      * @param maxDistance the maximum distance for this interaction; the maximum animation length
264      *                    gets multiplied by the ratio between the actual distance and this value
265      */
applyDismissing(Animator animator, float currValue, float endValue, float velocity, float maxDistance)266     public void applyDismissing(Animator animator, float currValue, float endValue,
267             float velocity, float maxDistance) {
268         AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
269                 maxDistance);
270         animator.setDuration(properties.mDuration);
271         animator.setInterpolator(properties.mInterpolator);
272     }
273 
274     /**
275      * Applies the interpolator and length to the animator, such that the fling animation is
276      * consistent with the finger motion for the case when the animation is making something
277      * disappear.
278      *
279      * @param animator    the animator to apply
280      * @param currValue   the current value
281      * @param endValue    the end value of the animator
282      * @param velocity    the current velocity of the motion
283      * @param maxDistance the maximum distance for this interaction; the maximum animation length
284      *                    gets multiplied by the ratio between the actual distance and this value
285      */
applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance)286     public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue,
287             float velocity, float maxDistance) {
288         AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
289                 maxDistance);
290         animator.setDuration(properties.mDuration);
291         animator.setInterpolator(properties.mInterpolator);
292     }
293 
getDismissingProperties(float currValue, float endValue, float velocity, float maxDistance)294     private AnimatorProperties getDismissingProperties(float currValue, float endValue,
295             float velocity, float maxDistance) {
296         float maxLengthSeconds = (float) (mMaxLengthSeconds
297                 * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f));
298         float diff = Math.abs(endValue - currValue);
299         float velAbs = Math.abs(velocity);
300         float y2 = calculateLinearOutFasterInY2(velAbs);
301 
302         float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2;
303         Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2);
304         float durationSeconds = startGradient * diff / velAbs;
305         if (durationSeconds <= maxLengthSeconds) {
306             mAnimatorProperties.mInterpolator = mLinearOutFasterIn;
307         } else if (velAbs >= mMinVelocityPxPerSecond) {
308 
309             // Cross fade between linear-out-faster-in and linear interpolator with current
310             // velocity.
311             durationSeconds = maxLengthSeconds;
312             VelocityInterpolator velocityInterpolator = new VelocityInterpolator(
313                     durationSeconds, velAbs, diff);
314             InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
315                     velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN);
316             mAnimatorProperties.mInterpolator = superInterpolator;
317         } else {
318 
319             // Just use a normal interpolator which doesn't take the velocity into account.
320             durationSeconds = maxLengthSeconds;
321             mAnimatorProperties.mInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
322         }
323         mAnimatorProperties.mDuration = (long) (durationSeconds * 1000);
324         return mAnimatorProperties;
325     }
326 
327     /**
328      * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
329      * velocity. The faster the velocity, the more "linear" the interpolator gets.
330      *
331      * @param velocity the velocity of the gesture.
332      * @return the y2 control point for a cubic bezier path interpolator
333      */
calculateLinearOutFasterInY2(float velocity)334     private float calculateLinearOutFasterInY2(float velocity) {
335         float t = (velocity - mMinVelocityPxPerSecond)
336                 / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond);
337         t = Math.max(0, Math.min(1, t));
338         return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
339     }
340 
341     /**
342      * @return the minimum velocity a gesture needs to have to be considered a fling
343      */
getMinVelocityPxPerSecond()344     public float getMinVelocityPxPerSecond() {
345         return mMinVelocityPxPerSecond;
346     }
347 
348     /**
349      * @return a velocity considered fast
350      */
getHighVelocityPxPerSecond()351     public float getHighVelocityPxPerSecond() {
352         return mHighVelocityPxPerSecond;
353     }
354 
355     /**
356      * An interpolator which interpolates two interpolators with an interpolator.
357      */
358     private static final class InterpolatorInterpolator implements Interpolator {
359 
360         private Interpolator mInterpolator1;
361         private Interpolator mInterpolator2;
362         private Interpolator mCrossfader;
363 
InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2, Interpolator crossfader)364         InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2,
365                 Interpolator crossfader) {
366             mInterpolator1 = interpolator1;
367             mInterpolator2 = interpolator2;
368             mCrossfader = crossfader;
369         }
370 
371         @Override
getInterpolation(float input)372         public float getInterpolation(float input) {
373             float t = mCrossfader.getInterpolation(input);
374             return (1 - t) * mInterpolator1.getInterpolation(input)
375                     + t * mInterpolator2.getInterpolation(input);
376         }
377     }
378 
379     /**
380      * An interpolator which interpolates with a fixed velocity.
381      */
382     private static final class VelocityInterpolator implements Interpolator {
383 
384         private float mDurationSeconds;
385         private float mVelocity;
386         private float mDiff;
387 
VelocityInterpolator(float durationSeconds, float velocity, float diff)388         private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
389             mDurationSeconds = durationSeconds;
390             mVelocity = velocity;
391             mDiff = diff;
392         }
393 
394         @Override
getInterpolation(float input)395         public float getInterpolation(float input) {
396             float time = input * mDurationSeconds;
397             return time * mVelocity / mDiff;
398         }
399     }
400 
401     private static class AnimatorProperties {
402         Interpolator mInterpolator;
403         long mDuration;
404 
405         /** Get an AndroidX interpolator wrapper of the current mInterpolator */
getInterpolator()406         public androidx.core.animation.Interpolator getInterpolator() {
407             return mInterpolator::getInterpolation;
408         }
409     }
410 
411     /** Builder for {@link #FlingAnimationUtils}. */
412     public static class Builder {
413         private final DisplayMetrics mDisplayMetrics;
414         float mMaxLengthSeconds;
415         float mSpeedUpFactor;
416         float mX2;
417         float mY2;
418 
419         @Inject
Builder(DisplayMetrics displayMetrics)420         public Builder(DisplayMetrics displayMetrics) {
421             mDisplayMetrics = displayMetrics;
422             reset();
423         }
424 
425         /** Sets the longest duration an animation can become in seconds. */
setMaxLengthSeconds(float maxLengthSeconds)426         public Builder setMaxLengthSeconds(float maxLengthSeconds) {
427             mMaxLengthSeconds = maxLengthSeconds;
428             return this;
429         }
430 
431         /**
432          * Sets the factor for how much the slow down should be shifted towards the end of the
433          * animation.
434          */
setSpeedUpFactor(float speedUpFactor)435         public Builder setSpeedUpFactor(float speedUpFactor) {
436             mSpeedUpFactor = speedUpFactor;
437             return this;
438         }
439 
440         /** Sets the x value to take for the second point of the bezier spline. */
setX2(float x2)441         public Builder setX2(float x2) {
442             mX2 = x2;
443             return this;
444         }
445 
446         /** Sets the y value to take for the second point of the bezier spline. */
setY2(float y2)447         public Builder setY2(float y2) {
448             mY2 = y2;
449             return this;
450         }
451 
452         /** Resets all parameters of the builder. */
reset()453         public Builder reset() {
454             mMaxLengthSeconds = 0;
455             mSpeedUpFactor = 0.0f;
456             mX2 = -1.0f;
457             mY2 = 1.0f;
458 
459             return this;
460         }
461 
462         /** Builds {@link #FlingAnimationUtils}. */
build()463         public FlingAnimationUtils build() {
464             return new FlingAnimationUtils(mDisplayMetrics, mMaxLengthSeconds, mSpeedUpFactor,
465                     mX2, mY2);
466         }
467     }
468 
interpolate(float start, float end, float amount)469     private static float interpolate(float start, float end, float amount) {
470         return start * (1.0f - amount) + end * amount;
471     }
472 }
473