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