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