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