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 mInterpolator = interpolator; 74 mFlywheel = flywheel; 75 mScrollerX = new SplineOverScroller(context); 76 mScrollerY = new SplineOverScroller(context); 77 } 78 79 /** 80 * Creates an OverScroller with flywheel enabled. 81 * @param context The context of this application. 82 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 83 * be used. 84 * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the 85 * velocity which is preserved in the bounce when the horizontal edge is reached. A null value 86 * means no bounce. This behavior is no longer supported and this coefficient has no effect. 87 * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This 88 * behavior is no longer supported and this coefficient has no effect. 89 * !deprecated Use {!link #OverScroller(Context, Interpolator, boolean)} instead. 90 */ OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY)91 public OverScroller(Context context, Interpolator interpolator, 92 float bounceCoefficientX, float bounceCoefficientY) { 93 this(context, interpolator, true); 94 } 95 96 /** 97 * Creates an OverScroller. 98 * @param context The context of this application. 99 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 100 * be used. 101 * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the 102 * velocity which is preserved in the bounce when the horizontal edge is reached. A null value 103 * means no bounce. This behavior is no longer supported and this coefficient has no effect. 104 * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This 105 * behavior is no longer supported and this coefficient has no effect. 106 * @param flywheel If true, successive fling motions will keep on increasing scroll speed. 107 * !deprecated Use {!link OverScroller(Context, Interpolator, boolean)} instead. 108 */ OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY, boolean flywheel)109 public OverScroller(Context context, Interpolator interpolator, 110 float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) { 111 this(context, interpolator, flywheel); 112 } 113 setInterpolator(Interpolator interpolator)114 void setInterpolator(Interpolator interpolator) { 115 mInterpolator = interpolator; 116 } 117 118 /** 119 * The amount of friction applied to flings. The default value 120 * is {@link ViewConfiguration#getScrollFriction}. 121 * 122 * @param friction A scalar dimension-less value representing the coefficient of 123 * friction. 124 */ setFriction(float friction)125 public final void setFriction(float friction) { 126 mScrollerX.setFriction(friction); 127 mScrollerY.setFriction(friction); 128 } 129 130 /** 131 * 132 * Returns whether the scroller has finished scrolling. 133 * 134 * @return True if the scroller has finished scrolling, false otherwise. 135 */ isFinished()136 public final boolean isFinished() { 137 return mScrollerX.mFinished && mScrollerY.mFinished; 138 } 139 140 /** 141 * Force the finished field to a particular value. Contrary to 142 * {@link #abortAnimation()}, forcing the animation to finished 143 * does NOT cause the scroller to move to the final x and y 144 * position. 145 * 146 * @param finished The new finished value. 147 */ forceFinished(boolean finished)148 public final void forceFinished(boolean finished) { 149 mScrollerX.mFinished = mScrollerY.mFinished = finished; 150 } 151 152 /** 153 * Returns the current X offset in the scroll. 154 * 155 * @return The new X offset as an absolute distance from the origin. 156 */ getCurrX()157 public final int getCurrX() { 158 return mScrollerX.mCurrentPosition; 159 } 160 161 /** 162 * Returns the current Y offset in the scroll. 163 * 164 * @return The new Y offset as an absolute distance from the origin. 165 */ getCurrY()166 public final int getCurrY() { 167 return mScrollerY.mCurrentPosition; 168 } 169 170 /** 171 * Returns the absolute value of the current velocity. 172 * 173 * @return The original velocity less the deceleration, norm of the X and Y velocity vector. 174 */ getCurrVelocity()175 public float getCurrVelocity() { 176 float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity; 177 squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity; 178 return FloatMath.sqrt(squaredNorm); 179 } 180 181 /** 182 * Returns the start X offset in the scroll. 183 * 184 * @return The start X offset as an absolute distance from the origin. 185 */ getStartX()186 public final int getStartX() { 187 return mScrollerX.mStart; 188 } 189 190 /** 191 * Returns the start Y offset in the scroll. 192 * 193 * @return The start Y offset as an absolute distance from the origin. 194 */ getStartY()195 public final int getStartY() { 196 return mScrollerY.mStart; 197 } 198 199 /** 200 * Returns where the scroll will end. Valid only for "fling" scrolls. 201 * 202 * @return The final X offset as an absolute distance from the origin. 203 */ getFinalX()204 public final int getFinalX() { 205 return mScrollerX.mFinal; 206 } 207 208 /** 209 * Returns where the scroll will end. Valid only for "fling" scrolls. 210 * 211 * @return The final Y offset as an absolute distance from the origin. 212 */ getFinalY()213 public final int getFinalY() { 214 return mScrollerY.mFinal; 215 } 216 217 /** 218 * Returns how long the scroll event will take, in milliseconds. 219 * 220 * @return The duration of the scroll in milliseconds. 221 * 222 * @hide Pending removal once nothing depends on it 223 * @deprecated OverScrollers don't necessarily have a fixed duration. 224 * This function will lie to the best of its ability. 225 */ 226 @Deprecated getDuration()227 public final int getDuration() { 228 return Math.max(mScrollerX.mDuration, mScrollerY.mDuration); 229 } 230 231 /** 232 * Extend the scroll animation. This allows a running animation to scroll 233 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 234 * 235 * @param extend Additional time to scroll in milliseconds. 236 * @see #setFinalX(int) 237 * @see #setFinalY(int) 238 * 239 * @hide Pending removal once nothing depends on it 240 * @deprecated OverScrollers don't necessarily have a fixed duration. 241 * Instead of setting a new final position and extending 242 * the duration of an existing scroll, use startScroll 243 * to begin a new animation. 244 */ 245 @Deprecated extendDuration(int extend)246 public void extendDuration(int extend) { 247 mScrollerX.extendDuration(extend); 248 mScrollerY.extendDuration(extend); 249 } 250 251 /** 252 * Sets the final position (X) for this scroller. 253 * 254 * @param newX The new X offset as an absolute distance from the origin. 255 * @see #extendDuration(int) 256 * @see #setFinalY(int) 257 * 258 * @hide Pending removal once nothing depends on it 259 * @deprecated OverScroller's final position may change during an animation. 260 * Instead of setting a new final position and extending 261 * the duration of an existing scroll, use startScroll 262 * to begin a new animation. 263 */ 264 @Deprecated setFinalX(int newX)265 public void setFinalX(int newX) { 266 mScrollerX.setFinalPosition(newX); 267 } 268 269 /** 270 * Sets the final position (Y) for this scroller. 271 * 272 * @param newY The new Y offset as an absolute distance from the origin. 273 * @see #extendDuration(int) 274 * @see #setFinalX(int) 275 * 276 * @hide Pending removal once nothing depends on it 277 * @deprecated OverScroller's final position may change during an animation. 278 * Instead of setting a new final position and extending 279 * the duration of an existing scroll, use startScroll 280 * to begin a new animation. 281 */ 282 @Deprecated setFinalY(int newY)283 public void setFinalY(int newY) { 284 mScrollerY.setFinalPosition(newY); 285 } 286 287 /** 288 * Call this when you want to know the new location. If it returns true, the 289 * animation is not yet finished. 290 */ computeScrollOffset()291 public boolean computeScrollOffset() { 292 if (isFinished()) { 293 return false; 294 } 295 296 switch (mMode) { 297 case SCROLL_MODE: 298 long time = AnimationUtils.currentAnimationTimeMillis(); 299 // Any scroller can be used for time, since they were started 300 // together in scroll mode. We use X here. 301 final long elapsedTime = time - mScrollerX.mStartTime; 302 303 final int duration = mScrollerX.mDuration; 304 if (elapsedTime < duration) { 305 float q = (float) (elapsedTime) / duration; 306 307 if (mInterpolator == null) { 308 q = Scroller.viscousFluid(q); 309 } else { 310 q = mInterpolator.getInterpolation(q); 311 } 312 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 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 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 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 final float distanceToApex = velocity * velocity / 2.0f / Math.abs(mDeceleration); 803 final float distanceToEdge = Math.abs(end - start); 804 final float totalDuration = (float) Math.sqrt( 805 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration)); 806 mStartTime -= (int) (1000.0f * (totalDuration - durationToApex)); 807 mStart = end; 808 mVelocity = (int) (- mDeceleration * totalDuration); 809 } 810 startBounceAfterEdge(int start, int end, int velocity)811 private void startBounceAfterEdge(int start, int end, int velocity) { 812 mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity); 813 fitOnBounceCurve(start, end, velocity); 814 onEdgeReached(); 815 } 816 startAfterEdge(int start, int min, int max, int velocity)817 private void startAfterEdge(int start, int min, int max, int velocity) { 818 if (start > min && start < max) { 819 Log.e("OverScroller", "startAfterEdge called from a valid position"); 820 mFinished = true; 821 return; 822 } 823 final boolean positive = start > max; 824 final int edge = positive ? max : min; 825 final int overDistance = start - edge; 826 boolean keepIncreasing = overDistance * velocity >= 0; 827 if (keepIncreasing) { 828 // Will result in a bounce or a to_boundary depending on velocity. 829 startBounceAfterEdge(start, edge, velocity); 830 } else { 831 final double totalDistance = getSplineFlingDistance(velocity); 832 if (totalDistance > Math.abs(overDistance)) { 833 fling(start, velocity, positive ? min : start, positive ? start : max, mOver); 834 } else { 835 startSpringback(start, edge, velocity); 836 } 837 } 838 } 839 notifyEdgeReached(int start, int end, int over)840 void notifyEdgeReached(int start, int end, int over) { 841 // mState is used to detect successive notifications 842 if (mState == SPLINE) { 843 mOver = over; 844 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 845 // We were in fling/scroll mode before: current velocity is such that distance to 846 // edge is increasing. This ensures that startAfterEdge will not start a new fling. 847 startAfterEdge(start, end, end, (int) mCurrVelocity); 848 } 849 } 850 onEdgeReached()851 private void onEdgeReached() { 852 // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. 853 float distance = mVelocity * mVelocity / (2.0f * Math.abs(mDeceleration)); 854 final float sign = Math.signum(mVelocity); 855 856 if (distance > mOver) { 857 // Default deceleration is not sufficient to slow us down before boundary 858 mDeceleration = - sign * mVelocity * mVelocity / (2.0f * mOver); 859 distance = mOver; 860 } 861 862 mOver = (int) distance; 863 mState = BALLISTIC; 864 mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance); 865 mDuration = - (int) (1000.0f * mVelocity / mDeceleration); 866 } 867 continueWhenFinished()868 boolean continueWhenFinished() { 869 switch (mState) { 870 case SPLINE: 871 // Duration from start to null velocity 872 if (mDuration < mSplineDuration) { 873 // If the animation was clamped, we reached the edge 874 mStart = mFinal; 875 // TODO Better compute speed when edge was reached 876 mVelocity = (int) mCurrVelocity; 877 mDeceleration = getDeceleration(mVelocity); 878 mStartTime += mDuration; 879 onEdgeReached(); 880 } else { 881 // Normal stop, no need to continue 882 return false; 883 } 884 break; 885 case BALLISTIC: 886 mStartTime += mDuration; 887 startSpringback(mFinal, mStart, 0); 888 break; 889 case CUBIC: 890 return false; 891 } 892 893 update(); 894 return true; 895 } 896 897 /* 898 * Update the current position and velocity for current time. Returns 899 * true if update has been done and false if animation duration has been 900 * reached. 901 */ update()902 boolean update() { 903 final long time = AnimationUtils.currentAnimationTimeMillis(); 904 final long currentTime = time - mStartTime; 905 906 if (currentTime > mDuration) { 907 return false; 908 } 909 910 double distance = 0.0; 911 switch (mState) { 912 case SPLINE: { 913 final float t = (float) currentTime / mSplineDuration; 914 final int index = (int) (NB_SAMPLES * t); 915 float distanceCoef = 1.f; 916 float velocityCoef = 0.f; 917 if (index < NB_SAMPLES) { 918 final float t_inf = (float) index / NB_SAMPLES; 919 final float t_sup = (float) (index + 1) / NB_SAMPLES; 920 final float d_inf = SPLINE_POSITION[index]; 921 final float d_sup = SPLINE_POSITION[index + 1]; 922 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); 923 distanceCoef = d_inf + (t - t_inf) * velocityCoef; 924 } 925 926 distance = distanceCoef * mSplineDistance; 927 mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; 928 break; 929 } 930 931 case BALLISTIC: { 932 final float t = currentTime / 1000.0f; 933 mCurrVelocity = mVelocity + mDeceleration * t; 934 distance = mVelocity * t + mDeceleration * t * t / 2.0f; 935 break; 936 } 937 938 case CUBIC: { 939 final float t = (float) (currentTime) / mDuration; 940 final float t2 = t * t; 941 final float sign = Math.signum(mVelocity); 942 distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2); 943 mCurrVelocity = sign * mOver * 6.0f * (- t + t2); 944 break; 945 } 946 } 947 948 mCurrentPosition = mStart + (int) Math.round(distance); 949 950 return true; 951 } 952 } 953 } 954