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 /** Fancy 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 animatorProperties = 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 animatorProperties.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 animatorProperties.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 animatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN; 144 } 145 animatorProperties.duration = (long) (durationSeconds * 1000); 146 return animatorProperties; 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 animatorProperties.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 animatorProperties.interpolator = superInterpolator; 217 } else { 218 219 // Just use a normal interpolator which doesn't take the velocity into account. 220 durationSeconds = maxLengthSeconds; 221 animatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN; 222 } 223 animatorProperties.duration = (long) (durationSeconds * 1000); 224 return animatorProperties; 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 interpolator1; 250 private Interpolator interpolator2; 251 private Interpolator crossfader; 252 InterpolatorInterpolator( Interpolator interpolator1, Interpolator interpolator2, Interpolator crossfader)253 InterpolatorInterpolator( 254 Interpolator interpolator1, Interpolator interpolator2, Interpolator crossfader) { 255 this.interpolator1 = interpolator1; 256 this.interpolator2 = interpolator2; 257 this.crossfader = crossfader; 258 } 259 260 @Override getInterpolation(float input)261 public float getInterpolation(float input) { 262 float t = crossfader.getInterpolation(input); 263 return (1 - t) * interpolator1.getInterpolation(input) 264 + t * interpolator2.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 durationSeconds; 272 private float velocity; 273 private float diff; 274 VelocityInterpolator(float durationSeconds, float velocity, float diff)275 private VelocityInterpolator(float durationSeconds, float velocity, float diff) { 276 this.durationSeconds = durationSeconds; 277 this.velocity = velocity; 278 this.diff = diff; 279 } 280 281 @Override getInterpolation(float input)282 public float getInterpolation(float input) { 283 float time = input * durationSeconds; 284 return time * velocity / diff; 285 } 286 } 287 288 private static class AnimatorProperties { 289 290 Interpolator interpolator; 291 long duration; 292 } 293 } 294