1 /* 2 * Copyright (C) 2010 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 android.widget; 18 19 import android.content.Context; 20 import android.hardware.SensorManager; 21 import android.util.Log; 22 import android.view.ViewConfiguration; 23 import android.view.animation.AnimationUtils; 24 import android.view.animation.Interpolator; 25 26 /** 27 * This class encapsulates scrolling with the ability to overshoot the bounds 28 * of a scrolling operation. This class is a drop-in replacement for 29 * {@link android.widget.Scroller} in most cases. 30 */ 31 public class OverScroller { 32 private int mMode; 33 34 private final SplineOverScroller mScrollerX; 35 private final SplineOverScroller mScrollerY; 36 37 private Interpolator mInterpolator; 38 39 private final boolean mFlywheel; 40 41 private static final int DEFAULT_DURATION = 250; 42 private static final int SCROLL_MODE = 0; 43 private static final int FLING_MODE = 1; 44 45 /** 46 * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel. 47 * @param context 48 */ OverScroller(Context context)49 public OverScroller(Context context) { 50 this(context, null); 51 } 52 53 /** 54 * Creates an OverScroller with flywheel enabled. 55 * @param context The context of this application. 56 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 57 * be used. 58 */ OverScroller(Context context, Interpolator interpolator)59 public OverScroller(Context context, Interpolator interpolator) { 60 this(context, interpolator, true); 61 } 62 63 /** 64 * Creates an OverScroller. 65 * @param context The context of this application. 66 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 67 * be used. 68 * @param flywheel If true, successive fling motions will keep on increasing scroll speed. 69 * @hide 70 */ OverScroller(Context context, Interpolator interpolator, boolean flywheel)71 public OverScroller(Context context, Interpolator interpolator, boolean flywheel) { 72 if (interpolator == null) { 73 mInterpolator = new Scroller.ViscousFluidInterpolator(); 74 } else { 75 mInterpolator = interpolator; 76 } 77 mFlywheel = flywheel; 78 mScrollerX = new SplineOverScroller(context); 79 mScrollerY = new SplineOverScroller(context); 80 } 81 82 /** 83 * Creates an OverScroller with flywheel enabled. 84 * @param context The context of this application. 85 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 86 * be used. 87 * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the 88 * velocity which is preserved in the bounce when the horizontal edge is reached. A null value 89 * means no bounce. This behavior is no longer supported and this coefficient has no effect. 90 * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This 91 * behavior is no longer supported and this coefficient has no effect. 92 * @deprecated Use {@link #OverScroller(Context, Interpolator)} instead. 93 */ 94 @Deprecated OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY)95 public OverScroller(Context context, Interpolator interpolator, 96 float bounceCoefficientX, float bounceCoefficientY) { 97 this(context, interpolator, true); 98 } 99 100 /** 101 * Creates an OverScroller. 102 * @param context The context of this application. 103 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 104 * be used. 105 * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the 106 * velocity which is preserved in the bounce when the horizontal edge is reached. A null value 107 * means no bounce. This behavior is no longer supported and this coefficient has no effect. 108 * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This 109 * behavior is no longer supported and this coefficient has no effect. 110 * @param flywheel If true, successive fling motions will keep on increasing scroll speed. 111 * @deprecated Use {@link #OverScroller(Context, Interpolator)} instead. 112 */ 113 @Deprecated OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY, boolean flywheel)114 public OverScroller(Context context, Interpolator interpolator, 115 float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) { 116 this(context, interpolator, flywheel); 117 } 118 setInterpolator(Interpolator interpolator)119 void setInterpolator(Interpolator interpolator) { 120 if (interpolator == null) { 121 mInterpolator = new Scroller.ViscousFluidInterpolator(); 122 } else { 123 mInterpolator = interpolator; 124 } 125 } 126 127 /** 128 * The amount of friction applied to flings. The default value 129 * is {@link ViewConfiguration#getScrollFriction}. 130 * 131 * @param friction A scalar dimension-less value representing the coefficient of 132 * friction. 133 */ setFriction(float friction)134 public final void setFriction(float friction) { 135 mScrollerX.setFriction(friction); 136 mScrollerY.setFriction(friction); 137 } 138 139 /** 140 * 141 * Returns whether the scroller has finished scrolling. 142 * 143 * @return True if the scroller has finished scrolling, false otherwise. 144 */ isFinished()145 public final boolean isFinished() { 146 return mScrollerX.mFinished && mScrollerY.mFinished; 147 } 148 149 /** 150 * Force the finished field to a particular value. Contrary to 151 * {@link #abortAnimation()}, forcing the animation to finished 152 * does NOT cause the scroller to move to the final x and y 153 * position. 154 * 155 * @param finished The new finished value. 156 */ forceFinished(boolean finished)157 public final void forceFinished(boolean finished) { 158 mScrollerX.mFinished = mScrollerY.mFinished = finished; 159 } 160 161 /** 162 * Returns the current X offset in the scroll. 163 * 164 * @return The new X offset as an absolute distance from the origin. 165 */ getCurrX()166 public final int getCurrX() { 167 return mScrollerX.mCurrentPosition; 168 } 169 170 /** 171 * Returns the current Y offset in the scroll. 172 * 173 * @return The new Y offset as an absolute distance from the origin. 174 */ getCurrY()175 public final int getCurrY() { 176 return mScrollerY.mCurrentPosition; 177 } 178 179 /** 180 * Returns the absolute value of the current velocity. 181 * 182 * @return The original velocity less the deceleration, norm of the X and Y velocity vector. 183 */ getCurrVelocity()184 public float getCurrVelocity() { 185 return (float) Math.hypot(mScrollerX.mCurrVelocity, mScrollerY.mCurrVelocity); 186 } 187 188 /** 189 * Returns the start X offset in the scroll. 190 * 191 * @return The start X offset as an absolute distance from the origin. 192 */ getStartX()193 public final int getStartX() { 194 return mScrollerX.mStart; 195 } 196 197 /** 198 * Returns the start Y offset in the scroll. 199 * 200 * @return The start Y offset as an absolute distance from the origin. 201 */ getStartY()202 public final int getStartY() { 203 return mScrollerY.mStart; 204 } 205 206 /** 207 * Returns where the scroll will end. Valid only for "fling" scrolls. 208 * 209 * @return The final X offset as an absolute distance from the origin. 210 */ getFinalX()211 public final int getFinalX() { 212 return mScrollerX.mFinal; 213 } 214 215 /** 216 * Returns where the scroll will end. Valid only for "fling" scrolls. 217 * 218 * @return The final Y offset as an absolute distance from the origin. 219 */ getFinalY()220 public final int getFinalY() { 221 return mScrollerY.mFinal; 222 } 223 224 /** 225 * Returns how long the scroll event will take, in milliseconds. 226 * 227 * @return The duration of the scroll in milliseconds. 228 * 229 * @hide Pending removal once nothing depends on it 230 * @deprecated OverScrollers don't necessarily have a fixed duration. 231 * This function will lie to the best of its ability. 232 */ 233 @Deprecated getDuration()234 public final int getDuration() { 235 return Math.max(mScrollerX.mDuration, mScrollerY.mDuration); 236 } 237 238 /** 239 * Extend the scroll animation. This allows a running animation to scroll 240 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 241 * 242 * @param extend Additional time to scroll in milliseconds. 243 * @see #setFinalX(int) 244 * @see #setFinalY(int) 245 * 246 * @hide Pending removal once nothing depends on it 247 * @deprecated OverScrollers don't necessarily have a fixed duration. 248 * Instead of setting a new final position and extending 249 * the duration of an existing scroll, use startScroll 250 * to begin a new animation. 251 */ 252 @Deprecated extendDuration(int extend)253 public void extendDuration(int extend) { 254 mScrollerX.extendDuration(extend); 255 mScrollerY.extendDuration(extend); 256 } 257 258 /** 259 * Sets the final position (X) for this scroller. 260 * 261 * @param newX The new X offset as an absolute distance from the origin. 262 * @see #extendDuration(int) 263 * @see #setFinalY(int) 264 * 265 * @hide Pending removal once nothing depends on it 266 * @deprecated OverScroller's final position may change during an animation. 267 * Instead of setting a new final position and extending 268 * the duration of an existing scroll, use startScroll 269 * to begin a new animation. 270 */ 271 @Deprecated setFinalX(int newX)272 public void setFinalX(int newX) { 273 mScrollerX.setFinalPosition(newX); 274 } 275 276 /** 277 * Sets the final position (Y) for this scroller. 278 * 279 * @param newY The new Y offset as an absolute distance from the origin. 280 * @see #extendDuration(int) 281 * @see #setFinalX(int) 282 * 283 * @hide Pending removal once nothing depends on it 284 * @deprecated OverScroller's final position may change during an animation. 285 * Instead of setting a new final position and extending 286 * the duration of an existing scroll, use startScroll 287 * to begin a new animation. 288 */ 289 @Deprecated setFinalY(int newY)290 public void setFinalY(int newY) { 291 mScrollerY.setFinalPosition(newY); 292 } 293 294 /** 295 * Call this when you want to know the new location. If it returns true, the 296 * animation is not yet finished. 297 */ computeScrollOffset()298 public boolean computeScrollOffset() { 299 if (isFinished()) { 300 return false; 301 } 302 303 switch (mMode) { 304 case SCROLL_MODE: 305 long time = AnimationUtils.currentAnimationTimeMillis(); 306 // Any scroller can be used for time, since they were started 307 // together in scroll mode. We use X here. 308 final long elapsedTime = time - mScrollerX.mStartTime; 309 310 final int duration = mScrollerX.mDuration; 311 if (elapsedTime < duration) { 312 final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration); 313 mScrollerX.updateScroll(q); 314 mScrollerY.updateScroll(q); 315 } else { 316 abortAnimation(); 317 } 318 break; 319 320 case FLING_MODE: 321 if (!mScrollerX.mFinished) { 322 if (!mScrollerX.update()) { 323 if (!mScrollerX.continueWhenFinished()) { 324 mScrollerX.finish(); 325 } 326 } 327 } 328 329 if (!mScrollerY.mFinished) { 330 if (!mScrollerY.update()) { 331 if (!mScrollerY.continueWhenFinished()) { 332 mScrollerY.finish(); 333 } 334 } 335 } 336 337 break; 338 } 339 340 return true; 341 } 342 343 /** 344 * Start scrolling by providing a starting point and the distance to travel. 345 * The scroll will use the default value of 250 milliseconds for the 346 * duration. 347 * 348 * @param startX Starting horizontal scroll offset in pixels. Positive 349 * numbers will scroll the content to the left. 350 * @param startY Starting vertical scroll offset in pixels. Positive numbers 351 * will scroll the content up. 352 * @param dx Horizontal distance to travel. Positive numbers will scroll the 353 * content to the left. 354 * @param dy Vertical distance to travel. Positive numbers will scroll the 355 * content up. 356 */ startScroll(int startX, int startY, int dx, int dy)357 public void startScroll(int startX, int startY, int dx, int dy) { 358 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 359 } 360 361 /** 362 * Start scrolling by providing a starting point and the distance to travel. 363 * 364 * @param startX Starting horizontal scroll offset in pixels. Positive 365 * numbers will scroll the content to the left. 366 * @param startY Starting vertical scroll offset in pixels. Positive numbers 367 * will scroll the content up. 368 * @param dx Horizontal distance to travel. Positive numbers will scroll the 369 * content to the left. 370 * @param dy Vertical distance to travel. Positive numbers will scroll the 371 * content up. 372 * @param duration Duration of the scroll in milliseconds. 373 */ startScroll(int startX, int startY, int dx, int dy, int duration)374 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 375 mMode = SCROLL_MODE; 376 mScrollerX.startScroll(startX, dx, duration); 377 mScrollerY.startScroll(startY, dy, duration); 378 } 379 380 /** 381 * Call this when you want to 'spring back' into a valid coordinate range. 382 * 383 * @param startX Starting X coordinate 384 * @param startY Starting Y coordinate 385 * @param minX Minimum valid X value 386 * @param maxX Maximum valid X value 387 * @param minY Minimum valid Y value 388 * @param maxY Minimum valid Y value 389 * @return true if a springback was initiated, false if startX and startY were 390 * already within the valid range. 391 */ springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)392 public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) { 393 mMode = FLING_MODE; 394 395 // Make sure both methods are called. 396 final boolean spingbackX = mScrollerX.springback(startX, minX, maxX); 397 final boolean spingbackY = mScrollerY.springback(startY, minY, maxY); 398 return spingbackX || spingbackY; 399 } 400 fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)401 public void fling(int startX, int startY, int velocityX, int velocityY, 402 int minX, int maxX, int minY, int maxY) { 403 fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); 404 } 405 406 /** 407 * Start scrolling based on a fling gesture. The distance traveled will 408 * depend on the initial velocity of the fling. 409 * 410 * @param startX Starting point of the scroll (X) 411 * @param startY Starting point of the scroll (Y) 412 * @param velocityX Initial velocity of the fling (X) measured in pixels per 413 * second. 414 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 415 * second 416 * @param minX Minimum X value. The scroller will not scroll past this point 417 * unless overX > 0. If overfling is allowed, it will use minX as 418 * a springback boundary. 419 * @param maxX Maximum X value. The scroller will not scroll past this point 420 * unless overX > 0. If overfling is allowed, it will use maxX as 421 * a springback boundary. 422 * @param minY Minimum Y value. The scroller will not scroll past this point 423 * unless overY > 0. If overfling is allowed, it will use minY as 424 * a springback boundary. 425 * @param maxY Maximum Y value. The scroller will not scroll past this point 426 * unless overY > 0. If overfling is allowed, it will use maxY as 427 * a springback boundary. 428 * @param overX Overfling range. If > 0, horizontal overfling in either 429 * direction will be possible. 430 * @param overY Overfling range. If > 0, vertical overfling in either 431 * direction will be possible. 432 */ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)433 public void fling(int startX, int startY, int velocityX, int velocityY, 434 int minX, int maxX, int minY, int maxY, int overX, int overY) { 435 // Continue a scroll or fling in progress 436 if (mFlywheel && !isFinished()) { 437 float oldVelocityX = mScrollerX.mCurrVelocity; 438 float oldVelocityY = mScrollerY.mCurrVelocity; 439 if (Math.signum(velocityX) == Math.signum(oldVelocityX) && 440 Math.signum(velocityY) == Math.signum(oldVelocityY)) { 441 velocityX += oldVelocityX; 442 velocityY += oldVelocityY; 443 } 444 } 445 446 mMode = FLING_MODE; 447 mScrollerX.fling(startX, velocityX, minX, maxX, overX); 448 mScrollerY.fling(startY, velocityY, minY, maxY, overY); 449 } 450 451 /** 452 * Notify the scroller that we've reached a horizontal boundary. 453 * Normally the information to handle this will already be known 454 * when the animation is started, such as in a call to one of the 455 * fling functions. However there are cases where this cannot be known 456 * in advance. This function will transition the current motion and 457 * animate from startX to finalX as appropriate. 458 * 459 * @param startX Starting/current X position 460 * @param finalX Desired final X position 461 * @param overX Magnitude of overscroll allowed. This should be the maximum 462 * desired distance from finalX. Absolute value - must be positive. 463 */ notifyHorizontalEdgeReached(int startX, int finalX, int overX)464 public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { 465 mScrollerX.notifyEdgeReached(startX, finalX, overX); 466 } 467 468 /** 469 * Notify the scroller that we've reached a vertical boundary. 470 * Normally the information to handle this will already be known 471 * when the animation is started, such as in a call to one of the 472 * fling functions. However there are cases where this cannot be known 473 * in advance. This function will animate a parabolic motion from 474 * startY to finalY. 475 * 476 * @param startY Starting/current Y position 477 * @param finalY Desired final Y position 478 * @param overY Magnitude of overscroll allowed. This should be the maximum 479 * desired distance from finalY. Absolute value - must be positive. 480 */ notifyVerticalEdgeReached(int startY, int finalY, int overY)481 public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { 482 mScrollerY.notifyEdgeReached(startY, finalY, overY); 483 } 484 485 /** 486 * Returns whether the current Scroller is currently returning to a valid position. 487 * Valid bounds were provided by the 488 * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. 489 * 490 * One should check this value before calling 491 * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress 492 * to restore a valid position will then be stopped. The caller has to take into account 493 * the fact that the started scroll will start from an overscrolled position. 494 * 495 * @return true when the current position is overscrolled and in the process of 496 * interpolating back to a valid value. 497 */ isOverScrolled()498 public boolean isOverScrolled() { 499 return ((!mScrollerX.mFinished && 500 mScrollerX.mState != SplineOverScroller.SPLINE) || 501 (!mScrollerY.mFinished && 502 mScrollerY.mState != SplineOverScroller.SPLINE)); 503 } 504 505 /** 506 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 507 * aborting the animating causes the scroller to move to the final x and y 508 * positions. 509 * 510 * @see #forceFinished(boolean) 511 */ abortAnimation()512 public void abortAnimation() { 513 mScrollerX.finish(); 514 mScrollerY.finish(); 515 } 516 517 /** 518 * Returns the time elapsed since the beginning of the scrolling. 519 * 520 * @return The elapsed time in milliseconds. 521 * 522 * @hide 523 */ timePassed()524 public int timePassed() { 525 final long time = AnimationUtils.currentAnimationTimeMillis(); 526 final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime); 527 return (int) (time - startTime); 528 } 529 530 /** 531 * @hide 532 */ isScrollingInDirection(float xvel, float yvel)533 public boolean isScrollingInDirection(float xvel, float yvel) { 534 final int dx = mScrollerX.mFinal - mScrollerX.mStart; 535 final int dy = mScrollerY.mFinal - mScrollerY.mStart; 536 return !isFinished() && Math.signum(xvel) == Math.signum(dx) && 537 Math.signum(yvel) == Math.signum(dy); 538 } 539 540 static class SplineOverScroller { 541 // Initial position 542 private int mStart; 543 544 // Current position 545 private int mCurrentPosition; 546 547 // Final position 548 private int mFinal; 549 550 // Initial velocity 551 private int mVelocity; 552 553 // Current velocity 554 private float mCurrVelocity; 555 556 // Constant current deceleration 557 private float mDeceleration; 558 559 // Animation starting time, in system milliseconds 560 private long mStartTime; 561 562 // Animation duration, in milliseconds 563 private int mDuration; 564 565 // Duration to complete spline component of animation 566 private int mSplineDuration; 567 568 // Distance to travel along spline animation 569 private int mSplineDistance; 570 571 // Whether the animation is currently in progress 572 private boolean mFinished; 573 574 // The allowed overshot distance before boundary is reached. 575 private int mOver; 576 577 // Fling friction 578 private float mFlingFriction = ViewConfiguration.getScrollFriction(); 579 580 // Current state of the animation. 581 private int mState = SPLINE; 582 583 // Constant gravity value, used in the deceleration phase. 584 private static final float GRAVITY = 2000.0f; 585 586 // A context-specific coefficient adjusted to physical values. 587 private float mPhysicalCoeff; 588 589 private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); 590 private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) 591 private static final float START_TENSION = 0.5f; 592 private static final float END_TENSION = 1.0f; 593 private static final float P1 = START_TENSION * INFLEXION; 594 private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); 595 596 private static final int NB_SAMPLES = 100; 597 private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; 598 private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; 599 600 private static final int SPLINE = 0; 601 private static final int CUBIC = 1; 602 private static final int BALLISTIC = 2; 603 604 static { 605 float x_min = 0.0f; 606 float y_min = 0.0f; 607 for (int i = 0; i < NB_SAMPLES; i++) { 608 final float alpha = (float) i / NB_SAMPLES; 609 610 float x_max = 1.0f; 611 float x, tx, coef; 612 while (true) { 613 x = x_min + (x_max - x_min) / 2.0f; 614 coef = 3.0f * x * (1.0f - x); 615 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; 616 if (Math.abs(tx - alpha) < 1E-5) break; 617 if (tx > alpha) x_max = x; 618 else x_min = x; 619 } 620 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; 621 622 float y_max = 1.0f; 623 float y, dy; 624 while (true) { 625 y = y_min + (y_max - y_min) / 2.0f; 626 coef = 3.0f * y * (1.0f - y); 627 dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; 628 if (Math.abs(dy - alpha) < 1E-5) break; 629 if (dy > alpha) y_max = y; 630 else y_min = y; 631 } 632 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; 633 } 634 SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; 635 } 636 setFriction(float friction)637 void setFriction(float friction) { 638 mFlingFriction = friction; 639 } 640 SplineOverScroller(Context context)641 SplineOverScroller(Context context) { 642 mFinished = true; 643 final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; 644 mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2) 645 * 39.37f // inch/meter 646 * ppi 647 * 0.84f; // look and feel tuning 648 } 649 updateScroll(float q)650 void updateScroll(float q) { 651 mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); 652 } 653 654 /* 655 * Get a signed deceleration that will reduce the velocity. 656 */ getDeceleration(int velocity)657 static private float getDeceleration(int velocity) { 658 return velocity > 0 ? -GRAVITY : GRAVITY; 659 } 660 661 /* 662 * Modifies mDuration to the duration it takes to get from start to newFinal using the 663 * spline interpolation. The previous duration was needed to get to oldFinal. 664 */ adjustDuration(int start, int oldFinal, int newFinal)665 private void adjustDuration(int start, int oldFinal, int newFinal) { 666 final int oldDistance = oldFinal - start; 667 final int newDistance = newFinal - start; 668 final float x = Math.abs((float) newDistance / oldDistance); 669 final int index = (int) (NB_SAMPLES * x); 670 if (index < NB_SAMPLES) { 671 final float x_inf = (float) index / NB_SAMPLES; 672 final float x_sup = (float) (index + 1) / NB_SAMPLES; 673 final float t_inf = SPLINE_TIME[index]; 674 final float t_sup = SPLINE_TIME[index + 1]; 675 final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf); 676 mDuration *= timeCoef; 677 } 678 } 679 startScroll(int start, int distance, int duration)680 void startScroll(int start, int distance, int duration) { 681 mFinished = false; 682 683 mCurrentPosition = mStart = start; 684 mFinal = start + distance; 685 686 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 687 mDuration = duration; 688 689 // Unused 690 mDeceleration = 0.0f; 691 mVelocity = 0; 692 } 693 finish()694 void finish() { 695 mCurrentPosition = mFinal; 696 // Not reset since WebView relies on this value for fast fling. 697 // TODO: restore when WebView uses the fast fling implemented in this class. 698 // mCurrVelocity = 0.0f; 699 mFinished = true; 700 } 701 setFinalPosition(int position)702 void setFinalPosition(int position) { 703 mFinal = position; 704 mFinished = false; 705 } 706 extendDuration(int extend)707 void extendDuration(int extend) { 708 final long time = AnimationUtils.currentAnimationTimeMillis(); 709 final int elapsedTime = (int) (time - mStartTime); 710 mDuration = elapsedTime + extend; 711 mFinished = false; 712 } 713 springback(int start, int min, int max)714 boolean springback(int start, int min, int max) { 715 mFinished = true; 716 717 mCurrentPosition = mStart = mFinal = start; 718 mVelocity = 0; 719 720 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 721 mDuration = 0; 722 723 if (start < min) { 724 startSpringback(start, min, 0); 725 } else if (start > max) { 726 startSpringback(start, max, 0); 727 } 728 729 return !mFinished; 730 } 731 startSpringback(int start, int end, int velocity)732 private void startSpringback(int start, int end, int velocity) { 733 // mStartTime has been set 734 mFinished = false; 735 mState = CUBIC; 736 mCurrentPosition = mStart = start; 737 mFinal = end; 738 final int delta = start - end; 739 mDeceleration = getDeceleration(delta); 740 // TODO take velocity into account 741 mVelocity = -delta; // only sign is used 742 mOver = Math.abs(delta); 743 mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration)); 744 } 745 fling(int start, int velocity, int min, int max, int over)746 void fling(int start, int velocity, int min, int max, int over) { 747 mOver = over; 748 mFinished = false; 749 mCurrVelocity = mVelocity = velocity; 750 mDuration = mSplineDuration = 0; 751 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 752 mCurrentPosition = mStart = start; 753 754 if (start > max || start < min) { 755 startAfterEdge(start, min, max, velocity); 756 return; 757 } 758 759 mState = SPLINE; 760 double totalDistance = 0.0; 761 762 if (velocity != 0) { 763 mDuration = mSplineDuration = getSplineFlingDuration(velocity); 764 totalDistance = getSplineFlingDistance(velocity); 765 } 766 767 mSplineDistance = (int) (totalDistance * Math.signum(velocity)); 768 mFinal = start + mSplineDistance; 769 770 // Clamp to a valid final position 771 if (mFinal < min) { 772 adjustDuration(mStart, mFinal, min); 773 mFinal = min; 774 } 775 776 if (mFinal > max) { 777 adjustDuration(mStart, mFinal, max); 778 mFinal = max; 779 } 780 } 781 getSplineDeceleration(int velocity)782 private double getSplineDeceleration(int velocity) { 783 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); 784 } 785 getSplineFlingDistance(int velocity)786 private double getSplineFlingDistance(int velocity) { 787 final double l = getSplineDeceleration(velocity); 788 final double decelMinusOne = DECELERATION_RATE - 1.0; 789 return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); 790 } 791 792 /* Returns the duration, expressed in milliseconds */ getSplineFlingDuration(int velocity)793 private int getSplineFlingDuration(int velocity) { 794 final double l = getSplineDeceleration(velocity); 795 final double decelMinusOne = DECELERATION_RATE - 1.0; 796 return (int) (1000.0 * Math.exp(l / decelMinusOne)); 797 } 798 fitOnBounceCurve(int start, int end, int velocity)799 private void fitOnBounceCurve(int start, int end, int velocity) { 800 // Simulate a bounce that started from edge 801 final float durationToApex = - velocity / mDeceleration; 802 // The float cast below is necessary to avoid integer overflow. 803 final float velocitySquared = (float) velocity * velocity; 804 final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration); 805 final float distanceToEdge = Math.abs(end - start); 806 final float totalDuration = (float) Math.sqrt( 807 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration)); 808 mStartTime -= (int) (1000.0f * (totalDuration - durationToApex)); 809 mCurrentPosition = mStart = end; 810 mVelocity = (int) (- mDeceleration * totalDuration); 811 } 812 startBounceAfterEdge(int start, int end, int velocity)813 private void startBounceAfterEdge(int start, int end, int velocity) { 814 mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity); 815 fitOnBounceCurve(start, end, velocity); 816 onEdgeReached(); 817 } 818 startAfterEdge(int start, int min, int max, int velocity)819 private void startAfterEdge(int start, int min, int max, int velocity) { 820 if (start > min && start < max) { 821 Log.e("OverScroller", "startAfterEdge called from a valid position"); 822 mFinished = true; 823 return; 824 } 825 final boolean positive = start > max; 826 final int edge = positive ? max : min; 827 final int overDistance = start - edge; 828 boolean keepIncreasing = overDistance * velocity >= 0; 829 if (keepIncreasing) { 830 // Will result in a bounce or a to_boundary depending on velocity. 831 startBounceAfterEdge(start, edge, velocity); 832 } else { 833 final double totalDistance = getSplineFlingDistance(velocity); 834 if (totalDistance > Math.abs(overDistance)) { 835 fling(start, velocity, positive ? min : start, positive ? start : max, mOver); 836 } else { 837 startSpringback(start, edge, velocity); 838 } 839 } 840 } 841 notifyEdgeReached(int start, int end, int over)842 void notifyEdgeReached(int start, int end, int over) { 843 // mState is used to detect successive notifications 844 if (mState == SPLINE) { 845 mOver = over; 846 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 847 // We were in fling/scroll mode before: current velocity is such that distance to 848 // edge is increasing. This ensures that startAfterEdge will not start a new fling. 849 startAfterEdge(start, end, end, (int) mCurrVelocity); 850 } 851 } 852 onEdgeReached()853 private void onEdgeReached() { 854 // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. 855 // The float cast below is necessary to avoid integer overflow. 856 final float velocitySquared = (float) mVelocity * mVelocity; 857 float distance = velocitySquared / (2.0f * Math.abs(mDeceleration)); 858 final float sign = Math.signum(mVelocity); 859 860 if (distance > mOver) { 861 // Default deceleration is not sufficient to slow us down before boundary 862 mDeceleration = - sign * velocitySquared / (2.0f * mOver); 863 distance = mOver; 864 } 865 866 mOver = (int) distance; 867 mState = BALLISTIC; 868 mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance); 869 mDuration = - (int) (1000.0f * mVelocity / mDeceleration); 870 } 871 continueWhenFinished()872 boolean continueWhenFinished() { 873 switch (mState) { 874 case SPLINE: 875 // Duration from start to null velocity 876 if (mDuration < mSplineDuration) { 877 // If the animation was clamped, we reached the edge 878 mCurrentPosition = mStart = mFinal; 879 // TODO Better compute speed when edge was reached 880 mVelocity = (int) mCurrVelocity; 881 mDeceleration = getDeceleration(mVelocity); 882 mStartTime += mDuration; 883 onEdgeReached(); 884 } else { 885 // Normal stop, no need to continue 886 return false; 887 } 888 break; 889 case BALLISTIC: 890 mStartTime += mDuration; 891 startSpringback(mFinal, mStart, 0); 892 break; 893 case CUBIC: 894 return false; 895 } 896 897 update(); 898 return true; 899 } 900 901 /* 902 * Update the current position and velocity for current time. Returns 903 * true if update has been done and false if animation duration has been 904 * reached. 905 */ update()906 boolean update() { 907 final long time = AnimationUtils.currentAnimationTimeMillis(); 908 final long currentTime = time - mStartTime; 909 910 if (currentTime == 0) { 911 // Skip work but report that we're still going if we have a nonzero duration. 912 return mDuration > 0; 913 } 914 if (currentTime > mDuration) { 915 return false; 916 } 917 918 double distance = 0.0; 919 switch (mState) { 920 case SPLINE: { 921 final float t = (float) currentTime / mSplineDuration; 922 final int index = (int) (NB_SAMPLES * t); 923 float distanceCoef = 1.f; 924 float velocityCoef = 0.f; 925 if (index < NB_SAMPLES) { 926 final float t_inf = (float) index / NB_SAMPLES; 927 final float t_sup = (float) (index + 1) / NB_SAMPLES; 928 final float d_inf = SPLINE_POSITION[index]; 929 final float d_sup = SPLINE_POSITION[index + 1]; 930 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); 931 distanceCoef = d_inf + (t - t_inf) * velocityCoef; 932 } 933 934 distance = distanceCoef * mSplineDistance; 935 mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; 936 break; 937 } 938 939 case BALLISTIC: { 940 final float t = currentTime / 1000.0f; 941 mCurrVelocity = mVelocity + mDeceleration * t; 942 distance = mVelocity * t + mDeceleration * t * t / 2.0f; 943 break; 944 } 945 946 case CUBIC: { 947 final float t = (float) (currentTime) / mDuration; 948 final float t2 = t * t; 949 final float sign = Math.signum(mVelocity); 950 distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2); 951 mCurrVelocity = sign * mOver * 6.0f * (- t + t2); 952 break; 953 } 954 } 955 956 mCurrentPosition = mStart + (int) Math.round(distance); 957 958 return true; 959 } 960 } 961 } 962