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