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