1 /* 2 * Copyright (C) 2006 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.launcher3; 18 19 import android.animation.TimeInterpolator; 20 import android.content.Context; 21 import android.hardware.SensorManager; 22 import android.os.Build; 23 import android.view.ViewConfiguration; 24 import android.view.animation.AnimationUtils; 25 import android.view.animation.Interpolator; 26 27 /** 28 * This class differs from the framework {@link android.widget.Scroller} in that 29 * you can modify the Interpolator post-construction. 30 */ 31 public class LauncherScroller { 32 private int mMode; 33 34 private int mStartX; 35 private int mStartY; 36 private int mFinalX; 37 private int mFinalY; 38 39 private int mMinX; 40 private int mMaxX; 41 private int mMinY; 42 private int mMaxY; 43 44 private int mCurrX; 45 private int mCurrY; 46 private long mStartTime; 47 private int mDuration; 48 private float mDurationReciprocal; 49 private float mDeltaX; 50 private float mDeltaY; 51 private boolean mFinished; 52 private TimeInterpolator mInterpolator; 53 private boolean mFlywheel; 54 55 private float mVelocity; 56 private float mCurrVelocity; 57 private int mDistance; 58 59 private float mFlingFriction = ViewConfiguration.getScrollFriction(); 60 61 private static final int DEFAULT_DURATION = 250; 62 private static final int SCROLL_MODE = 0; 63 private static final int FLING_MODE = 1; 64 65 private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); 66 private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) 67 private static final float START_TENSION = 0.5f; 68 private static final float END_TENSION = 1.0f; 69 private static final float P1 = START_TENSION * INFLEXION; 70 private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); 71 72 private static final int NB_SAMPLES = 100; 73 private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; 74 private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; 75 76 private float mDeceleration; 77 private final float mPpi; 78 79 // A context-specific coefficient adjusted to physical values. 80 private float mPhysicalCoeff; 81 82 static { 83 float x_min = 0.0f; 84 float y_min = 0.0f; 85 for (int i = 0; i < NB_SAMPLES; i++) { 86 final float alpha = (float) i / NB_SAMPLES; 87 88 float x_max = 1.0f; 89 float x, tx, coef; 90 while (true) { 91 x = x_min + (x_max - x_min) / 2.0f; 92 coef = 3.0f * x * (1.0f - x); 93 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; 94 if (Math.abs(tx - alpha) < 1E-5) break; 95 if (tx > alpha) x_max = x; 96 else x_min = x; 97 } 98 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; 99 100 float y_max = 1.0f; 101 float y, dy; 102 while (true) { 103 y = y_min + (y_max - y_min) / 2.0f; 104 coef = 3.0f * y * (1.0f - y); 105 dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; 106 if (Math.abs(dy - alpha) < 1E-5) break; 107 if (dy > alpha) y_max = y; 108 else y_min = y; 109 } 110 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; 111 } 112 SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; 113 114 // This controls the viscous fluid effect (how much of it) 115 sViscousFluidScale = 8.0f; 116 // must be set to 1.0 (used in viscousFluid()) 117 sViscousFluidNormalize = 1.0f; 118 sViscousFluidNormalize = 1.0f / viscousFluid(1.0f); 119 120 } 121 122 private static float sViscousFluidScale; 123 private static float sViscousFluidNormalize; 124 setInterpolator(TimeInterpolator interpolator)125 public void setInterpolator(TimeInterpolator interpolator) { 126 mInterpolator = interpolator; 127 } 128 129 /** 130 * Create a Scroller with the default duration and interpolator. 131 */ LauncherScroller(Context context)132 public LauncherScroller(Context context) { 133 this(context, null); 134 } 135 136 /** 137 * Create a Scroller with the specified interpolator. If the interpolator is 138 * null, the default (viscous) interpolator will be used. "Flywheel" behavior will 139 * be in effect for apps targeting Honeycomb or newer. 140 */ LauncherScroller(Context context, Interpolator interpolator)141 public LauncherScroller(Context context, Interpolator interpolator) { 142 this(context, interpolator, 143 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); 144 } 145 146 /** 147 * Create a Scroller with the specified interpolator. If the interpolator is 148 * null, the default (viscous) interpolator will be used. Specify whether or 149 * not to support progressive "flywheel" behavior in flinging. 150 */ LauncherScroller(Context context, Interpolator interpolator, boolean flywheel)151 public LauncherScroller(Context context, Interpolator interpolator, boolean flywheel) { 152 mFinished = true; 153 mInterpolator = interpolator; 154 mPpi = context.getResources().getDisplayMetrics().density * 160.0f; 155 mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); 156 mFlywheel = flywheel; 157 158 mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning 159 } 160 161 /** 162 * The amount of friction applied to flings. The default value 163 * is {@link ViewConfiguration#getScrollFriction}. 164 * 165 * @param friction A scalar dimension-less value representing the coefficient of 166 * friction. 167 */ setFriction(float friction)168 public final void setFriction(float friction) { 169 mDeceleration = computeDeceleration(friction); 170 mFlingFriction = friction; 171 } 172 computeDeceleration(float friction)173 private float computeDeceleration(float friction) { 174 return SensorManager.GRAVITY_EARTH // g (m/s^2) 175 * 39.37f // inch/meter 176 * mPpi // pixels per inch 177 * friction; 178 } 179 180 /** 181 * 182 * Returns whether the scroller has finished scrolling. 183 * 184 * @return True if the scroller has finished scrolling, false otherwise. 185 */ isFinished()186 public final boolean isFinished() { 187 return mFinished; 188 } 189 190 /** 191 * Force the finished field to a particular value. 192 * 193 * @param finished The new finished value. 194 */ forceFinished(boolean finished)195 public final void forceFinished(boolean finished) { 196 mFinished = finished; 197 } 198 199 /** 200 * Returns how long the scroll event will take, in milliseconds. 201 * 202 * @return The duration of the scroll in milliseconds. 203 */ getDuration()204 public final int getDuration() { 205 return mDuration; 206 } 207 208 /** 209 * Returns the current X offset in the scroll. 210 * 211 * @return The new X offset as an absolute distance from the origin. 212 */ getCurrX()213 public final int getCurrX() { 214 return mCurrX; 215 } 216 217 /** 218 * Returns the current Y offset in the scroll. 219 * 220 * @return The new Y offset as an absolute distance from the origin. 221 */ getCurrY()222 public final int getCurrY() { 223 return mCurrY; 224 } 225 226 /** 227 * Returns the current velocity. 228 * 229 * @return The original velocity less the deceleration. Result may be 230 * negative. 231 */ getCurrVelocity()232 public float getCurrVelocity() { 233 return mMode == FLING_MODE ? 234 mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f; 235 } 236 237 /** 238 * Returns the start X offset in the scroll. 239 * 240 * @return The start X offset as an absolute distance from the origin. 241 */ getStartX()242 public final int getStartX() { 243 return mStartX; 244 } 245 246 /** 247 * Returns the start Y offset in the scroll. 248 * 249 * @return The start Y offset as an absolute distance from the origin. 250 */ getStartY()251 public final int getStartY() { 252 return mStartY; 253 } 254 255 /** 256 * Returns where the scroll will end. Valid only for "fling" scrolls. 257 * 258 * @return The final X offset as an absolute distance from the origin. 259 */ getFinalX()260 public final int getFinalX() { 261 return mFinalX; 262 } 263 264 /** 265 * Returns where the scroll will end. Valid only for "fling" scrolls. 266 * 267 * @return The final Y offset as an absolute distance from the origin. 268 */ getFinalY()269 public final int getFinalY() { 270 return mFinalY; 271 } 272 273 /** 274 * Call this when you want to know the new location. If it returns true, 275 * the animation is not yet finished. 276 */ computeScrollOffset()277 public boolean computeScrollOffset() { 278 if (mFinished) { 279 return false; 280 } 281 282 int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 283 284 if (timePassed < mDuration) { 285 switch (mMode) { 286 case SCROLL_MODE: 287 float x = timePassed * mDurationReciprocal; 288 289 if (mInterpolator == null) 290 x = viscousFluid(x); 291 else 292 x = mInterpolator.getInterpolation(x); 293 294 mCurrX = mStartX + Math.round(x * mDeltaX); 295 mCurrY = mStartY + Math.round(x * mDeltaY); 296 break; 297 case FLING_MODE: 298 final float t = (float) timePassed / mDuration; 299 final int index = (int) (NB_SAMPLES * t); 300 float distanceCoef = 1.f; 301 float velocityCoef = 0.f; 302 if (index < NB_SAMPLES) { 303 final float t_inf = (float) index / NB_SAMPLES; 304 final float t_sup = (float) (index + 1) / NB_SAMPLES; 305 final float d_inf = SPLINE_POSITION[index]; 306 final float d_sup = SPLINE_POSITION[index + 1]; 307 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); 308 distanceCoef = d_inf + (t - t_inf) * velocityCoef; 309 } 310 311 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; 312 313 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); 314 // Pin to mMinX <= mCurrX <= mMaxX 315 mCurrX = Math.min(mCurrX, mMaxX); 316 mCurrX = Math.max(mCurrX, mMinX); 317 318 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); 319 // Pin to mMinY <= mCurrY <= mMaxY 320 mCurrY = Math.min(mCurrY, mMaxY); 321 mCurrY = Math.max(mCurrY, mMinY); 322 323 if (mCurrX == mFinalX && mCurrY == mFinalY) { 324 mFinished = true; 325 } 326 327 break; 328 } 329 } 330 else { 331 mCurrX = mFinalX; 332 mCurrY = mFinalY; 333 mFinished = true; 334 } 335 return true; 336 } 337 338 /** 339 * Start scrolling by providing a starting point and the distance to travel. 340 * The scroll will use the default value of 250 milliseconds for the 341 * duration. 342 * 343 * @param startX Starting horizontal scroll offset in pixels. Positive 344 * numbers will scroll the content to the left. 345 * @param startY Starting vertical scroll offset in pixels. Positive numbers 346 * will scroll the content up. 347 * @param dx Horizontal distance to travel. Positive numbers will scroll the 348 * content to the left. 349 * @param dy Vertical distance to travel. Positive numbers will scroll the 350 * content up. 351 */ startScroll(int startX, int startY, int dx, int dy)352 public void startScroll(int startX, int startY, int dx, int dy) { 353 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 354 } 355 356 /** 357 * Start scrolling by providing a starting point, the distance to travel, 358 * and the duration of the scroll. 359 * 360 * @param startX Starting horizontal scroll offset in pixels. Positive 361 * numbers will scroll the content to the left. 362 * @param startY Starting vertical scroll offset in pixels. Positive numbers 363 * will scroll the content up. 364 * @param dx Horizontal distance to travel. Positive numbers will scroll the 365 * content to the left. 366 * @param dy Vertical distance to travel. Positive numbers will scroll the 367 * content up. 368 * @param duration Duration of the scroll in milliseconds. 369 */ startScroll(int startX, int startY, int dx, int dy, int duration)370 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 371 mMode = SCROLL_MODE; 372 mFinished = false; 373 mDuration = duration; 374 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 375 mStartX = startX; 376 mStartY = startY; 377 mFinalX = startX + dx; 378 mFinalY = startY + dy; 379 mDeltaX = dx; 380 mDeltaY = dy; 381 mDurationReciprocal = 1.0f / (float) mDuration; 382 } 383 384 /** 385 * Start scrolling based on a fling gesture. The distance travelled will 386 * depend on the initial velocity of the fling. 387 * 388 * @param startX Starting point of the scroll (X) 389 * @param startY Starting point of the scroll (Y) 390 * @param velocityX Initial velocity of the fling (X) measured in pixels per 391 * second. 392 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 393 * second 394 * @param minX Minimum X value. The scroller will not scroll past this 395 * point. 396 * @param maxX Maximum X value. The scroller will not scroll past this 397 * point. 398 * @param minY Minimum Y value. The scroller will not scroll past this 399 * point. 400 * @param maxY Maximum Y value. The scroller will not scroll past this 401 * point. 402 */ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)403 public void fling(int startX, int startY, int velocityX, int velocityY, 404 int minX, int maxX, int minY, int maxY) { 405 // Continue a scroll or fling in progress 406 if (mFlywheel && !mFinished) { 407 float oldVel = getCurrVelocity(); 408 409 float dx = (float) (mFinalX - mStartX); 410 float dy = (float) (mFinalY - mStartY); 411 float hyp = (float) Math.hypot(dx, dy); 412 413 float ndx = dx / hyp; 414 float ndy = dy / hyp; 415 416 float oldVelocityX = ndx * oldVel; 417 float oldVelocityY = ndy * oldVel; 418 if (Math.signum(velocityX) == Math.signum(oldVelocityX) && 419 Math.signum(velocityY) == Math.signum(oldVelocityY)) { 420 velocityX += oldVelocityX; 421 velocityY += oldVelocityY; 422 } 423 } 424 425 mMode = FLING_MODE; 426 mFinished = false; 427 428 float velocity = (float) Math.hypot(velocityX, velocityY); 429 430 mVelocity = velocity; 431 mDuration = getSplineFlingDuration(velocity); 432 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 433 mStartX = startX; 434 mStartY = startY; 435 436 float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; 437 float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; 438 439 double totalDistance = getSplineFlingDistance(velocity); 440 mDistance = (int) (totalDistance * Math.signum(velocity)); 441 442 mMinX = minX; 443 mMaxX = maxX; 444 mMinY = minY; 445 mMaxY = maxY; 446 447 mFinalX = startX + (int) Math.round(totalDistance * coeffX); 448 // Pin to mMinX <= mFinalX <= mMaxX 449 mFinalX = Math.min(mFinalX, mMaxX); 450 mFinalX = Math.max(mFinalX, mMinX); 451 452 mFinalY = startY + (int) Math.round(totalDistance * coeffY); 453 // Pin to mMinY <= mFinalY <= mMaxY 454 mFinalY = Math.min(mFinalY, mMaxY); 455 mFinalY = Math.max(mFinalY, mMinY); 456 } 457 getSplineDeceleration(float velocity)458 private double getSplineDeceleration(float velocity) { 459 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); 460 } 461 getSplineFlingDuration(float velocity)462 private int getSplineFlingDuration(float velocity) { 463 final double l = getSplineDeceleration(velocity); 464 final double decelMinusOne = DECELERATION_RATE - 1.0; 465 return (int) (1000.0 * Math.exp(l / decelMinusOne)); 466 } 467 getSplineFlingDistance(float velocity)468 private double getSplineFlingDistance(float velocity) { 469 final double l = getSplineDeceleration(velocity); 470 final double decelMinusOne = DECELERATION_RATE - 1.0; 471 return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); 472 } 473 viscousFluid(float x)474 static float viscousFluid(float x) 475 { 476 x *= sViscousFluidScale; 477 if (x < 1.0f) { 478 x -= (1.0f - (float)Math.exp(-x)); 479 } else { 480 float start = 0.36787944117f; // 1/e == exp(-1) 481 x = 1.0f - (float)Math.exp(1.0f - x); 482 x = start + x * (1.0f - start); 483 } 484 x *= sViscousFluidNormalize; 485 return x; 486 } 487 488 /** 489 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 490 * aborting the animating cause the scroller to move to the final x and y 491 * position 492 * 493 * @see #forceFinished(boolean) 494 */ abortAnimation()495 public void abortAnimation() { 496 mCurrX = mFinalX; 497 mCurrY = mFinalY; 498 mFinished = true; 499 } 500 501 /** 502 * Extend the scroll animation. This allows a running animation to scroll 503 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 504 * 505 * @param extend Additional time to scroll in milliseconds. 506 * @see #setFinalX(int) 507 * @see #setFinalY(int) 508 */ extendDuration(int extend)509 public void extendDuration(int extend) { 510 int passed = timePassed(); 511 mDuration = passed + extend; 512 mDurationReciprocal = 1.0f / mDuration; 513 mFinished = false; 514 } 515 516 /** 517 * Returns the time elapsed since the beginning of the scrolling. 518 * 519 * @return The elapsed time in milliseconds. 520 */ timePassed()521 public int timePassed() { 522 return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 523 } 524 525 /** 526 * Sets the final position (X) for this scroller. 527 * 528 * @param newX The new X offset as an absolute distance from the origin. 529 * @see #extendDuration(int) 530 * @see #setFinalY(int) 531 */ setFinalX(int newX)532 public void setFinalX(int newX) { 533 mFinalX = newX; 534 mDeltaX = mFinalX - mStartX; 535 mFinished = false; 536 } 537 538 /** 539 * Sets the final position (Y) for this scroller. 540 * 541 * @param newY The new Y offset as an absolute distance from the origin. 542 * @see #extendDuration(int) 543 * @see #setFinalX(int) 544 */ setFinalY(int newY)545 public void setFinalY(int newY) { 546 mFinalY = newY; 547 mDeltaY = mFinalY - mStartY; 548 mFinished = false; 549 } 550 551 /** 552 * @hide 553 */ isScrollingInDirection(float xvel, float yvel)554 public boolean isScrollingInDirection(float xvel, float yvel) { 555 return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && 556 Math.signum(yvel) == Math.signum(mFinalY - mStartY); 557 } 558 } 559