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.systemui.statusbar; 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 import com.android.systemui.Interpolators; 26 import com.android.systemui.statusbar.notification.NotificationUtils; 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 float LINEAR_OUT_SLOW_IN_X2 = 0.35f; 34 private static final float LINEAR_OUT_SLOW_IN_X2_MAX = 0.68f; 35 private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f; 36 private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f; 37 private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f; 38 private static final float MIN_VELOCITY_DP_PER_SECOND = 250; 39 private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000; 40 41 private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 0.75f; 42 private final float mSpeedUpFactor; 43 private final float mY2; 44 45 private float mMinVelocityPxPerSecond; 46 private float mMaxLengthSeconds; 47 private float mHighVelocityPxPerSecond; 48 private float mLinearOutSlowInX2; 49 50 private AnimatorProperties mAnimatorProperties = new AnimatorProperties(); 51 private PathInterpolator mInterpolator; 52 private float mCachedStartGradient = -1; 53 private float mCachedVelocityFactor = -1; 54 FlingAnimationUtils(Context ctx, float maxLengthSeconds)55 public FlingAnimationUtils(Context ctx, float maxLengthSeconds) { 56 this(ctx, maxLengthSeconds, 0.0f); 57 } 58 59 /** 60 * @param maxLengthSeconds the longest duration an animation can become in seconds 61 * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards 62 * the end of the animation. 0 means it's at the beginning and no 63 * acceleration will take place. 64 */ FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor)65 public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor) { 66 this(ctx, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f); 67 } 68 69 /** 70 * @param maxLengthSeconds the longest duration an animation can become in seconds 71 * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards 72 * the end of the animation. 0 means it's at the beginning and no 73 * acceleration will take place. 74 * @param x2 the x value to take for the second point of the bezier spline. If a value below 0 75 * is provided, the value is automatically calculated. 76 * @param y2 the y value to take for the second point of the bezier spline 77 */ FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor, float x2, float y2)78 public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor, float x2, 79 float y2) { 80 mMaxLengthSeconds = maxLengthSeconds; 81 mSpeedUpFactor = speedUpFactor; 82 if (x2 < 0) { 83 mLinearOutSlowInX2 = NotificationUtils.interpolate(LINEAR_OUT_SLOW_IN_X2, 84 LINEAR_OUT_SLOW_IN_X2_MAX, 85 mSpeedUpFactor); 86 } else { 87 mLinearOutSlowInX2 = x2; 88 } 89 mY2 = y2; 90 91 mMinVelocityPxPerSecond 92 = MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density; 93 mHighVelocityPxPerSecond 94 = HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density; 95 } 96 97 /** 98 * Applies the interpolator and length to the animator, such that the fling animation is 99 * consistent with the finger motion. 100 * 101 * @param animator the animator to apply 102 * @param currValue the current value 103 * @param endValue the end value of the animator 104 * @param velocity the current velocity of the motion 105 */ apply(Animator animator, float currValue, float endValue, float velocity)106 public void apply(Animator animator, float currValue, float endValue, float velocity) { 107 apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue)); 108 } 109 110 /** 111 * Applies the interpolator and length to the animator, such that the fling animation is 112 * consistent with the finger motion. 113 * 114 * @param animator the animator to apply 115 * @param currValue the current value 116 * @param endValue the end value of the animator 117 * @param velocity the current velocity of the motion 118 */ apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity)119 public void apply(ViewPropertyAnimator animator, float currValue, float endValue, 120 float velocity) { 121 apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue)); 122 } 123 124 /** 125 * Applies the interpolator and length to the animator, such that the fling animation is 126 * consistent with the finger motion. 127 * 128 * @param animator the animator to apply 129 * @param currValue the current value 130 * @param endValue the end value of the animator 131 * @param velocity the current velocity of the motion 132 * @param maxDistance the maximum distance for this interaction; the maximum animation length 133 * gets multiplied by the ratio between the actual distance and this value 134 */ apply(Animator animator, float currValue, float endValue, float velocity, float maxDistance)135 public void apply(Animator animator, float currValue, float endValue, float velocity, 136 float maxDistance) { 137 AnimatorProperties properties = getProperties(currValue, endValue, velocity, 138 maxDistance); 139 animator.setDuration(properties.duration); 140 animator.setInterpolator(properties.interpolator); 141 } 142 143 /** 144 * Applies the interpolator and length to the animator, such that the fling animation is 145 * consistent with the finger motion. 146 * 147 * @param animator the animator to apply 148 * @param currValue the current value 149 * @param endValue the end value of the animator 150 * @param velocity the current velocity of the motion 151 * @param maxDistance the maximum distance for this interaction; the maximum animation length 152 * gets multiplied by the ratio between the actual distance and this value 153 */ apply(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance)154 public void apply(ViewPropertyAnimator animator, float currValue, float endValue, 155 float velocity, float maxDistance) { 156 AnimatorProperties properties = getProperties(currValue, endValue, velocity, 157 maxDistance); 158 animator.setDuration(properties.duration); 159 animator.setInterpolator(properties.interpolator); 160 } 161 getProperties(float currValue, float endValue, float velocity, float maxDistance)162 private AnimatorProperties getProperties(float currValue, 163 float endValue, float velocity, float maxDistance) { 164 float maxLengthSeconds = (float) (mMaxLengthSeconds 165 * Math.sqrt(Math.abs(endValue - currValue) / maxDistance)); 166 float diff = Math.abs(endValue - currValue); 167 float velAbs = Math.abs(velocity); 168 float velocityFactor = mSpeedUpFactor == 0.0f 169 ? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f); 170 float startGradient = NotificationUtils.interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT, 171 mY2 / mLinearOutSlowInX2, velocityFactor); 172 float durationSeconds = startGradient * diff / velAbs; 173 Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor); 174 if (durationSeconds <= maxLengthSeconds) { 175 mAnimatorProperties.interpolator = slowInInterpolator; 176 } else if (velAbs >= mMinVelocityPxPerSecond) { 177 178 // Cross fade between fast-out-slow-in and linear interpolator with current velocity. 179 durationSeconds = maxLengthSeconds; 180 VelocityInterpolator velocityInterpolator 181 = new VelocityInterpolator(durationSeconds, velAbs, diff); 182 InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( 183 velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN); 184 mAnimatorProperties.interpolator = superInterpolator; 185 } else { 186 187 // Just use a normal interpolator which doesn't take the velocity into account. 188 durationSeconds = maxLengthSeconds; 189 mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN; 190 } 191 mAnimatorProperties.duration = (long) (durationSeconds * 1000); 192 return mAnimatorProperties; 193 } 194 getInterpolator(float startGradient, float velocityFactor)195 private Interpolator getInterpolator(float startGradient, float velocityFactor) { 196 if (startGradient != mCachedStartGradient 197 || velocityFactor != mCachedVelocityFactor) { 198 float speedup = mSpeedUpFactor * (1.0f - velocityFactor); 199 mInterpolator = new PathInterpolator(speedup, 200 speedup * startGradient, 201 mLinearOutSlowInX2, mY2); 202 mCachedStartGradient = startGradient; 203 mCachedVelocityFactor = velocityFactor; 204 } 205 return mInterpolator; 206 } 207 208 /** 209 * Applies the interpolator and length to the animator, such that the fling animation is 210 * consistent with the finger motion for the case when the animation is making something 211 * disappear. 212 * 213 * @param animator the animator to apply 214 * @param currValue the current value 215 * @param endValue the end value of the animator 216 * @param velocity the current velocity of the motion 217 * @param maxDistance the maximum distance for this interaction; the maximum animation length 218 * gets multiplied by the ratio between the actual distance and this value 219 */ applyDismissing(Animator animator, float currValue, float endValue, float velocity, float maxDistance)220 public void applyDismissing(Animator animator, float currValue, float endValue, 221 float velocity, float maxDistance) { 222 AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, 223 maxDistance); 224 animator.setDuration(properties.duration); 225 animator.setInterpolator(properties.interpolator); 226 } 227 228 /** 229 * Applies the interpolator and length to the animator, such that the fling animation is 230 * consistent with the finger motion for the case when the animation is making something 231 * disappear. 232 * 233 * @param animator the animator to apply 234 * @param currValue the current value 235 * @param endValue the end value of the animator 236 * @param velocity the current velocity of the motion 237 * @param maxDistance the maximum distance for this interaction; the maximum animation length 238 * gets multiplied by the ratio between the actual distance and this value 239 */ applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue, float velocity, float maxDistance)240 public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue, 241 float velocity, float maxDistance) { 242 AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, 243 maxDistance); 244 animator.setDuration(properties.duration); 245 animator.setInterpolator(properties.interpolator); 246 } 247 getDismissingProperties(float currValue, float endValue, float velocity, float maxDistance)248 private AnimatorProperties getDismissingProperties(float currValue, float endValue, 249 float velocity, float maxDistance) { 250 float maxLengthSeconds = (float) (mMaxLengthSeconds 251 * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f)); 252 float diff = Math.abs(endValue - currValue); 253 float velAbs = Math.abs(velocity); 254 float y2 = calculateLinearOutFasterInY2(velAbs); 255 256 float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2; 257 Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2); 258 float durationSeconds = startGradient * diff / velAbs; 259 if (durationSeconds <= maxLengthSeconds) { 260 mAnimatorProperties.interpolator = mLinearOutFasterIn; 261 } else if (velAbs >= mMinVelocityPxPerSecond) { 262 263 // Cross fade between linear-out-faster-in and linear interpolator with current 264 // velocity. 265 durationSeconds = maxLengthSeconds; 266 VelocityInterpolator velocityInterpolator 267 = new VelocityInterpolator(durationSeconds, velAbs, diff); 268 InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( 269 velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN); 270 mAnimatorProperties.interpolator = superInterpolator; 271 } else { 272 273 // Just use a normal interpolator which doesn't take the velocity into account. 274 durationSeconds = maxLengthSeconds; 275 mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN; 276 } 277 mAnimatorProperties.duration = (long) (durationSeconds * 1000); 278 return mAnimatorProperties; 279 } 280 281 /** 282 * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the 283 * velocity. The faster the velocity, the more "linear" the interpolator gets. 284 * 285 * @param velocity the velocity of the gesture. 286 * @return the y2 control point for a cubic bezier path interpolator 287 */ calculateLinearOutFasterInY2(float velocity)288 private float calculateLinearOutFasterInY2(float velocity) { 289 float t = (velocity - mMinVelocityPxPerSecond) 290 / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond); 291 t = Math.max(0, Math.min(1, t)); 292 return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX; 293 } 294 295 /** 296 * @return the minimum velocity a gesture needs to have to be considered a fling 297 */ getMinVelocityPxPerSecond()298 public float getMinVelocityPxPerSecond() { 299 return mMinVelocityPxPerSecond; 300 } 301 302 /** 303 * An interpolator which interpolates two interpolators with an interpolator. 304 */ 305 private static final class InterpolatorInterpolator implements Interpolator { 306 307 private Interpolator mInterpolator1; 308 private Interpolator mInterpolator2; 309 private Interpolator mCrossfader; 310 InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2, Interpolator crossfader)311 InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2, 312 Interpolator crossfader) { 313 mInterpolator1 = interpolator1; 314 mInterpolator2 = interpolator2; 315 mCrossfader = crossfader; 316 } 317 318 @Override getInterpolation(float input)319 public float getInterpolation(float input) { 320 float t = mCrossfader.getInterpolation(input); 321 return (1 - t) * mInterpolator1.getInterpolation(input) 322 + t * mInterpolator2.getInterpolation(input); 323 } 324 } 325 326 /** 327 * An interpolator which interpolates with a fixed velocity. 328 */ 329 private static final class VelocityInterpolator implements Interpolator { 330 331 private float mDurationSeconds; 332 private float mVelocity; 333 private float mDiff; 334 VelocityInterpolator(float durationSeconds, float velocity, float diff)335 private VelocityInterpolator(float durationSeconds, float velocity, float diff) { 336 mDurationSeconds = durationSeconds; 337 mVelocity = velocity; 338 mDiff = diff; 339 } 340 341 @Override getInterpolation(float input)342 public float getInterpolation(float input) { 343 float time = input * mDurationSeconds; 344 return time * mVelocity / mDiff; 345 } 346 } 347 348 private static class AnimatorProperties { 349 Interpolator interpolator; 350 long duration; 351 } 352 353 } 354