ScrollerCompat.java revision 846786b33c7325e99603c2a7947f976b633c3496
1/* 2 * Copyright (C) 2012 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.support.v4.widget; 18 19import android.content.Context; 20import android.os.Build; 21import android.view.animation.AnimationUtils; 22import android.view.animation.Interpolator; 23import android.widget.Scroller; 24 25/** 26 * Provides access to new {@link android.widget.Scroller Scroller} APIs when available. 27 * 28 * <p>This class provides a platform version-independent mechanism for obeying the 29 * current device's preferred scroll physics and fling behavior. It offers a subset of 30 * the APIs from Scroller or OverScroller.</p> 31 */ 32public class ScrollerCompat { 33 private static final String TAG = "ScrollerCompat"; 34 35 Object mScroller; 36 ScrollerCompatImpl mImpl; 37 38 interface ScrollerCompatImpl { 39 Object createScroller(Context context, Interpolator interpolator); 40 boolean isFinished(Object scroller); 41 int getCurrX(Object scroller); 42 int getCurrY(Object scroller); 43 float getCurrVelocity(Object scroller); 44 boolean computeScrollOffset(Object scroller); 45 void startScroll(Object scroller, int startX, int startY, int dx, int dy); 46 void startScroll(Object scroller, int startX, int startY, int dx, int dy, int duration); 47 void fling(Object scroller, int startX, int startY, int velX, int velY, 48 int minX, int maxX, int minY, int maxY); 49 void fling(Object scroller, int startX, int startY, int velX, int velY, 50 int minX, int maxX, int minY, int maxY, int overX, int overY); 51 void abortAnimation(Object scroller); 52 void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, int overX); 53 void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY); 54 boolean isOverScrolled(Object scroller); 55 int getFinalX(Object scroller); 56 int getFinalY(Object scroller); 57 } 58 59 static final int CHASE_FRAME_TIME = 16; // ms per target frame 60 61 static class Chaser { 62 private int mX; 63 private int mY; 64 private int mTargetX; 65 private int mTargetY; 66 private float mTranslateSmoothing = 2; 67 private long mLastTime; 68 private boolean mFinished = true; 69 70 @Override 71 public String toString() { 72 return "{x=" + mX + " y=" + mY + " targetX=" + mTargetX + " targetY=" + mTargetY + 73 " smoothing=" + mTranslateSmoothing + " lastTime=" + mLastTime + "}"; 74 } 75 76 public int getCurrX() { 77 return mX; 78 } 79 80 public int getCurrY() { 81 return mY; 82 } 83 84 public int getFinalX() { 85 return mTargetX; 86 } 87 88 public int getFinalY() { 89 return mTargetY; 90 } 91 92 public void setCurrentPosition(int x, int y) { 93 mX = x; 94 mY = y; 95 mFinished = false; 96 } 97 98 public void setSmoothing(float smoothing) { 99 if (smoothing < 0) { 100 throw new IllegalArgumentException("smoothing value must be positive"); 101 } 102 mTranslateSmoothing = smoothing; 103 } 104 105 public boolean isSmoothingEnabled() { 106 return mTranslateSmoothing > 0; 107 } 108 109 public void setTarget(int targetX, int targetY) { 110 mTargetX = targetX; 111 mTargetY = targetY; 112 } 113 114 public void abort() { 115 mX = mTargetX; 116 mY = mTargetY; 117 mLastTime = AnimationUtils.currentAnimationTimeMillis(); 118 mFinished = true; 119 } 120 121 public boolean isFinished() { 122 return mFinished || (mX == mTargetX && mY == mTargetY); 123 } 124 125 public boolean computeScrollOffset() { 126 if (isSmoothingEnabled() && !isFinished()) { 127 final long now = AnimationUtils.currentAnimationTimeMillis(); 128 final long dt = now - mLastTime; 129 final float framesElapsed = (float) dt / CHASE_FRAME_TIME; 130 131 if (framesElapsed > 0) { 132 for (int i = 0; i < framesElapsed; i++) { 133 final int totalDx = mTargetX - mX; 134 final int totalDy = mTargetY - mY; 135 136 final int dx = (int) (totalDx / mTranslateSmoothing); 137 final int dy = (int) (totalDy / mTranslateSmoothing); 138 139 mX += dx; 140 mY += dy; 141 142 // Handle cropping at the end 143 if (mX != mTargetX && dx == 0) { 144 mX = mTargetX; 145 } 146 if (mY != mTargetY && dy == 0) { 147 mY = mTargetY; 148 } 149 } 150 151 mLastTime = now; 152 } 153 mFinished = mX == mTargetX && mY == mTargetY; 154 return true; 155 } 156 return false; 157 } 158 } 159 160 static class ScrollerCompatImplBase implements ScrollerCompatImpl { 161 private Chaser mChaser; 162 163 public ScrollerCompatImplBase() { 164 mChaser = createChaser(); 165 } 166 167 protected Chaser createChaser() { 168 // Override if running on a platform version where this isn't needed 169 return new Chaser(); 170 } 171 172 @Override 173 public Object createScroller(Context context, Interpolator interpolator) { 174 return interpolator != null ? 175 new Scroller(context, interpolator) : new Scroller(context); 176 } 177 178 @Override 179 public boolean isFinished(Object scroller) { 180 return (!isSmoothingEnabled() || mChaser.isFinished()) && 181 ((Scroller) scroller).isFinished(); 182 } 183 184 @Override 185 public int getCurrX(Object scroller) { 186 if (isSmoothingEnabled()) { 187 return mChaser.getCurrX(); 188 } 189 return ((Scroller) scroller).getCurrX(); 190 } 191 192 @Override 193 public int getCurrY(Object scroller) { 194 if (isSmoothingEnabled()) { 195 return mChaser.getCurrY(); 196 } 197 return ((Scroller) scroller).getCurrY(); 198 } 199 200 @Override 201 public float getCurrVelocity(Object scroller) { 202 return 0; 203 } 204 205 @Override 206 public boolean computeScrollOffset(Object scroller) { 207 final Scroller s = (Scroller) scroller; 208 final boolean result = s.computeScrollOffset(); 209 if (isSmoothingEnabled()) { 210 mChaser.setTarget(s.getCurrX(), s.getCurrY()); 211 if (isSmoothingEnabled() && !mChaser.isFinished()) { 212 return mChaser.computeScrollOffset() || result; 213 } 214 } 215 return result; 216 } 217 218 private boolean isSmoothingEnabled() { 219 return mChaser != null && mChaser.isSmoothingEnabled(); 220 } 221 222 @Override 223 public void startScroll(Object scroller, int startX, int startY, int dx, int dy) { 224 if (isSmoothingEnabled()) { 225 mChaser.abort(); 226 mChaser.setCurrentPosition(startX, startY); 227 } 228 ((Scroller) scroller).startScroll(startX, startY, dx, dy); 229 } 230 231 @Override 232 public void startScroll(Object scroller, int startX, int startY, int dx, int dy, 233 int duration) { 234 if (isSmoothingEnabled()) { 235 mChaser.abort(); 236 mChaser.setCurrentPosition(startX, startY); 237 } 238 ((Scroller) scroller).startScroll(startX, startY, dx, dy, duration); 239 } 240 241 @Override 242 public void fling(Object scroller, int startX, int startY, int velX, int velY, 243 int minX, int maxX, int minY, int maxY) { 244 if (isSmoothingEnabled()) { 245 mChaser.abort(); 246 mChaser.setCurrentPosition(startX, startY); 247 } 248 ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); 249 } 250 251 @Override 252 public void fling(Object scroller, int startX, int startY, int velX, int velY, 253 int minX, int maxX, int minY, int maxY, int overX, int overY) { 254 if (isSmoothingEnabled()) { 255 mChaser.abort(); 256 mChaser.setCurrentPosition(startX, startY); 257 } 258 ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); 259 } 260 261 @Override 262 public void abortAnimation(Object scroller) { 263 if (mChaser != null) { 264 mChaser.abort(); 265 } 266 ((Scroller) scroller).abortAnimation(); 267 } 268 269 @Override 270 public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, 271 int overX) { 272 // No-op 273 } 274 275 @Override 276 public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { 277 // No-op 278 } 279 280 @Override 281 public boolean isOverScrolled(Object scroller) { 282 // Always false 283 return false; 284 } 285 286 @Override 287 public int getFinalX(Object scroller) { 288 return ((Scroller) scroller).getFinalX(); 289 } 290 291 @Override 292 public int getFinalY(Object scroller) { 293 return ((Scroller) scroller).getFinalY(); 294 } 295 } 296 297 static class ScrollerCompatImplGingerbread implements ScrollerCompatImpl { 298 private Chaser mChaser; 299 300 public ScrollerCompatImplGingerbread() { 301 mChaser = createChaser(); 302 } 303 304 @Override 305 public Object createScroller(Context context, Interpolator interpolator) { 306 return ScrollerCompatGingerbread.createScroller(context, interpolator); 307 } 308 309 protected Chaser createChaser() { 310 return new Chaser(); 311 } 312 313 @Override 314 public boolean isFinished(Object scroller) { 315 return (!isSmoothingEnabled() || mChaser.isFinished()) && 316 ScrollerCompatGingerbread.isFinished(scroller); 317 } 318 319 @Override 320 public int getCurrX(Object scroller) { 321 if (isSmoothingEnabled()) { 322 return mChaser.getCurrX(); 323 } 324 return ScrollerCompatGingerbread.getCurrX(scroller); 325 } 326 327 @Override 328 public int getCurrY(Object scroller) { 329 if (isSmoothingEnabled()) { 330 return mChaser.getCurrY(); 331 } 332 return ScrollerCompatGingerbread.getCurrY(scroller); 333 } 334 335 @Override 336 public float getCurrVelocity(Object scroller) { 337 return 0; 338 } 339 340 @Override 341 public boolean computeScrollOffset(Object scroller) { 342 final boolean result = ScrollerCompatGingerbread.computeScrollOffset(scroller); 343 if (isSmoothingEnabled()) { 344 mChaser.setTarget(ScrollerCompatGingerbread.getCurrX(scroller), 345 ScrollerCompatGingerbread.getCurrY(scroller)); 346 if (!mChaser.isFinished()) { 347 return mChaser.computeScrollOffset() || result; 348 } 349 } 350 return result; 351 } 352 353 private boolean isSmoothingEnabled() { 354 return mChaser != null && mChaser.isSmoothingEnabled(); 355 } 356 357 @Override 358 public void startScroll(Object scroller, int startX, int startY, int dx, int dy) { 359 if (isSmoothingEnabled()) { 360 mChaser.abort(); 361 mChaser.setCurrentPosition(startX, startY); 362 } 363 ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy); 364 } 365 366 @Override 367 public void startScroll(Object scroller, int startX, int startY, int dx, int dy, 368 int duration) { 369 if (isSmoothingEnabled()) { 370 mChaser.abort(); 371 mChaser.setCurrentPosition(startX, startY); 372 } 373 ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy, duration); 374 } 375 376 @Override 377 public void fling(Object scroller, int startX, int startY, int velX, int velY, 378 int minX, int maxX, int minY, int maxY) { 379 if (isSmoothingEnabled()) { 380 mChaser.abort(); 381 mChaser.setCurrentPosition(startX, startY); 382 } 383 ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY, 384 minX, maxX, minY, maxY); 385 } 386 387 @Override 388 public void fling(Object scroller, int startX, int startY, int velX, int velY, 389 int minX, int maxX, int minY, int maxY, int overX, int overY) { 390 if (isSmoothingEnabled()) { 391 mChaser.abort(); 392 mChaser.setCurrentPosition(startX, startY); 393 } 394 ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY, 395 minX, maxX, minY, maxY, overX, overY); 396 } 397 398 @Override 399 public void abortAnimation(Object scroller) { 400 if (mChaser != null) { 401 mChaser.abort(); 402 } 403 ScrollerCompatGingerbread.abortAnimation(scroller); 404 } 405 406 @Override 407 public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, 408 int overX) { 409 ScrollerCompatGingerbread.notifyHorizontalEdgeReached(scroller, startX, finalX, overX); 410 } 411 412 @Override 413 public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { 414 ScrollerCompatGingerbread.notifyVerticalEdgeReached(scroller, startY, finalY, overY); 415 } 416 417 @Override 418 public boolean isOverScrolled(Object scroller) { 419 return ScrollerCompatGingerbread.isOverScrolled(scroller); 420 } 421 422 @Override 423 public int getFinalX(Object scroller) { 424 return ScrollerCompatGingerbread.getFinalX(scroller); 425 } 426 427 @Override 428 public int getFinalY(Object scroller) { 429 return ScrollerCompatGingerbread.getFinalY(scroller); 430 } 431 } 432 433 static class ScrollerCompatImplIcs extends ScrollerCompatImplGingerbread { 434 @Override 435 public float getCurrVelocity(Object scroller) { 436 return ScrollerCompatIcs.getCurrVelocity(scroller); 437 } 438 } 439 440 public static ScrollerCompat create(Context context) { 441 return create(context, null); 442 } 443 444 public static ScrollerCompat create(Context context, Interpolator interpolator) { 445 return new ScrollerCompat(context, interpolator); 446 } 447 448 ScrollerCompat(Context context, Interpolator interpolator) { 449 final int version = Build.VERSION.SDK_INT; 450 if (version >= 14) { // ICS 451 mImpl = new ScrollerCompatImplIcs(); 452 } else if (version >= 9) { // Gingerbread 453 mImpl = new ScrollerCompatImplGingerbread(); 454 } else { 455 mImpl = new ScrollerCompatImplBase(); 456 } 457 mScroller = mImpl.createScroller(context, interpolator); 458 } 459 460 /** 461 * Returns whether the scroller has finished scrolling. 462 * 463 * @return True if the scroller has finished scrolling, false otherwise. 464 */ 465 public boolean isFinished() { 466 return mImpl.isFinished(mScroller); 467 } 468 469 /** 470 * Returns the current X offset in the scroll. 471 * 472 * @return The new X offset as an absolute distance from the origin. 473 */ 474 public int getCurrX() { 475 return mImpl.getCurrX(mScroller); 476 } 477 478 /** 479 * Returns the current Y offset in the scroll. 480 * 481 * @return The new Y offset as an absolute distance from the origin. 482 */ 483 public int getCurrY() { 484 return mImpl.getCurrY(mScroller); 485 } 486 487 /** 488 * @return The final X position for the scroll in progress, if known. 489 */ 490 public int getFinalX() { 491 return mImpl.getFinalX(mScroller); 492 } 493 494 /** 495 * @return The final Y position for the scroll in progress, if known. 496 */ 497 public int getFinalY() { 498 return mImpl.getFinalY(mScroller); 499 } 500 501 /** 502 * Returns the current velocity on platform versions that support it. 503 * 504 * <p>The device must support at least API level 14 (Ice Cream Sandwich). 505 * On older platform versions this method will return 0. This method should 506 * only be used as input for nonessential visual effects such as {@link EdgeEffectCompat}.</p> 507 * 508 * @return The original velocity less the deceleration. Result may be 509 * negative. 510 */ 511 public float getCurrVelocity() { 512 return mImpl.getCurrVelocity(mScroller); 513 } 514 515 /** 516 * Call this when you want to know the new location. If it returns true, 517 * the animation is not yet finished. loc will be altered to provide the 518 * new location. 519 */ 520 public boolean computeScrollOffset() { 521 return mImpl.computeScrollOffset(mScroller); 522 } 523 524 /** 525 * Start scrolling by providing a starting point and the distance to travel. 526 * The scroll will use the default value of 250 milliseconds for the 527 * duration. 528 * 529 * @param startX Starting horizontal scroll offset in pixels. Positive 530 * numbers will scroll the content to the left. 531 * @param startY Starting vertical scroll offset in pixels. Positive numbers 532 * will scroll the content up. 533 * @param dx Horizontal distance to travel. Positive numbers will scroll the 534 * content to the left. 535 * @param dy Vertical distance to travel. Positive numbers will scroll the 536 * content up. 537 */ 538 public void startScroll(int startX, int startY, int dx, int dy) { 539 mImpl.startScroll(mScroller, startX, startY, dx, dy); 540 } 541 542 /** 543 * Start scrolling by providing a starting point and the distance to travel. 544 * 545 * @param startX Starting horizontal scroll offset in pixels. Positive 546 * numbers will scroll the content to the left. 547 * @param startY Starting vertical scroll offset in pixels. Positive numbers 548 * will scroll the content up. 549 * @param dx Horizontal distance to travel. Positive numbers will scroll the 550 * content to the left. 551 * @param dy Vertical distance to travel. Positive numbers will scroll the 552 * content up. 553 * @param duration Duration of the scroll in milliseconds. 554 */ 555 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 556 mImpl.startScroll(mScroller, startX, startY, dx, dy, duration); 557 } 558 559 /** 560 * Start scrolling based on a fling gesture. The distance travelled will 561 * depend on the initial velocity of the fling. 562 * 563 * @param startX Starting point of the scroll (X) 564 * @param startY Starting point of the scroll (Y) 565 * @param velocityX Initial velocity of the fling (X) measured in pixels per 566 * second. 567 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 568 * second 569 * @param minX Minimum X value. The scroller will not scroll past this 570 * point. 571 * @param maxX Maximum X value. The scroller will not scroll past this 572 * point. 573 * @param minY Minimum Y value. The scroller will not scroll past this 574 * point. 575 * @param maxY Maximum Y value. The scroller will not scroll past this 576 * point. 577 */ 578 public void fling(int startX, int startY, int velocityX, int velocityY, 579 int minX, int maxX, int minY, int maxY) { 580 mImpl.fling(mScroller, startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); 581 } 582 583 /** 584 * Start scrolling based on a fling gesture. The distance travelled will 585 * depend on the initial velocity of the fling. 586 * 587 * @param startX Starting point of the scroll (X) 588 * @param startY Starting point of the scroll (Y) 589 * @param velocityX Initial velocity of the fling (X) measured in pixels per 590 * second. 591 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 592 * second 593 * @param minX Minimum X value. The scroller will not scroll past this 594 * point. 595 * @param maxX Maximum X value. The scroller will not scroll past this 596 * point. 597 * @param minY Minimum Y value. The scroller will not scroll past this 598 * point. 599 * @param maxY Maximum Y value. The scroller will not scroll past this 600 * point. 601 * @param overX Overfling range. If > 0, horizontal overfling in either 602 * direction will be possible. 603 * @param overY Overfling range. If > 0, vertical overfling in either 604 * direction will be possible. 605 */ 606 public void fling(int startX, int startY, int velocityX, int velocityY, 607 int minX, int maxX, int minY, int maxY, int overX, int overY) { 608 mImpl.fling(mScroller, startX, startY, velocityX, velocityY, 609 minX, maxX, minY, maxY, overX, overY); 610 } 611 612 /** 613 * Stops the animation. Aborting the animation causes the scroller to move to the final x and y 614 * position. 615 */ 616 public void abortAnimation() { 617 mImpl.abortAnimation(mScroller); 618 } 619 620 621 /** 622 * Notify the scroller that we've reached a horizontal boundary. 623 * Normally the information to handle this will already be known 624 * when the animation is started, such as in a call to one of the 625 * fling functions. However there are cases where this cannot be known 626 * in advance. This function will transition the current motion and 627 * animate from startX to finalX as appropriate. 628 * 629 * @param startX Starting/current X position 630 * @param finalX Desired final X position 631 * @param overX Magnitude of overscroll allowed. This should be the maximum 632 * desired distance from finalX. Absolute value - must be positive. 633 */ 634 public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { 635 mImpl.notifyHorizontalEdgeReached(mScroller, startX, finalX, overX); 636 } 637 638 /** 639 * Notify the scroller that we've reached a vertical boundary. 640 * Normally the information to handle this will already be known 641 * when the animation is started, such as in a call to one of the 642 * fling functions. However there are cases where this cannot be known 643 * in advance. This function will animate a parabolic motion from 644 * startY to finalY. 645 * 646 * @param startY Starting/current Y position 647 * @param finalY Desired final Y position 648 * @param overY Magnitude of overscroll allowed. This should be the maximum 649 * desired distance from finalY. Absolute value - must be positive. 650 */ 651 public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { 652 mImpl.notifyVerticalEdgeReached(mScroller, startY, finalY, overY); 653 } 654 655 /** 656 * Returns whether the current Scroller is currently returning to a valid position. 657 * Valid bounds were provided by the 658 * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. 659 * 660 * One should check this value before calling 661 * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress 662 * to restore a valid position will then be stopped. The caller has to take into account 663 * the fact that the started scroll will start from an overscrolled position. 664 * 665 * @return true when the current position is overscrolled and in the process of 666 * interpolating back to a valid value. 667 */ 668 public boolean isOverScrolled() { 669 return mImpl.isOverScrolled(mScroller); 670 } 671} 672