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