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