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