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 mScrollerX.updateScroll(q); 303 mScrollerY.updateScroll(q); 304 } else { 305 abortAnimation(); 306 } 307 break; 308 309 case FLING_MODE: 310 if (!mScrollerX.mFinished) { 311 if (!mScrollerX.update()) { 312 if (!mScrollerX.continueWhenFinished()) { 313 mScrollerX.finish(); 314 } 315 } 316 } 317 318 if (!mScrollerY.mFinished) { 319 if (!mScrollerY.update()) { 320 if (!mScrollerY.continueWhenFinished()) { 321 mScrollerY.finish(); 322 } 323 } 324 } 325 326 break; 327 } 328 329 return true; 330 } 331 332 /** 333 * Start scrolling by providing a starting point and the distance to travel. 334 * The scroll will use the default value of 250 milliseconds for the 335 * duration. 336 * 337 * @param startX Starting horizontal scroll offset in pixels. Positive 338 * numbers will scroll the content to the left. 339 * @param startY Starting vertical scroll offset in pixels. Positive numbers 340 * will scroll the content up. 341 * @param dx Horizontal distance to travel. Positive numbers will scroll the 342 * content to the left. 343 * @param dy Vertical distance to travel. Positive numbers will scroll the 344 * content up. 345 */ startScroll(int startX, int startY, int dx, int dy)346 public void startScroll(int startX, int startY, int dx, int dy) { 347 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 348 } 349 350 /** 351 * Start scrolling by providing a starting point and the distance to travel. 352 * 353 * @param startX Starting horizontal scroll offset in pixels. Positive 354 * numbers will scroll the content to the left. 355 * @param startY Starting vertical scroll offset in pixels. Positive numbers 356 * will scroll the content up. 357 * @param dx Horizontal distance to travel. Positive numbers will scroll the 358 * content to the left. 359 * @param dy Vertical distance to travel. Positive numbers will scroll the 360 * content up. 361 * @param duration Duration of the scroll in milliseconds. 362 */ startScroll(int startX, int startY, int dx, int dy, int duration)363 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 364 mMode = SCROLL_MODE; 365 mScrollerX.startScroll(startX, dx, duration); 366 mScrollerY.startScroll(startY, dy, duration); 367 } 368 369 /** 370 * Call this when you want to 'spring back' into a valid coordinate range. 371 * 372 * @param startX Starting X coordinate 373 * @param startY Starting Y coordinate 374 * @param minX Minimum valid X value 375 * @param maxX Maximum valid X value 376 * @param minY Minimum valid Y value 377 * @param maxY Minimum valid Y value 378 * @return true if a springback was initiated, false if startX and startY were 379 * already within the valid range. 380 */ springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)381 public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) { 382 mMode = FLING_MODE; 383 384 // Make sure both methods are called. 385 final boolean spingbackX = mScrollerX.springback(startX, minX, maxX); 386 final boolean spingbackY = mScrollerY.springback(startY, minY, maxY); 387 return spingbackX || spingbackY; 388 } 389 fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)390 public void fling(int startX, int startY, int velocityX, int velocityY, 391 int minX, int maxX, int minY, int maxY) { 392 fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); 393 } 394 395 /** 396 * Start scrolling based on a fling gesture. The distance traveled will 397 * depend on the initial velocity of the fling. 398 * 399 * @param startX Starting point of the scroll (X) 400 * @param startY Starting point of the scroll (Y) 401 * @param velocityX Initial velocity of the fling (X) measured in pixels per 402 * second. 403 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 404 * second 405 * @param minX Minimum X value. The scroller will not scroll past this point 406 * unless overX > 0. If overfling is allowed, it will use minX as 407 * a springback boundary. 408 * @param maxX Maximum X value. The scroller will not scroll past this point 409 * unless overX > 0. If overfling is allowed, it will use maxX as 410 * a springback boundary. 411 * @param minY Minimum Y value. The scroller will not scroll past this point 412 * unless overY > 0. If overfling is allowed, it will use minY as 413 * a springback boundary. 414 * @param maxY Maximum Y value. The scroller will not scroll past this point 415 * unless overY > 0. If overfling is allowed, it will use maxY as 416 * a springback boundary. 417 * @param overX Overfling range. If > 0, horizontal overfling in either 418 * direction will be possible. 419 * @param overY Overfling range. If > 0, vertical overfling in either 420 * direction will be possible. 421 */ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)422 public void fling(int startX, int startY, int velocityX, int velocityY, 423 int minX, int maxX, int minY, int maxY, int overX, int overY) { 424 // Continue a scroll or fling in progress 425 if (mFlywheel && !isFinished()) { 426 float oldVelocityX = mScrollerX.mCurrVelocity; 427 float oldVelocityY = mScrollerY.mCurrVelocity; 428 if (Math.signum(velocityX) == Math.signum(oldVelocityX) && 429 Math.signum(velocityY) == Math.signum(oldVelocityY)) { 430 velocityX += oldVelocityX; 431 velocityY += oldVelocityY; 432 } 433 } 434 435 mMode = FLING_MODE; 436 mScrollerX.fling(startX, velocityX, minX, maxX, overX); 437 mScrollerY.fling(startY, velocityY, minY, maxY, overY); 438 } 439 440 /** 441 * Notify the scroller that we've reached a horizontal boundary. 442 * Normally the information to handle this will already be known 443 * when the animation is started, such as in a call to one of the 444 * fling functions. However there are cases where this cannot be known 445 * in advance. This function will transition the current motion and 446 * animate from startX to finalX as appropriate. 447 * 448 * @param startX Starting/current X position 449 * @param finalX Desired final X position 450 * @param overX Magnitude of overscroll allowed. This should be the maximum 451 * desired distance from finalX. Absolute value - must be positive. 452 */ notifyHorizontalEdgeReached(int startX, int finalX, int overX)453 public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { 454 mScrollerX.notifyEdgeReached(startX, finalX, overX); 455 } 456 457 /** 458 * Notify the scroller that we've reached a vertical boundary. 459 * Normally the information to handle this will already be known 460 * when the animation is started, such as in a call to one of the 461 * fling functions. However there are cases where this cannot be known 462 * in advance. This function will animate a parabolic motion from 463 * startY to finalY. 464 * 465 * @param startY Starting/current Y position 466 * @param finalY Desired final Y position 467 * @param overY Magnitude of overscroll allowed. This should be the maximum 468 * desired distance from finalY. Absolute value - must be positive. 469 */ notifyVerticalEdgeReached(int startY, int finalY, int overY)470 public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { 471 mScrollerY.notifyEdgeReached(startY, finalY, overY); 472 } 473 474 /** 475 * Returns whether the current Scroller is currently returning to a valid position. 476 * Valid bounds were provided by the 477 * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. 478 * 479 * One should check this value before calling 480 * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress 481 * to restore a valid position will then be stopped. The caller has to take into account 482 * the fact that the started scroll will start from an overscrolled position. 483 * 484 * @return true when the current position is overscrolled and in the process of 485 * interpolating back to a valid value. 486 */ isOverScrolled()487 public boolean isOverScrolled() { 488 return ((!mScrollerX.mFinished && 489 mScrollerX.mState != SplineOverScroller.SPLINE) || 490 (!mScrollerY.mFinished && 491 mScrollerY.mState != SplineOverScroller.SPLINE)); 492 } 493 494 /** 495 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 496 * aborting the animating causes the scroller to move to the final x and y 497 * positions. 498 * 499 * @see #forceFinished(boolean) 500 */ abortAnimation()501 public void abortAnimation() { 502 mScrollerX.finish(); 503 mScrollerY.finish(); 504 } 505 506 /** 507 * Returns the time elapsed since the beginning of the scrolling. 508 * 509 * @return The elapsed time in milliseconds. 510 * 511 * @hide 512 */ timePassed()513 public int timePassed() { 514 final long time = AnimationUtils.currentAnimationTimeMillis(); 515 final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime); 516 return (int) (time - startTime); 517 } 518 519 /** 520 * @hide 521 */ 522 @UnsupportedAppUsage isScrollingInDirection(float xvel, float yvel)523 public boolean isScrollingInDirection(float xvel, float yvel) { 524 final int dx = mScrollerX.mFinal - mScrollerX.mStart; 525 final int dy = mScrollerY.mFinal - mScrollerY.mStart; 526 return !isFinished() && Math.signum(xvel) == Math.signum(dx) && 527 Math.signum(yvel) == Math.signum(dy); 528 } 529 530 static class SplineOverScroller { 531 // Initial position 532 private int mStart; 533 534 // Current position 535 private int mCurrentPosition; 536 537 // Final position 538 private int mFinal; 539 540 // Initial velocity 541 private int mVelocity; 542 543 // Current velocity 544 @UnsupportedAppUsage 545 private float mCurrVelocity; 546 547 // Constant current deceleration 548 private float mDeceleration; 549 550 // Animation starting time, in system milliseconds 551 private long mStartTime; 552 553 // Animation duration, in milliseconds 554 private int mDuration; 555 556 // Duration to complete spline component of animation 557 private int mSplineDuration; 558 559 // Distance to travel along spline animation 560 private int mSplineDistance; 561 562 // Whether the animation is currently in progress 563 private boolean mFinished; 564 565 // The allowed overshot distance before boundary is reached. 566 private int mOver; 567 568 // Fling friction 569 private float mFlingFriction = ViewConfiguration.getScrollFriction(); 570 571 // Current state of the animation. 572 private int mState = SPLINE; 573 574 // Constant gravity value, used in the deceleration phase. 575 private static final float GRAVITY = 2000.0f; 576 577 // A context-specific coefficient adjusted to physical values. 578 private float mPhysicalCoeff; 579 580 private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); 581 private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) 582 private static final float START_TENSION = 0.5f; 583 private static final float END_TENSION = 1.0f; 584 private static final float P1 = START_TENSION * INFLEXION; 585 private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); 586 587 private static final int NB_SAMPLES = 100; 588 private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; 589 private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; 590 591 private static final int SPLINE = 0; 592 private static final int CUBIC = 1; 593 private static final int BALLISTIC = 2; 594 595 static { 596 float x_min = 0.0f; 597 float y_min = 0.0f; 598 for (int i = 0; i < NB_SAMPLES; i++) { 599 final float alpha = (float) i / NB_SAMPLES; 600 601 float x_max = 1.0f; 602 float x, tx, coef; 603 while (true) { 604 x = x_min + (x_max - x_min) / 2.0f; 605 coef = 3.0f * x * (1.0f - x); 606 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; 607 if (Math.abs(tx - alpha) < 1E-5) break; 608 if (tx > alpha) x_max = x; 609 else x_min = x; 610 } 611 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; 612 613 float y_max = 1.0f; 614 float y, dy; 615 while (true) { 616 y = y_min + (y_max - y_min) / 2.0f; 617 coef = 3.0f * y * (1.0f - y); 618 dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; 619 if (Math.abs(dy - alpha) < 1E-5) break; 620 if (dy > alpha) y_max = y; 621 else y_min = y; 622 } 623 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; 624 } 625 SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; 626 } 627 setFriction(float friction)628 void setFriction(float friction) { 629 mFlingFriction = friction; 630 } 631 SplineOverScroller(Context context)632 SplineOverScroller(Context context) { 633 mFinished = true; 634 final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; 635 mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2) 636 * 39.37f // inch/meter 637 * ppi 638 * 0.84f; // look and feel tuning 639 } 640 updateScroll(float q)641 void updateScroll(float q) { 642 mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); 643 } 644 645 /* 646 * Get a signed deceleration that will reduce the velocity. 647 */ getDeceleration(int velocity)648 static private float getDeceleration(int velocity) { 649 return velocity > 0 ? -GRAVITY : GRAVITY; 650 } 651 652 /* 653 * Modifies mDuration to the duration it takes to get from start to newFinal using the 654 * spline interpolation. The previous duration was needed to get to oldFinal. 655 */ adjustDuration(int start, int oldFinal, int newFinal)656 private void adjustDuration(int start, int oldFinal, int newFinal) { 657 final int oldDistance = oldFinal - start; 658 final int newDistance = newFinal - start; 659 final float x = Math.abs((float) newDistance / oldDistance); 660 final int index = (int) (NB_SAMPLES * x); 661 if (index < NB_SAMPLES) { 662 final float x_inf = (float) index / NB_SAMPLES; 663 final float x_sup = (float) (index + 1) / NB_SAMPLES; 664 final float t_inf = SPLINE_TIME[index]; 665 final float t_sup = SPLINE_TIME[index + 1]; 666 final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf); 667 mDuration *= timeCoef; 668 } 669 } 670 startScroll(int start, int distance, int duration)671 void startScroll(int start, int distance, int duration) { 672 mFinished = false; 673 674 mCurrentPosition = mStart = start; 675 mFinal = start + distance; 676 677 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 678 mDuration = duration; 679 680 // Unused 681 mDeceleration = 0.0f; 682 mVelocity = 0; 683 } 684 finish()685 void finish() { 686 mCurrentPosition = mFinal; 687 // Not reset since WebView relies on this value for fast fling. 688 // TODO: restore when WebView uses the fast fling implemented in this class. 689 // mCurrVelocity = 0.0f; 690 mFinished = true; 691 } 692 setFinalPosition(int position)693 void setFinalPosition(int position) { 694 mFinal = position; 695 mSplineDistance = mFinal - mStart; 696 mFinished = false; 697 } 698 extendDuration(int extend)699 void extendDuration(int extend) { 700 final long time = AnimationUtils.currentAnimationTimeMillis(); 701 final int elapsedTime = (int) (time - mStartTime); 702 mDuration = mSplineDuration = elapsedTime + extend; 703 mFinished = false; 704 } 705 springback(int start, int min, int max)706 boolean springback(int start, int min, int max) { 707 mFinished = true; 708 709 mCurrentPosition = mStart = mFinal = start; 710 mVelocity = 0; 711 712 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 713 mDuration = 0; 714 715 if (start < min) { 716 startSpringback(start, min, 0); 717 } else if (start > max) { 718 startSpringback(start, max, 0); 719 } 720 721 return !mFinished; 722 } 723 startSpringback(int start, int end, int velocity)724 private void startSpringback(int start, int end, int velocity) { 725 // mStartTime has been set 726 mFinished = false; 727 mState = CUBIC; 728 mCurrentPosition = mStart = start; 729 mFinal = end; 730 final int delta = start - end; 731 mDeceleration = getDeceleration(delta); 732 // TODO take velocity into account 733 mVelocity = -delta; // only sign is used 734 mOver = Math.abs(delta); 735 mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration)); 736 } 737 fling(int start, int velocity, int min, int max, int over)738 void fling(int start, int velocity, int min, int max, int over) { 739 mOver = over; 740 mFinished = false; 741 mCurrVelocity = mVelocity = velocity; 742 mDuration = mSplineDuration = 0; 743 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 744 mCurrentPosition = mStart = start; 745 746 if (start > max || start < min) { 747 startAfterEdge(start, min, max, velocity); 748 return; 749 } 750 751 mState = SPLINE; 752 double totalDistance = 0.0; 753 754 if (velocity != 0) { 755 mDuration = mSplineDuration = getSplineFlingDuration(velocity); 756 totalDistance = getSplineFlingDistance(velocity); 757 } 758 759 mSplineDistance = (int) (totalDistance * Math.signum(velocity)); 760 mFinal = start + mSplineDistance; 761 762 // Clamp to a valid final position 763 if (mFinal < min) { 764 adjustDuration(mStart, mFinal, min); 765 mFinal = min; 766 } 767 768 if (mFinal > max) { 769 adjustDuration(mStart, mFinal, max); 770 mFinal = max; 771 } 772 } 773 getSplineDeceleration(int velocity)774 private double getSplineDeceleration(int velocity) { 775 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); 776 } 777 getSplineFlingDistance(int velocity)778 private double getSplineFlingDistance(int velocity) { 779 final double l = getSplineDeceleration(velocity); 780 final double decelMinusOne = DECELERATION_RATE - 1.0; 781 return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); 782 } 783 784 /* Returns the duration, expressed in milliseconds */ getSplineFlingDuration(int velocity)785 private int getSplineFlingDuration(int velocity) { 786 final double l = getSplineDeceleration(velocity); 787 final double decelMinusOne = DECELERATION_RATE - 1.0; 788 return (int) (1000.0 * Math.exp(l / decelMinusOne)); 789 } 790 fitOnBounceCurve(int start, int end, int velocity)791 private void fitOnBounceCurve(int start, int end, int velocity) { 792 // Simulate a bounce that started from edge 793 final float durationToApex = - velocity / mDeceleration; 794 // The float cast below is necessary to avoid integer overflow. 795 final float velocitySquared = (float) velocity * velocity; 796 final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration); 797 final float distanceToEdge = Math.abs(end - start); 798 final float totalDuration = (float) Math.sqrt( 799 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration)); 800 mStartTime -= (int) (1000.0f * (totalDuration - durationToApex)); 801 mCurrentPosition = mStart = end; 802 mVelocity = (int) (- mDeceleration * totalDuration); 803 } 804 startBounceAfterEdge(int start, int end, int velocity)805 private void startBounceAfterEdge(int start, int end, int velocity) { 806 mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity); 807 fitOnBounceCurve(start, end, velocity); 808 onEdgeReached(); 809 } 810 startAfterEdge(int start, int min, int max, int velocity)811 private void startAfterEdge(int start, int min, int max, int velocity) { 812 if (start > min && start < max) { 813 Log.e("OverScroller", "startAfterEdge called from a valid position"); 814 mFinished = true; 815 return; 816 } 817 final boolean positive = start > max; 818 final int edge = positive ? max : min; 819 final int overDistance = start - edge; 820 boolean keepIncreasing = overDistance * velocity >= 0; 821 if (keepIncreasing) { 822 // Will result in a bounce or a to_boundary depending on velocity. 823 startBounceAfterEdge(start, edge, velocity); 824 } else { 825 final double totalDistance = getSplineFlingDistance(velocity); 826 if (totalDistance > Math.abs(overDistance)) { 827 fling(start, velocity, positive ? min : start, positive ? start : max, mOver); 828 } else { 829 startSpringback(start, edge, velocity); 830 } 831 } 832 } 833 notifyEdgeReached(int start, int end, int over)834 void notifyEdgeReached(int start, int end, int over) { 835 // mState is used to detect successive notifications 836 if (mState == SPLINE) { 837 mOver = over; 838 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 839 // We were in fling/scroll mode before: current velocity is such that distance to 840 // edge is increasing. This ensures that startAfterEdge will not start a new fling. 841 startAfterEdge(start, end, end, (int) mCurrVelocity); 842 } 843 } 844 onEdgeReached()845 private void onEdgeReached() { 846 // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. 847 // The float cast below is necessary to avoid integer overflow. 848 final float velocitySquared = (float) mVelocity * mVelocity; 849 float distance = velocitySquared / (2.0f * Math.abs(mDeceleration)); 850 final float sign = Math.signum(mVelocity); 851 852 if (distance > mOver) { 853 // Default deceleration is not sufficient to slow us down before boundary 854 mDeceleration = - sign * velocitySquared / (2.0f * mOver); 855 distance = mOver; 856 } 857 858 mOver = (int) distance; 859 mState = BALLISTIC; 860 mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance); 861 mDuration = - (int) (1000.0f * mVelocity / mDeceleration); 862 } 863 continueWhenFinished()864 boolean continueWhenFinished() { 865 switch (mState) { 866 case SPLINE: 867 // Duration from start to null velocity 868 if (mDuration < mSplineDuration) { 869 // If the animation was clamped, we reached the edge 870 mCurrentPosition = mStart = mFinal; 871 // TODO Better compute speed when edge was reached 872 mVelocity = (int) mCurrVelocity; 873 mDeceleration = getDeceleration(mVelocity); 874 mStartTime += mDuration; 875 onEdgeReached(); 876 } else { 877 // Normal stop, no need to continue 878 return false; 879 } 880 break; 881 case BALLISTIC: 882 mStartTime += mDuration; 883 startSpringback(mFinal, mStart, 0); 884 break; 885 case CUBIC: 886 return false; 887 } 888 889 update(); 890 return true; 891 } 892 893 /* 894 * Update the current position and velocity for current time. Returns 895 * true if update has been done and false if animation duration has been 896 * reached. 897 */ update()898 boolean update() { 899 final long time = AnimationUtils.currentAnimationTimeMillis(); 900 final long currentTime = time - mStartTime; 901 902 if (currentTime == 0) { 903 // Skip work but report that we're still going if we have a nonzero duration. 904 return mDuration > 0; 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