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