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