OverScroller.java revision 6579b0b4ac0e781efab044aaaf3f66447cf5e067
1/* 2 * Copyright (C) 2006 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 17package android.widget; 18 19import android.content.Context; 20import android.util.FloatMath; 21import android.view.animation.AnimationUtils; 22import android.view.animation.Interpolator; 23import android.widget.Scroller.MagneticScroller; 24 25/** 26 * This class encapsulates scrolling with the ability to overshoot the bounds 27 * of a scrolling operation. This class is a drop-in replacement for 28 * {@link android.widget.Scroller} in most cases. 29 */ 30public class OverScroller { 31 private int mMode; 32 33 private MagneticOverScroller mScrollerX; 34 private MagneticOverScroller mScrollerY; 35 36 private final Interpolator mInterpolator; 37 38 private static final int DEFAULT_DURATION = 250; 39 private static final int SCROLL_MODE = 0; 40 private static final int FLING_MODE = 1; 41 42 /** 43 * Creates an OverScroller with a viscous fluid scroll interpolator. 44 * @param context 45 */ 46 public OverScroller(Context context) { 47 this(context, null); 48 } 49 50 /** 51 * Creates an OverScroller with default edge bounce coefficients. 52 * @param context The context of this application. 53 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 54 * be used. 55 */ 56 public OverScroller(Context context, Interpolator interpolator) { 57 this(context, interpolator, MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT, 58 MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT); 59 } 60 61 /** 62 * Creates an OverScroller. 63 * @param context The context of this application. 64 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will 65 * be used. 66 * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the 67 * velocity which is preserved in the bounce when the horizontal edge is reached. A null value 68 * means no bounce. 69 * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. 70 */ 71 public OverScroller(Context context, Interpolator interpolator, 72 float bounceCoefficientX, float bounceCoefficientY) { 73 mInterpolator = interpolator; 74 mScrollerX = new MagneticOverScroller(); 75 mScrollerY = new MagneticOverScroller(); 76 MagneticScroller.initializeFromContext(context); 77 78 mScrollerX.setBounceCoefficient(bounceCoefficientX); 79 mScrollerY.setBounceCoefficient(bounceCoefficientY); 80 } 81 82 /** 83 * 84 * Returns whether the scroller has finished scrolling. 85 * 86 * @return True if the scroller has finished scrolling, false otherwise. 87 */ 88 public final boolean isFinished() { 89 return mScrollerX.mFinished && mScrollerY.mFinished; 90 } 91 92 /** 93 * Force the finished field to a particular value. Contrary to 94 * {@link #abortAnimation()}, forcing the animation to finished 95 * does NOT cause the scroller to move to the final x and y 96 * position. 97 * 98 * @param finished The new finished value. 99 */ 100 public final void forceFinished(boolean finished) { 101 mScrollerX.mFinished = mScrollerY.mFinished = finished; 102 } 103 104 /** 105 * Returns the current X offset in the scroll. 106 * 107 * @return The new X offset as an absolute distance from the origin. 108 */ 109 public final int getCurrX() { 110 return mScrollerX.mCurrentPosition; 111 } 112 113 /** 114 * Returns the current Y offset in the scroll. 115 * 116 * @return The new Y offset as an absolute distance from the origin. 117 */ 118 public final int getCurrY() { 119 return mScrollerY.mCurrentPosition; 120 } 121 122 /** 123 * @hide 124 * Returns the current velocity. 125 * 126 * @return The original velocity less the deceleration, norm of the X and Y velocity vector. 127 */ 128 public float getCurrVelocity() { 129 float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity; 130 squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity; 131 return FloatMath.sqrt(squaredNorm); 132 } 133 134 /** 135 * Returns the start X offset in the scroll. 136 * 137 * @return The start X offset as an absolute distance from the origin. 138 */ 139 public final int getStartX() { 140 return mScrollerX.mStart; 141 } 142 143 /** 144 * Returns the start Y offset in the scroll. 145 * 146 * @return The start Y offset as an absolute distance from the origin. 147 */ 148 public final int getStartY() { 149 return mScrollerY.mStart; 150 } 151 152 /** 153 * Returns where the scroll will end. Valid only for "fling" scrolls. 154 * 155 * @return The final X offset as an absolute distance from the origin. 156 */ 157 public final int getFinalX() { 158 return mScrollerX.mFinal; 159 } 160 161 /** 162 * Returns where the scroll will end. Valid only for "fling" scrolls. 163 * 164 * @return The final Y offset as an absolute distance from the origin. 165 */ 166 public final int getFinalY() { 167 return mScrollerY.mFinal; 168 } 169 170 /** 171 * Returns how long the scroll event will take, in milliseconds. 172 * 173 * @return The duration of the scroll in milliseconds. 174 * 175 * @hide Pending removal once nothing depends on it 176 * @deprecated OverScrollers don't necessarily have a fixed duration. 177 * This function will lie to the best of its ability. 178 */ 179 public final int getDuration() { 180 return Math.max(mScrollerX.mDuration, mScrollerY.mDuration); 181 } 182 183 /** 184 * Extend the scroll animation. This allows a running animation to scroll 185 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 186 * 187 * @param extend Additional time to scroll in milliseconds. 188 * @see #setFinalX(int) 189 * @see #setFinalY(int) 190 * 191 * @hide Pending removal once nothing depends on it 192 * @deprecated OverScrollers don't necessarily have a fixed duration. 193 * Instead of setting a new final position and extending 194 * the duration of an existing scroll, use startScroll 195 * to begin a new animation. 196 */ 197 public void extendDuration(int extend) { 198 mScrollerX.extendDuration(extend); 199 mScrollerY.extendDuration(extend); 200 } 201 202 /** 203 * Sets the final position (X) for this scroller. 204 * 205 * @param newX The new X offset as an absolute distance from the origin. 206 * @see #extendDuration(int) 207 * @see #setFinalY(int) 208 * 209 * @hide Pending removal once nothing depends on it 210 * @deprecated OverScroller's final position may change during an animation. 211 * Instead of setting a new final position and extending 212 * the duration of an existing scroll, use startScroll 213 * to begin a new animation. 214 */ 215 public void setFinalX(int newX) { 216 mScrollerX.setFinalPosition(newX); 217 } 218 219 /** 220 * Sets the final position (Y) for this scroller. 221 * 222 * @param newY The new Y offset as an absolute distance from the origin. 223 * @see #extendDuration(int) 224 * @see #setFinalX(int) 225 * 226 * @hide Pending removal once nothing depends on it 227 * @deprecated OverScroller's final position may change during an animation. 228 * Instead of setting a new final position and extending 229 * the duration of an existing scroll, use startScroll 230 * to begin a new animation. 231 */ 232 public void setFinalY(int newY) { 233 mScrollerY.setFinalPosition(newY); 234 } 235 236 /** 237 * Call this when you want to know the new location. If it returns true, the 238 * animation is not yet finished. 239 */ 240 public boolean computeScrollOffset() { 241 if (isFinished()) { 242 return false; 243 } 244 245 switch (mMode) { 246 case SCROLL_MODE: 247 long time = AnimationUtils.currentAnimationTimeMillis(); 248 // Any scroller can be used for time, since they were started 249 // together in scroll mode. We use X here. 250 final long elapsedTime = time - mScrollerX.mStartTime; 251 252 final int duration = mScrollerX.mDuration; 253 if (elapsedTime < duration) { 254 float q = (float) (elapsedTime) / duration; 255 256 if (mInterpolator == null) 257 q = Scroller.viscousFluid(q); 258 else 259 q = mInterpolator.getInterpolation(q); 260 261 mScrollerX.updateScroll(q); 262 mScrollerY.updateScroll(q); 263 } else { 264 abortAnimation(); 265 } 266 break; 267 268 case FLING_MODE: 269 if (!mScrollerX.mFinished) { 270 if (!mScrollerX.update()) { 271 if (!mScrollerX.continueWhenFinished()) { 272 mScrollerX.finish(); 273 } 274 } 275 } 276 277 if (!mScrollerY.mFinished) { 278 if (!mScrollerY.update()) { 279 if (!mScrollerY.continueWhenFinished()) { 280 mScrollerY.finish(); 281 } 282 } 283 } 284 285 break; 286 } 287 288 return true; 289 } 290 291 /** 292 * Start scrolling by providing a starting point and the distance to travel. 293 * The scroll will use the default value of 250 milliseconds for the 294 * duration. 295 * 296 * @param startX Starting horizontal scroll offset in pixels. Positive 297 * numbers will scroll the content to the left. 298 * @param startY Starting vertical scroll offset in pixels. Positive numbers 299 * will scroll the content up. 300 * @param dx Horizontal distance to travel. Positive numbers will scroll the 301 * content to the left. 302 * @param dy Vertical distance to travel. Positive numbers will scroll the 303 * content up. 304 */ 305 public void startScroll(int startX, int startY, int dx, int dy) { 306 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 307 } 308 309 /** 310 * Start scrolling by providing a starting point and the distance to travel. 311 * 312 * @param startX Starting horizontal scroll offset in pixels. Positive 313 * numbers will scroll the content to the left. 314 * @param startY Starting vertical scroll offset in pixels. Positive numbers 315 * will scroll the content up. 316 * @param dx Horizontal distance to travel. Positive numbers will scroll the 317 * content to the left. 318 * @param dy Vertical distance to travel. Positive numbers will scroll the 319 * content up. 320 * @param duration Duration of the scroll in milliseconds. 321 */ 322 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 323 mMode = SCROLL_MODE; 324 mScrollerX.startScroll(startX, dx, duration); 325 mScrollerY.startScroll(startY, dy, duration); 326 } 327 328 /** 329 * Call this when you want to 'spring back' into a valid coordinate range. 330 * 331 * @param startX Starting X coordinate 332 * @param startY Starting Y coordinate 333 * @param minX Minimum valid X value 334 * @param maxX Maximum valid X value 335 * @param minY Minimum valid Y value 336 * @param maxY Minimum valid Y value 337 * @return true if a springback was initiated, false if startX and startY were 338 * already within the valid range. 339 */ 340 public boolean springback(int startX, int startY, int minX, int maxX, int minY, int maxY) { 341 mMode = FLING_MODE; 342 343 // Make sure both methods are called. 344 final boolean spingbackX = mScrollerX.springback(startX, minX, maxX); 345 final boolean spingbackY = mScrollerY.springback(startY, minY, maxY); 346 return spingbackX || spingbackY; 347 } 348 349 public void fling(int startX, int startY, int velocityX, int velocityY, 350 int minX, int maxX, int minY, int maxY) { 351 fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); 352 } 353 354 /** 355 * Start scrolling based on a fling gesture. The distance traveled will 356 * depend on the initial velocity of the fling. 357 * 358 * @param startX Starting point of the scroll (X) 359 * @param startY Starting point of the scroll (Y) 360 * @param velocityX Initial velocity of the fling (X) measured in pixels per 361 * second. 362 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 363 * second 364 * @param minX Minimum X value. The scroller will not scroll past this point 365 * unless overX > 0. If overfling is allowed, it will use minX as 366 * a springback boundary. 367 * @param maxX Maximum X value. The scroller will not scroll past this point 368 * unless overX > 0. If overfling is allowed, it will use maxX as 369 * a springback boundary. 370 * @param minY Minimum Y value. The scroller will not scroll past this point 371 * unless overY > 0. If overfling is allowed, it will use minY as 372 * a springback boundary. 373 * @param maxY Maximum Y value. The scroller will not scroll past this point 374 * unless overY > 0. If overfling is allowed, it will use maxY as 375 * a springback boundary. 376 * @param overX Overfling range. If > 0, horizontal overfling in either 377 * direction will be possible. 378 * @param overY Overfling range. If > 0, vertical overfling in either 379 * direction will be possible. 380 */ 381 public void fling(int startX, int startY, int velocityX, int velocityY, 382 int minX, int maxX, int minY, int maxY, int overX, int overY) { 383 mMode = FLING_MODE; 384 mScrollerX.fling(startX, velocityX, minX, maxX, overX); 385 mScrollerY.fling(startY, velocityY, minY, maxY, overY); 386 } 387 388 /** 389 * Notify the scroller that we've reached a horizontal boundary. 390 * Normally the information to handle this will already be known 391 * when the animation is started, such as in a call to one of the 392 * fling functions. However there are cases where this cannot be known 393 * in advance. This function will transition the current motion and 394 * animate from startX to finalX as appropriate. 395 * 396 * @param startX Starting/current X position 397 * @param finalX Desired final X position 398 * @param overX Magnitude of overscroll allowed. This should be the maximum 399 * desired distance from finalX. Absolute value - must be positive. 400 */ 401 public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { 402 mScrollerX.notifyEdgeReached(startX, finalX, overX); 403 } 404 405 /** 406 * Notify the scroller that we've reached a vertical boundary. 407 * Normally the information to handle this will already be known 408 * when the animation is started, such as in a call to one of the 409 * fling functions. However there are cases where this cannot be known 410 * in advance. This function will animate a parabolic motion from 411 * startY to finalY. 412 * 413 * @param startY Starting/current Y position 414 * @param finalY Desired final Y position 415 * @param overY Magnitude of overscroll allowed. This should be the maximum 416 * desired distance from finalY. 417 */ 418 public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { 419 mScrollerY.notifyEdgeReached(startY, finalY, overY); 420 } 421 422 /** 423 * Returns whether the current Scroller is currently returning to a valid position. 424 * Valid bounds were provided by the 425 * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. 426 * 427 * One should check this value before calling 428 * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress 429 * to restore a valid position will then be stopped. The caller has to take into account 430 * the fact that the started scroll will start from an overscrolled position. 431 * 432 * @return true when the current position is overscrolled and in the process of 433 * interpolating back to a valid value. 434 */ 435 public boolean isOverscrolled() { 436 return ((!mScrollerX.mFinished && 437 mScrollerX.mState != MagneticOverScroller.TO_EDGE) || 438 (!mScrollerY.mFinished && 439 mScrollerY.mState != MagneticOverScroller.TO_EDGE)); 440 } 441 442 /** 443 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 444 * aborting the animating causes the scroller to move to the final x and y 445 * positions. 446 * 447 * @see #forceFinished(boolean) 448 */ 449 public void abortAnimation() { 450 mScrollerX.finish(); 451 mScrollerY.finish(); 452 } 453 454 /** 455 * Returns the time elapsed since the beginning of the scrolling. 456 * 457 * @return The elapsed time in milliseconds. 458 * 459 * @hide 460 */ 461 public int timePassed() { 462 final long time = AnimationUtils.currentAnimationTimeMillis(); 463 final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime); 464 return (int) (time - startTime); 465 } 466 467 static class MagneticOverScroller extends Scroller.MagneticScroller { 468 private static final int TO_EDGE = 0; 469 private static final int TO_BOUNDARY = 1; 470 private static final int TO_BOUNCE = 2; 471 472 private int mState = TO_EDGE; 473 474 // The allowed overshot distance before boundary is reached. 475 private int mOver; 476 477 // Duration in milliseconds to go back from edge to edge. Springback is half of it. 478 private static final int OVERSCROLL_SPRINGBACK_DURATION = 200; 479 480 // Oscillation period 481 private static final float TIME_COEF = 482 1000.0f * (float) Math.PI / OVERSCROLL_SPRINGBACK_DURATION; 483 484 // If the velocity is smaller than this value, no bounce is triggered 485 // when the edge limits are reached (would result in a zero pixels 486 // displacement anyway). 487 private static final float MINIMUM_VELOCITY_FOR_BOUNCE = 140.0f; 488 489 // Proportion of the velocity that is preserved when the edge is reached. 490 private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.16f; 491 492 private float mBounceCoefficient = DEFAULT_BOUNCE_COEFFICIENT; 493 494 void setBounceCoefficient(float coefficient) { 495 mBounceCoefficient = coefficient; 496 } 497 498 boolean springback(int start, int min, int max) { 499 mFinished = true; 500 501 mStart = start; 502 mVelocity = 0; 503 504 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 505 mDuration = 0; 506 507 if (start < min) { 508 startSpringback(start, min, false); 509 } else if (start > max) { 510 startSpringback(start, max, true); 511 } 512 513 return !mFinished; 514 } 515 516 private void startSpringback(int start, int end, boolean positive) { 517 mFinished = false; 518 mState = TO_BOUNCE; 519 mStart = mFinal = end; 520 mDuration = OVERSCROLL_SPRINGBACK_DURATION; 521 mStartTime -= OVERSCROLL_SPRINGBACK_DURATION / 2; 522 mVelocity = (int) (Math.abs(end - start) * TIME_COEF * (positive ? 1.0 : -1.0f)); 523 } 524 525 void fling(int start, int velocity, int min, int max, int over) { 526 mState = TO_EDGE; 527 mOver = over; 528 529 super.fling(start, velocity, min, max); 530 531 if (start > max) { 532 if (start >= max + over) { 533 springback(max + over, min, max); 534 } else { 535 if (velocity <= 0) { 536 springback(start, min, max); 537 } else { 538 long time = AnimationUtils.currentAnimationTimeMillis(); 539 final double durationSinceEdge = 540 Math.atan((start-max) * TIME_COEF / velocity) / TIME_COEF; 541 mStartTime = (int) (time - 1000.0f * durationSinceEdge); 542 543 // Simulate a bounce that started from edge 544 mStart = max; 545 546 mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF)); 547 548 onEdgeReached(); 549 } 550 } 551 } else { 552 if (start < min) { 553 if (start <= min - over) { 554 springback(min - over, min, max); 555 } else { 556 if (velocity >= 0) { 557 springback(start, min, max); 558 } else { 559 long time = AnimationUtils.currentAnimationTimeMillis(); 560 final double durationSinceEdge = 561 Math.atan((start-min) * TIME_COEF / velocity) / TIME_COEF; 562 mStartTime = (int) (time - 1000.0f * durationSinceEdge); 563 564 // Simulate a bounce that started from edge 565 mStart = min; 566 567 mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF)); 568 569 onEdgeReached(); 570 } 571 572 } 573 } 574 } 575 } 576 577 void notifyEdgeReached(int start, int end, int over) { 578 mDeceleration = getDeceleration(mVelocity); 579 580 // Local time, used to compute edge crossing time. 581 float timeCurrent = mCurrVelocity / mDeceleration; 582 final int distance = end - start; 583 float timeEdge = -(float) Math.sqrt((2.0f * distance / mDeceleration) 584 + (timeCurrent * timeCurrent)); 585 586 mVelocity = (int) (mDeceleration * timeEdge); 587 588 // Simulate a symmetric bounce that started from edge 589 mStart = end; 590 591 mOver = over; 592 593 long time = AnimationUtils.currentAnimationTimeMillis(); 594 mStartTime = (int) (time - 1000.0f * (timeCurrent - timeEdge)); 595 596 onEdgeReached(); 597 } 598 599 private void onEdgeReached() { 600 // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. 601 final float distance = mVelocity / TIME_COEF; 602 603 if (Math.abs(distance) < mOver) { 604 // Spring force will bring us back to final position 605 mState = TO_BOUNCE; 606 mFinal = mStart; 607 mDuration = OVERSCROLL_SPRINGBACK_DURATION; 608 } else { 609 // Velocity is too high, we will hit the boundary limit 610 mState = TO_BOUNDARY; 611 int over = mVelocity > 0 ? mOver : -mOver; 612 mFinal = mStart + over; 613 mDuration = (int) (1000.0f * Math.asin(over / distance) / TIME_COEF); 614 } 615 } 616 617 @Override 618 boolean continueWhenFinished() { 619 switch (mState) { 620 case TO_EDGE: 621 // Duration from start to null velocity 622 int duration = (int) (-1000.0f * mVelocity / mDeceleration); 623 if (mDuration < duration) { 624 // If the animation was clamped, we reached the edge 625 mStart = mFinal; 626 // Speed when edge was reached 627 mVelocity = (int) (mVelocity + mDeceleration * mDuration / 1000.0f); 628 mStartTime += mDuration; 629 onEdgeReached(); 630 } else { 631 // Normal stop, no need to continue 632 return false; 633 } 634 break; 635 case TO_BOUNDARY: 636 mStartTime += mDuration; 637 startSpringback(mFinal, mFinal - (mVelocity > 0 ? mOver:-mOver), mVelocity > 0); 638 break; 639 case TO_BOUNCE: 640 //mVelocity = (int) (mVelocity * BOUNCE_COEFFICIENT); 641 mVelocity = (int) (mVelocity * mBounceCoefficient); 642 if (Math.abs(mVelocity) < MINIMUM_VELOCITY_FOR_BOUNCE) { 643 return false; 644 } 645 mStartTime += mDuration; 646 break; 647 } 648 649 update(); 650 return true; 651 } 652 653 /* 654 * Update the current position and velocity for current time. Returns 655 * true if update has been done and false if animation duration has been 656 * reached. 657 */ 658 @Override 659 boolean update() { 660 final long time = AnimationUtils.currentAnimationTimeMillis(); 661 final long duration = time - mStartTime; 662 663 if (duration > mDuration) { 664 return false; 665 } 666 667 double distance; 668 final float t = duration / 1000.0f; 669 if (mState == TO_EDGE) { 670 mCurrVelocity = mVelocity + mDeceleration * t; 671 distance = mVelocity * t + mDeceleration * t * t / 2.0f; 672 } else { 673 final float d = t * TIME_COEF; 674 mCurrVelocity = mVelocity * (float)Math.cos(d); 675 distance = mVelocity / TIME_COEF * Math.sin(d); 676 } 677 678 mCurrentPosition = mStart + (int) distance; 679 return true; 680 } 681 } 682} 683