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 getSplineFlingDistance(int velocity)530 double getSplineFlingDistance(int velocity) { 531 return mScrollerY.getSplineFlingDistance(velocity); 532 } 533 534 static class SplineOverScroller { 535 // Initial position 536 private int mStart; 537 538 // Current position 539 private int mCurrentPosition; 540 541 // Final position 542 private int mFinal; 543 544 // Initial velocity 545 private int mVelocity; 546 547 // Current velocity 548 @UnsupportedAppUsage 549 private float mCurrVelocity; 550 551 // Constant current deceleration 552 private float mDeceleration; 553 554 // Animation starting time, in system milliseconds 555 private long mStartTime; 556 557 // Animation duration, in milliseconds 558 private int mDuration; 559 560 // Duration to complete spline component of animation 561 private int mSplineDuration; 562 563 // Distance to travel along spline animation 564 private int mSplineDistance; 565 566 // Whether the animation is currently in progress 567 private boolean mFinished; 568 569 // The allowed overshot distance before boundary is reached. 570 private int mOver; 571 572 // Fling friction 573 private float mFlingFriction = ViewConfiguration.getScrollFriction(); 574 575 // Current state of the animation. 576 private int mState = SPLINE; 577 578 // Constant gravity value, used in the deceleration phase. 579 private static final float GRAVITY = 2000.0f; 580 581 // A context-specific coefficient adjusted to physical values. 582 private float mPhysicalCoeff; 583 584 private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); 585 private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) 586 private static final float START_TENSION = 0.5f; 587 private static final float END_TENSION = 1.0f; 588 private static final float P1 = START_TENSION * INFLEXION; 589 private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); 590 591 private static final int NB_SAMPLES = 100; 592 private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; 593 private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; 594 595 private static final int SPLINE = 0; 596 private static final int CUBIC = 1; 597 private static final int BALLISTIC = 2; 598 599 static { 600 float x_min = 0.0f; 601 float y_min = 0.0f; 602 for (int i = 0; i < NB_SAMPLES; i++) { 603 final float alpha = (float) i / NB_SAMPLES; 604 605 float x_max = 1.0f; 606 float x, tx, coef; 607 while (true) { 608 x = x_min + (x_max - x_min) / 2.0f; 609 coef = 3.0f * x * (1.0f - x); 610 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; 611 if (Math.abs(tx - alpha) < 1E-5) break; 612 if (tx > alpha) x_max = x; 613 else x_min = x; 614 } 615 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; 616 617 float y_max = 1.0f; 618 float y, dy; 619 while (true) { 620 y = y_min + (y_max - y_min) / 2.0f; 621 coef = 3.0f * y * (1.0f - y); 622 dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; 623 if (Math.abs(dy - alpha) < 1E-5) break; 624 if (dy > alpha) y_max = y; 625 else y_min = y; 626 } 627 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; 628 } 629 SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; 630 } 631 setFriction(float friction)632 void setFriction(float friction) { 633 mFlingFriction = friction; 634 } 635 SplineOverScroller(Context context)636 SplineOverScroller(Context context) { 637 mFinished = true; 638 final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; 639 mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2) 640 * 39.37f // inch/meter 641 * ppi 642 * 0.84f; // look and feel tuning 643 } 644 updateScroll(float q)645 void updateScroll(float q) { 646 mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); 647 } 648 649 /* 650 * Get a signed deceleration that will reduce the velocity. 651 */ getDeceleration(int velocity)652 static private float getDeceleration(int velocity) { 653 return velocity > 0 ? -GRAVITY : GRAVITY; 654 } 655 656 /* 657 * Modifies mDuration to the duration it takes to get from start to newFinal using the 658 * spline interpolation. The previous duration was needed to get to oldFinal. 659 */ adjustDuration(int start, int oldFinal, int newFinal)660 private void adjustDuration(int start, int oldFinal, int newFinal) { 661 final int oldDistance = oldFinal - start; 662 final int newDistance = newFinal - start; 663 final float x = Math.abs((float) newDistance / oldDistance); 664 final int index = (int) (NB_SAMPLES * x); 665 if (index < NB_SAMPLES) { 666 final float x_inf = (float) index / NB_SAMPLES; 667 final float x_sup = (float) (index + 1) / NB_SAMPLES; 668 final float t_inf = SPLINE_TIME[index]; 669 final float t_sup = SPLINE_TIME[index + 1]; 670 final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf); 671 mDuration *= timeCoef; 672 } 673 } 674 startScroll(int start, int distance, int duration)675 void startScroll(int start, int distance, int duration) { 676 mFinished = false; 677 678 mCurrentPosition = mStart = start; 679 mFinal = start + distance; 680 681 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 682 mDuration = duration; 683 684 // Unused 685 mDeceleration = 0.0f; 686 mVelocity = 0; 687 } 688 finish()689 void finish() { 690 mCurrentPosition = mFinal; 691 // Not reset since WebView relies on this value for fast fling. 692 // TODO: restore when WebView uses the fast fling implemented in this class. 693 // mCurrVelocity = 0.0f; 694 mFinished = true; 695 } 696 setFinalPosition(int position)697 void setFinalPosition(int position) { 698 mFinal = position; 699 mSplineDistance = mFinal - mStart; 700 mFinished = false; 701 } 702 extendDuration(int extend)703 void extendDuration(int extend) { 704 final long time = AnimationUtils.currentAnimationTimeMillis(); 705 final int elapsedTime = (int) (time - mStartTime); 706 mDuration = mSplineDuration = elapsedTime + extend; 707 mFinished = false; 708 } 709 springback(int start, int min, int max)710 boolean springback(int start, int min, int max) { 711 mFinished = true; 712 713 mCurrentPosition = mStart = mFinal = start; 714 mVelocity = 0; 715 716 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 717 mDuration = 0; 718 719 if (start < min) { 720 startSpringback(start, min, 0); 721 } else if (start > max) { 722 startSpringback(start, max, 0); 723 } 724 725 return !mFinished; 726 } 727 startSpringback(int start, int end, int velocity)728 private void startSpringback(int start, int end, int velocity) { 729 // mStartTime has been set 730 mFinished = false; 731 mState = CUBIC; 732 mCurrentPosition = mStart = start; 733 mFinal = end; 734 final int delta = start - end; 735 mDeceleration = getDeceleration(delta); 736 // TODO take velocity into account 737 mVelocity = -delta; // only sign is used 738 mOver = Math.abs(delta); 739 mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration)); 740 } 741 fling(int start, int velocity, int min, int max, int over)742 void fling(int start, int velocity, int min, int max, int over) { 743 mOver = over; 744 mFinished = false; 745 mCurrVelocity = mVelocity = velocity; 746 mDuration = mSplineDuration = 0; 747 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 748 mCurrentPosition = mStart = start; 749 750 if (start > max || start < min) { 751 startAfterEdge(start, min, max, velocity); 752 return; 753 } 754 755 mState = SPLINE; 756 double totalDistance = 0.0; 757 758 if (velocity != 0) { 759 mDuration = mSplineDuration = getSplineFlingDuration(velocity); 760 totalDistance = getSplineFlingDistance(velocity); 761 } 762 763 mSplineDistance = (int) (totalDistance * Math.signum(velocity)); 764 mFinal = start + mSplineDistance; 765 766 // Clamp to a valid final position 767 if (mFinal < min) { 768 adjustDuration(mStart, mFinal, min); 769 mFinal = min; 770 } 771 772 if (mFinal > max) { 773 adjustDuration(mStart, mFinal, max); 774 mFinal = max; 775 } 776 } 777 getSplineDeceleration(int velocity)778 private double getSplineDeceleration(int velocity) { 779 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); 780 } 781 getSplineFlingDistance(int velocity)782 private double getSplineFlingDistance(int velocity) { 783 final double l = getSplineDeceleration(velocity); 784 final double decelMinusOne = DECELERATION_RATE - 1.0; 785 return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); 786 } 787 788 /* Returns the duration, expressed in milliseconds */ getSplineFlingDuration(int velocity)789 private int getSplineFlingDuration(int velocity) { 790 final double l = getSplineDeceleration(velocity); 791 final double decelMinusOne = DECELERATION_RATE - 1.0; 792 return (int) (1000.0 * Math.exp(l / decelMinusOne)); 793 } 794 fitOnBounceCurve(int start, int end, int velocity)795 private void fitOnBounceCurve(int start, int end, int velocity) { 796 // Simulate a bounce that started from edge 797 final float durationToApex = - velocity / mDeceleration; 798 // The float cast below is necessary to avoid integer overflow. 799 final float velocitySquared = (float) velocity * velocity; 800 final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration); 801 final float distanceToEdge = Math.abs(end - start); 802 final float totalDuration = (float) Math.sqrt( 803 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration)); 804 mStartTime -= (int) (1000.0f * (totalDuration - durationToApex)); 805 mCurrentPosition = mStart = end; 806 mVelocity = (int) (- mDeceleration * totalDuration); 807 } 808 startBounceAfterEdge(int start, int end, int velocity)809 private void startBounceAfterEdge(int start, int end, int velocity) { 810 mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity); 811 fitOnBounceCurve(start, end, velocity); 812 onEdgeReached(); 813 } 814 startAfterEdge(int start, int min, int max, int velocity)815 private void startAfterEdge(int start, int min, int max, int velocity) { 816 if (start > min && start < max) { 817 Log.e("OverScroller", "startAfterEdge called from a valid position"); 818 mFinished = true; 819 return; 820 } 821 final boolean positive = start > max; 822 final int edge = positive ? max : min; 823 final int overDistance = start - edge; 824 boolean keepIncreasing = overDistance * velocity >= 0; 825 if (keepIncreasing) { 826 // Will result in a bounce or a to_boundary depending on velocity. 827 startBounceAfterEdge(start, edge, velocity); 828 } else { 829 final double totalDistance = getSplineFlingDistance(velocity); 830 if (totalDistance > Math.abs(overDistance)) { 831 fling(start, velocity, positive ? min : start, positive ? start : max, mOver); 832 } else { 833 startSpringback(start, edge, velocity); 834 } 835 } 836 } 837 notifyEdgeReached(int start, int end, int over)838 void notifyEdgeReached(int start, int end, int over) { 839 // mState is used to detect successive notifications 840 if (mState == SPLINE) { 841 mOver = over; 842 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 843 // We were in fling/scroll mode before: current velocity is such that distance to 844 // edge is increasing. This ensures that startAfterEdge will not start a new fling. 845 startAfterEdge(start, end, end, (int) mCurrVelocity); 846 } 847 } 848 onEdgeReached()849 private void onEdgeReached() { 850 // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. 851 // The float cast below is necessary to avoid integer overflow. 852 final float velocitySquared = (float) mVelocity * mVelocity; 853 float distance = velocitySquared / (2.0f * Math.abs(mDeceleration)); 854 final float sign = Math.signum(mVelocity); 855 856 if (distance > mOver) { 857 // Default deceleration is not sufficient to slow us down before boundary 858 mDeceleration = - sign * velocitySquared / (2.0f * mOver); 859 distance = mOver; 860 } 861 862 mOver = (int) distance; 863 mState = BALLISTIC; 864 mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance); 865 mDuration = - (int) (1000.0f * mVelocity / mDeceleration); 866 } 867 continueWhenFinished()868 boolean continueWhenFinished() { 869 switch (mState) { 870 case SPLINE: 871 // Duration from start to null velocity 872 if (mDuration < mSplineDuration) { 873 // If the animation was clamped, we reached the edge 874 mCurrentPosition = mStart = mFinal; 875 // TODO Better compute speed when edge was reached 876 mVelocity = (int) mCurrVelocity; 877 mDeceleration = getDeceleration(mVelocity); 878 mStartTime += mDuration; 879 onEdgeReached(); 880 } else { 881 // Normal stop, no need to continue 882 return false; 883 } 884 break; 885 case BALLISTIC: 886 mStartTime += mDuration; 887 startSpringback(mFinal, mStart, 0); 888 break; 889 case CUBIC: 890 return false; 891 } 892 893 update(); 894 return true; 895 } 896 897 /* 898 * Update the current position and velocity for current time. Returns 899 * true if update has been done and false if animation duration has been 900 * reached. 901 */ update()902 boolean update() { 903 final long time = AnimationUtils.currentAnimationTimeMillis(); 904 final long currentTime = time - mStartTime; 905 906 if (currentTime == 0) { 907 // Skip work but report that we're still going if we have a nonzero duration. 908 return mDuration > 0; 909 } 910 if (currentTime > mDuration) { 911 return false; 912 } 913 914 double distance = 0.0; 915 switch (mState) { 916 case SPLINE: { 917 final float t = (float) currentTime / mSplineDuration; 918 final int index = (int) (NB_SAMPLES * t); 919 float distanceCoef = 1.f; 920 float velocityCoef = 0.f; 921 if (index < NB_SAMPLES) { 922 final float t_inf = (float) index / NB_SAMPLES; 923 final float t_sup = (float) (index + 1) / NB_SAMPLES; 924 final float d_inf = SPLINE_POSITION[index]; 925 final float d_sup = SPLINE_POSITION[index + 1]; 926 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); 927 distanceCoef = d_inf + (t - t_inf) * velocityCoef; 928 } 929 930 distance = distanceCoef * mSplineDistance; 931 mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; 932 break; 933 } 934 935 case BALLISTIC: { 936 final float t = currentTime / 1000.0f; 937 mCurrVelocity = mVelocity + mDeceleration * t; 938 distance = mVelocity * t + mDeceleration * t * t / 2.0f; 939 break; 940 } 941 942 case CUBIC: { 943 final float t = (float) (currentTime) / mDuration; 944 final float t2 = t * t; 945 final float sign = Math.signum(mVelocity); 946 distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2); 947 mCurrVelocity = sign * mOver * 6.0f * (- t + t2); 948 break; 949 } 950 } 951 952 mCurrentPosition = mStart + (int) Math.round(distance); 953 954 return true; 955 } 956 } 957 } 958