1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.content.Context; 20 import android.hardware.SensorManager; 21 import android.util.FloatMath; 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 MagneticOverScroller mScrollerX; 35 private MagneticOverScroller mScrollerY; 36 37 private final Interpolator mInterpolator; 38 39 private static final int DEFAULT_DURATION = 250; 40 private static final int SCROLL_MODE = 0; 41 private static final int FLING_MODE = 1; 42 43 /** 44 * Creates an OverScroller with a viscous fluid scroll interpolator. 45 * @param context 46 */ 47 public OverScroller(Context context) { 48 this(context, null); 49 } 50 51 /** 52 * Creates an OverScroller with default edge bounce coefficients. 53 * @param context The context of this application. 54 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 55 * be used. 56 */ 57 public OverScroller(Context context, Interpolator interpolator) { 58 this(context, interpolator, MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT, 59 MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT); 60 } 61 62 /** 63 * Creates an OverScroller. 64 * @param context The context of this application. 65 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 66 * be used. 67 * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the 68 * velocity which is preserved in the bounce when the horizontal edge is reached. A null value 69 * means no bounce. 70 * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. 71 */ 72 public OverScroller(Context context, Interpolator interpolator, 73 float bounceCoefficientX, float bounceCoefficientY) { 74 mInterpolator = interpolator; 75 mScrollerX = new MagneticOverScroller(); 76 mScrollerY = new MagneticOverScroller(); 77 MagneticOverScroller.initializeFromContext(context); 78 79 mScrollerX.setBounceCoefficient(bounceCoefficientX); 80 mScrollerY.setBounceCoefficient(bounceCoefficientY); 81 } 82 83 /** 84 * 85 * Returns whether the scroller has finished scrolling. 86 * 87 * @return True if the scroller has finished scrolling, false otherwise. 88 */ 89 public final boolean isFinished() { 90 return mScrollerX.mFinished && mScrollerY.mFinished; 91 } 92 93 /** 94 * Force the finished field to a particular value. Contrary to 95 * {@link #abortAnimation()}, forcing the animation to finished 96 * does NOT cause the scroller to move to the final x and y 97 * position. 98 * 99 * @param finished The new finished value. 100 */ 101 public final void forceFinished(boolean finished) { 102 mScrollerX.mFinished = mScrollerY.mFinished = finished; 103 } 104 105 /** 106 * Returns the current X offset in the scroll. 107 * 108 * @return The new X offset as an absolute distance from the origin. 109 */ 110 public final int getCurrX() { 111 return mScrollerX.mCurrentPosition; 112 } 113 114 /** 115 * Returns the current Y offset in the scroll. 116 * 117 * @return The new Y offset as an absolute distance from the origin. 118 */ 119 public final int getCurrY() { 120 return mScrollerY.mCurrentPosition; 121 } 122 123 /** 124 * @hide 125 * Returns the current velocity. 126 * 127 * @return The original velocity less the deceleration, norm of the X and Y velocity vector. 128 */ 129 public float getCurrVelocity() { 130 float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity; 131 squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity; 132 return FloatMath.sqrt(squaredNorm); 133 } 134 135 /** 136 * Returns the start X offset in the scroll. 137 * 138 * @return The start X offset as an absolute distance from the origin. 139 */ 140 public final int getStartX() { 141 return mScrollerX.mStart; 142 } 143 144 /** 145 * Returns the start Y offset in the scroll. 146 * 147 * @return The start Y offset as an absolute distance from the origin. 148 */ 149 public final int getStartY() { 150 return mScrollerY.mStart; 151 } 152 153 /** 154 * Returns where the scroll will end. Valid only for "fling" scrolls. 155 * 156 * @return The final X offset as an absolute distance from the origin. 157 */ 158 public final int getFinalX() { 159 return mScrollerX.mFinal; 160 } 161 162 /** 163 * Returns where the scroll will end. Valid only for "fling" scrolls. 164 * 165 * @return The final Y offset as an absolute distance from the origin. 166 */ 167 public final int getFinalY() { 168 return mScrollerY.mFinal; 169 } 170 171 /** 172 * Returns how long the scroll event will take, in milliseconds. 173 * 174 * @return The duration of the scroll in milliseconds. 175 * 176 * @hide Pending removal once nothing depends on it 177 * @deprecated OverScrollers don't necessarily have a fixed duration. 178 * This function will lie to the best of its ability. 179 */ 180 public final int getDuration() { 181 return Math.max(mScrollerX.mDuration, mScrollerY.mDuration); 182 } 183 184 /** 185 * Extend the scroll animation. This allows a running animation to scroll 186 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 187 * 188 * @param extend Additional time to scroll in milliseconds. 189 * @see #setFinalX(int) 190 * @see #setFinalY(int) 191 * 192 * @hide Pending removal once nothing depends on it 193 * @deprecated OverScrollers don't necessarily have a fixed duration. 194 * Instead of setting a new final position and extending 195 * the duration of an existing scroll, use startScroll 196 * to begin a new animation. 197 */ 198 public void extendDuration(int extend) { 199 mScrollerX.extendDuration(extend); 200 mScrollerY.extendDuration(extend); 201 } 202 203 /** 204 * Sets the final position (X) for this scroller. 205 * 206 * @param newX The new X offset as an absolute distance from the origin. 207 * @see #extendDuration(int) 208 * @see #setFinalY(int) 209 * 210 * @hide Pending removal once nothing depends on it 211 * @deprecated OverScroller's final position may change during an animation. 212 * Instead of setting a new final position and extending 213 * the duration of an existing scroll, use startScroll 214 * to begin a new animation. 215 */ 216 public void setFinalX(int newX) { 217 mScrollerX.setFinalPosition(newX); 218 } 219 220 /** 221 * Sets the final position (Y) for this scroller. 222 * 223 * @param newY The new Y offset as an absolute distance from the origin. 224 * @see #extendDuration(int) 225 * @see #setFinalX(int) 226 * 227 * @hide Pending removal once nothing depends on it 228 * @deprecated OverScroller's final position may change during an animation. 229 * Instead of setting a new final position and extending 230 * the duration of an existing scroll, use startScroll 231 * to begin a new animation. 232 */ 233 public void setFinalY(int newY) { 234 mScrollerY.setFinalPosition(newY); 235 } 236 237 /** 238 * Call this when you want to know the new location. If it returns true, the 239 * animation is not yet finished. 240 */ 241 public boolean computeScrollOffset() { 242 if (isFinished()) { 243 return false; 244 } 245 246 switch (mMode) { 247 case SCROLL_MODE: 248 long time = AnimationUtils.currentAnimationTimeMillis(); 249 // Any scroller can be used for time, since they were started 250 // together in scroll mode. We use X here. 251 final long elapsedTime = time - mScrollerX.mStartTime; 252 253 final int duration = mScrollerX.mDuration; 254 if (elapsedTime < duration) { 255 float q = (float) (elapsedTime) / duration; 256 257 if (mInterpolator == null) 258 q = Scroller.viscousFluid(q); 259 else 260 q = mInterpolator.getInterpolation(q); 261 262 mScrollerX.updateScroll(q); 263 mScrollerY.updateScroll(q); 264 } else { 265 abortAnimation(); 266 } 267 break; 268 269 case FLING_MODE: 270 if (!mScrollerX.mFinished) { 271 if (!mScrollerX.update()) { 272 if (!mScrollerX.continueWhenFinished()) { 273 mScrollerX.finish(); 274 } 275 } 276 } 277 278 if (!mScrollerY.mFinished) { 279 if (!mScrollerY.update()) { 280 if (!mScrollerY.continueWhenFinished()) { 281 mScrollerY.finish(); 282 } 283 } 284 } 285 286 break; 287 } 288 289 return true; 290 } 291 292 /** 293 * Start scrolling by providing a starting point and the distance to travel. 294 * The scroll will use the default value of 250 milliseconds for the 295 * duration. 296 * 297 * @param startX Starting horizontal scroll offset in pixels. Positive 298 * numbers will scroll the content to the left. 299 * @param startY Starting vertical scroll offset in pixels. Positive numbers 300 * will scroll the content up. 301 * @param dx Horizontal distance to travel. Positive numbers will scroll the 302 * content to the left. 303 * @param dy Vertical distance to travel. Positive numbers will scroll the 304 * content up. 305 */ 306 public void startScroll(int startX, int startY, int dx, int dy) { 307 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 308 } 309 310 /** 311 * Start scrolling by providing a starting point and the distance to travel. 312 * 313 * @param startX Starting horizontal scroll offset in pixels. Positive 314 * numbers will scroll the content to the left. 315 * @param startY Starting vertical scroll offset in pixels. Positive numbers 316 * will scroll the content up. 317 * @param dx Horizontal distance to travel. Positive numbers will scroll the 318 * content to the left. 319 * @param dy Vertical distance to travel. Positive numbers will scroll the 320 * content up. 321 * @param duration Duration of the scroll in milliseconds. 322 */ 323 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 324 mMode = SCROLL_MODE; 325 mScrollerX.startScroll(startX, dx, duration); 326 mScrollerY.startScroll(startY, dy, duration); 327 } 328 329 /** 330 * Call this when you want to 'spring back' into a valid coordinate range. 331 * 332 * @param startX Starting X coordinate 333 * @param startY Starting Y coordinate 334 * @param minX Minimum valid X value 335 * @param maxX Maximum valid X value 336 * @param minY Minimum valid Y value 337 * @param maxY Minimum valid Y value 338 * @return true if a springback was initiated, false if startX and startY were 339 * already within the valid range. 340 */ 341 public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) { 342 mMode = FLING_MODE; 343 344 // Make sure both methods are called. 345 final boolean spingbackX = mScrollerX.springback(startX, minX, maxX); 346 final boolean spingbackY = mScrollerY.springback(startY, minY, maxY); 347 return spingbackX || spingbackY; 348 } 349 350 public void fling(int startX, int startY, int velocityX, int velocityY, 351 int minX, int maxX, int minY, int maxY) { 352 fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); 353 } 354 355 /** 356 * Start scrolling based on a fling gesture. The distance traveled will 357 * depend on the initial velocity of the fling. 358 * 359 * @param startX Starting point of the scroll (X) 360 * @param startY Starting point of the scroll (Y) 361 * @param velocityX Initial velocity of the fling (X) measured in pixels per 362 * second. 363 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 364 * second 365 * @param minX Minimum X value. The scroller will not scroll past this point 366 * unless overX > 0. If overfling is allowed, it will use minX as 367 * a springback boundary. 368 * @param maxX Maximum X value. The scroller will not scroll past this point 369 * unless overX > 0. If overfling is allowed, it will use maxX as 370 * a springback boundary. 371 * @param minY Minimum Y value. The scroller will not scroll past this point 372 * unless overY > 0. If overfling is allowed, it will use minY as 373 * a springback boundary. 374 * @param maxY Maximum Y value. The scroller will not scroll past this point 375 * unless overY > 0. If overfling is allowed, it will use maxY as 376 * a springback boundary. 377 * @param overX Overfling range. If > 0, horizontal overfling in either 378 * direction will be possible. 379 * @param overY Overfling range. If > 0, vertical overfling in either 380 * direction will be possible. 381 */ 382 public void fling(int startX, int startY, int velocityX, int velocityY, 383 int minX, int maxX, int minY, int maxY, int overX, int overY) { 384 mMode = FLING_MODE; 385 mScrollerX.fling(startX, velocityX, minX, maxX, overX); 386 mScrollerY.fling(startY, velocityY, minY, maxY, overY); 387 } 388 389 /** 390 * Notify the scroller that we've reached a horizontal boundary. 391 * Normally the information to handle this will already be known 392 * when the animation is started, such as in a call to one of the 393 * fling functions. However there are cases where this cannot be known 394 * in advance. This function will transition the current motion and 395 * animate from startX to finalX as appropriate. 396 * 397 * @param startX Starting/current X position 398 * @param finalX Desired final X position 399 * @param overX Magnitude of overscroll allowed. This should be the maximum 400 * desired distance from finalX. Absolute value - must be positive. 401 */ 402 public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { 403 mScrollerX.notifyEdgeReached(startX, finalX, overX); 404 } 405 406 /** 407 * Notify the scroller that we've reached a vertical boundary. 408 * Normally the information to handle this will already be known 409 * when the animation is started, such as in a call to one of the 410 * fling functions. However there are cases where this cannot be known 411 * in advance. This function will animate a parabolic motion from 412 * startY to finalY. 413 * 414 * @param startY Starting/current Y position 415 * @param finalY Desired final Y position 416 * @param overY Magnitude of overscroll allowed. This should be the maximum 417 * desired distance from finalY. 418 */ 419 public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { 420 mScrollerY.notifyEdgeReached(startY, finalY, overY); 421 } 422 423 /** 424 * Returns whether the current Scroller is currently returning to a valid position. 425 * Valid bounds were provided by the 426 * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. 427 * 428 * One should check this value before calling 429 * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress 430 * to restore a valid position will then be stopped. The caller has to take into account 431 * the fact that the started scroll will start from an overscrolled position. 432 * 433 * @return true when the current position is overscrolled and in the process of 434 * interpolating back to a valid value. 435 */ 436 public boolean isOverScrolled() { 437 return ((!mScrollerX.mFinished && 438 mScrollerX.mState != MagneticOverScroller.TO_EDGE) || 439 (!mScrollerY.mFinished && 440 mScrollerY.mState != MagneticOverScroller.TO_EDGE)); 441 } 442 443 /** 444 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 445 * aborting the animating causes the scroller to move to the final x and y 446 * positions. 447 * 448 * @see #forceFinished(boolean) 449 */ 450 public void abortAnimation() { 451 mScrollerX.finish(); 452 mScrollerY.finish(); 453 } 454 455 /** 456 * Returns the time elapsed since the beginning of the scrolling. 457 * 458 * @return The elapsed time in milliseconds. 459 * 460 * @hide 461 */ 462 public int timePassed() { 463 final long time = AnimationUtils.currentAnimationTimeMillis(); 464 final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime); 465 return (int) (time - startTime); 466 } 467 468 static class MagneticOverScroller { 469 // Initial position 470 int mStart; 471 472 // Current position 473 int mCurrentPosition; 474 475 // Final position 476 int mFinal; 477 478 // Initial velocity 479 int mVelocity; 480 481 // Current velocity 482 float mCurrVelocity; 483 484 // Constant current deceleration 485 float mDeceleration; 486 487 // Animation starting time, in system milliseconds 488 long mStartTime; 489 490 // Animation duration, in milliseconds 491 int mDuration; 492 493 // Whether the animation is currently in progress 494 boolean mFinished; 495 496 // Constant gravity value, used to scale deceleration 497 static float GRAVITY; 498 499 static void initializeFromContext(Context context) { 500 final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; 501 GRAVITY = SensorManager.GRAVITY_EARTH // g (m/s^2) 502 * 39.37f // inch/meter 503 * ppi // pixels per inch 504 * ViewConfiguration.getScrollFriction(); 505 } 506 507 private static final int TO_EDGE = 0; 508 private static final int TO_BOUNDARY = 1; 509 private static final int TO_BOUNCE = 2; 510 511 private int mState = TO_EDGE; 512 513 // The allowed overshot distance before boundary is reached. 514 private int mOver; 515 516 // Duration in milliseconds to go back from edge to edge. Springback is half of it. 517 private static final int OVERSCROLL_SPRINGBACK_DURATION = 200; 518 519 // Oscillation period 520 private static final float TIME_COEF = 521 1000.0f * (float) Math.PI / OVERSCROLL_SPRINGBACK_DURATION; 522 523 // If the velocity is smaller than this value, no bounce is triggered 524 // when the edge limits are reached (would result in a zero pixels 525 // displacement anyway). 526 private static final float MINIMUM_VELOCITY_FOR_BOUNCE = Float.MAX_VALUE;//140.0f; 527 528 // Proportion of the velocity that is preserved when the edge is reached. 529 private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.16f; 530 531 private float mBounceCoefficient = DEFAULT_BOUNCE_COEFFICIENT; 532 533 MagneticOverScroller() { 534 mFinished = true; 535 } 536 537 void updateScroll(float q) { 538 mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); 539 } 540 541 /* 542 * Get a signed deceleration that will reduce the velocity. 543 */ 544 static float getDeceleration(int velocity) { 545 return velocity > 0 ? -GRAVITY : GRAVITY; 546 } 547 548 /* 549 * Returns the time (in milliseconds) it will take to go from start to end. 550 */ 551 static int computeDuration(int start, int end, float initialVelocity, float deceleration) { 552 final int distance = start - end; 553 final float discriminant = initialVelocity * initialVelocity - 2.0f * deceleration 554 * distance; 555 if (discriminant >= 0.0f) { 556 float delta = (float) Math.sqrt(discriminant); 557 if (deceleration < 0.0f) { 558 delta = -delta; 559 } 560 return (int) (1000.0f * (-initialVelocity - delta) / deceleration); 561 } 562 563 // End position can not be reached 564 return 0; 565 } 566 567 void startScroll(int start, int distance, int duration) { 568 mFinished = false; 569 570 mStart = start; 571 mFinal = start + distance; 572 573 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 574 mDuration = duration; 575 576 // Unused 577 mDeceleration = 0.0f; 578 mVelocity = 0; 579 } 580 581 void fling(int start, int velocity, int min, int max) { 582 mFinished = false; 583 584 mStart = start; 585 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 586 587 mVelocity = velocity; 588 589 mDeceleration = getDeceleration(velocity); 590 591 // A start from an invalid position immediately brings back to a valid position 592 if (mStart < min) { 593 mDuration = 0; 594 mFinal = min; 595 return; 596 } 597 598 if (mStart > max) { 599 mDuration = 0; 600 mFinal = max; 601 return; 602 } 603 604 // Duration are expressed in milliseconds 605 mDuration = (int) (-1000.0f * velocity / mDeceleration); 606 607 mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration)); 608 609 // Clamp to a valid final position 610 if (mFinal < min) { 611 mFinal = min; 612 mDuration = computeDuration(mStart, min, mVelocity, mDeceleration); 613 } 614 615 if (mFinal > max) { 616 mFinal = max; 617 mDuration = computeDuration(mStart, max, mVelocity, mDeceleration); 618 } 619 } 620 621 void finish() { 622 mCurrentPosition = mFinal; 623 // Not reset since WebView relies on this value for fast fling. 624 // mCurrVelocity = 0.0f; 625 mFinished = true; 626 } 627 628 void setFinalPosition(int position) { 629 mFinal = position; 630 mFinished = false; 631 } 632 633 void extendDuration(int extend) { 634 final long time = AnimationUtils.currentAnimationTimeMillis(); 635 final int elapsedTime = (int) (time - mStartTime); 636 mDuration = elapsedTime + extend; 637 mFinished = false; 638 } 639 640 void setBounceCoefficient(float coefficient) { 641 mBounceCoefficient = coefficient; 642 } 643 644 boolean springback(int start, int min, int max) { 645 mFinished = true; 646 647 mStart = start; 648 mVelocity = 0; 649 650 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 651 mDuration = 0; 652 653 if (start < min) { 654 startSpringback(start, min, false); 655 } else if (start > max) { 656 startSpringback(start, max, true); 657 } 658 659 return !mFinished; 660 } 661 662 private void startSpringback(int start, int end, boolean positive) { 663 mFinished = false; 664 mState = TO_BOUNCE; 665 mStart = mFinal = end; 666 mDuration = OVERSCROLL_SPRINGBACK_DURATION; 667 mStartTime -= OVERSCROLL_SPRINGBACK_DURATION / 2; 668 mVelocity = (int) (Math.abs(end - start) * TIME_COEF * (positive ? 1.0 : -1.0f)); 669 } 670 671 void fling(int start, int velocity, int min, int max, int over) { 672 mState = TO_EDGE; 673 mOver = over; 674 675 mFinished = false; 676 677 mStart = start; 678 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 679 680 mVelocity = velocity; 681 682 mDeceleration = getDeceleration(velocity); 683 684 // Duration are expressed in milliseconds 685 mDuration = (int) (-1000.0f * velocity / mDeceleration); 686 687 mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration)); 688 689 // Clamp to a valid final position 690 if (mFinal < min) { 691 mFinal = min; 692 mDuration = computeDuration(mStart, min, mVelocity, mDeceleration); 693 } 694 695 if (mFinal > max) { 696 mFinal = max; 697 mDuration = computeDuration(mStart, max, mVelocity, mDeceleration); 698 } 699 700 if (start > max) { 701 if (start >= max + over) { 702 springback(max + over, min, max); 703 } else { 704 if (velocity <= 0) { 705 springback(start, min, max); 706 } else { 707 long time = AnimationUtils.currentAnimationTimeMillis(); 708 final double durationSinceEdge = 709 Math.atan((start-max) * TIME_COEF / velocity) / TIME_COEF; 710 mStartTime = (int) (time - 1000.0f * durationSinceEdge); 711 712 // Simulate a bounce that started from edge 713 mStart = max; 714 715 mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF)); 716 717 onEdgeReached(); 718 } 719 } 720 } else { 721 if (start < min) { 722 if (start <= min - over) { 723 springback(min - over, min, max); 724 } else { 725 if (velocity >= 0) { 726 springback(start, min, max); 727 } else { 728 long time = AnimationUtils.currentAnimationTimeMillis(); 729 final double durationSinceEdge = 730 Math.atan((start-min) * TIME_COEF / velocity) / TIME_COEF; 731 mStartTime = (int) (time - 1000.0f * durationSinceEdge); 732 733 // Simulate a bounce that started from edge 734 mStart = min; 735 736 mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF)); 737 738 onEdgeReached(); 739 } 740 741 } 742 } 743 } 744 } 745 746 void notifyEdgeReached(int start, int end, int over) { 747 mDeceleration = getDeceleration(mVelocity); 748 749 // Local time, used to compute edge crossing time. 750 float timeCurrent = mCurrVelocity / mDeceleration; 751 final int distance = end - start; 752 float timeEdge = -(float) Math.sqrt((2.0f * distance / mDeceleration) 753 + (timeCurrent * timeCurrent)); 754 755 mVelocity = (int) (mDeceleration * timeEdge); 756 757 // Simulate a symmetric bounce that started from edge 758 mStart = end; 759 760 mOver = over; 761 762 long time = AnimationUtils.currentAnimationTimeMillis(); 763 mStartTime = (int) (time - 1000.0f * (timeCurrent - timeEdge)); 764 765 onEdgeReached(); 766 } 767 768 private void onEdgeReached() { 769 // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. 770 final float distance = mVelocity / TIME_COEF; 771 772 if (Math.abs(distance) < mOver) { 773 // Spring force will bring us back to final position 774 mState = TO_BOUNCE; 775 mFinal = mStart; 776 mDuration = OVERSCROLL_SPRINGBACK_DURATION; 777 } else { 778 // Velocity is too high, we will hit the boundary limit 779 mState = TO_BOUNDARY; 780 int over = mVelocity > 0 ? mOver : -mOver; 781 mFinal = mStart + over; 782 mDuration = (int) (1000.0f * Math.asin(over / distance) / TIME_COEF); 783 } 784 } 785 786 boolean continueWhenFinished() { 787 switch (mState) { 788 case TO_EDGE: 789 // Duration from start to null velocity 790 int duration = (int) (-1000.0f * mVelocity / mDeceleration); 791 if (mDuration < duration) { 792 // If the animation was clamped, we reached the edge 793 mStart = mFinal; 794 // Speed when edge was reached 795 mVelocity = (int) (mVelocity + mDeceleration * mDuration / 1000.0f); 796 mStartTime += mDuration; 797 onEdgeReached(); 798 } else { 799 // Normal stop, no need to continue 800 return false; 801 } 802 break; 803 case TO_BOUNDARY: 804 mStartTime += mDuration; 805 startSpringback(mFinal, mFinal - (mVelocity > 0 ? mOver:-mOver), mVelocity > 0); 806 break; 807 case TO_BOUNCE: 808 //mVelocity = (int) (mVelocity * BOUNCE_COEFFICIENT); 809 mVelocity = (int) (mVelocity * mBounceCoefficient); 810 if (Math.abs(mVelocity) < MINIMUM_VELOCITY_FOR_BOUNCE) { 811 return false; 812 } 813 mStartTime += mDuration; 814 break; 815 } 816 817 update(); 818 return true; 819 } 820 821 /* 822 * Update the current position and velocity for current time. Returns 823 * true if update has been done and false if animation duration has been 824 * reached. 825 */ 826 boolean update() { 827 final long time = AnimationUtils.currentAnimationTimeMillis(); 828 final long duration = time - mStartTime; 829 830 if (duration > mDuration) { 831 return false; 832 } 833 834 double distance; 835 final float t = duration / 1000.0f; 836 if (mState == TO_EDGE) { 837 mCurrVelocity = mVelocity + mDeceleration * t; 838 distance = mVelocity * t + mDeceleration * t * t / 2.0f; 839 } else { 840 final float d = t * TIME_COEF; 841 mCurrVelocity = mVelocity * (float)Math.cos(d); 842 distance = mVelocity / TIME_COEF * Math.sin(d); 843 } 844 845 mCurrentPosition = mStart + (int) distance; 846 return true; 847 } 848 } 849 } 850