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 17 package android.widget; 18 19 import android.content.Context; 20 import android.hardware.SensorManager; 21 import android.os.Build; 22 import android.view.ViewConfiguration; 23 import android.view.animation.AnimationUtils; 24 import android.view.animation.Interpolator; 25 26 27 /** 28 * <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller} 29 * or {@link OverScroller}) to collect the data you need to produce a scrolling 30 * animation—for example, in response to a fling gesture. Scrollers track 31 * scroll offsets for you over time, but they don't automatically apply those 32 * positions to your view. It's your responsibility to get and apply new 33 * coordinates at a rate that will make the scrolling animation look smooth.</p> 34 * 35 * <p>Here is a simple example:</p> 36 * 37 * <pre> private Scroller mScroller = new Scroller(context); 38 * ... 39 * public void zoomIn() { 40 * // Revert any animation currently in progress 41 * mScroller.forceFinished(true); 42 * // Start scrolling by providing a starting point and 43 * // the distance to travel 44 * mScroller.startScroll(0, 0, 100, 0); 45 * // Invalidate to request a redraw 46 * invalidate(); 47 * }</pre> 48 * 49 * <p>To track the changing positions of the x/y coordinates, use 50 * {@link #computeScrollOffset}. The method returns a boolean to indicate 51 * whether the scroller is finished. If it isn't, it means that a fling or 52 * programmatic pan operation is still in progress. You can use this method to 53 * find the current offsets of the x and y coordinates, for example:</p> 54 * 55 * <pre>if (mScroller.computeScrollOffset()) { 56 * // Get current x and y positions 57 * int currX = mScroller.getCurrX(); 58 * int currY = mScroller.getCurrY(); 59 * ... 60 * }</pre> 61 */ 62 public class Scroller { 63 private final Interpolator mInterpolator; 64 65 private int mMode; 66 67 private int mStartX; 68 private int mStartY; 69 private int mFinalX; 70 private int mFinalY; 71 72 private int mMinX; 73 private int mMaxX; 74 private int mMinY; 75 private int mMaxY; 76 77 private int mCurrX; 78 private int mCurrY; 79 private long mStartTime; 80 private int mDuration; 81 private float mDurationReciprocal; 82 private float mDeltaX; 83 private float mDeltaY; 84 private boolean mFinished; 85 private boolean mFlywheel; 86 87 private float mVelocity; 88 private float mCurrVelocity; 89 private int mDistance; 90 91 private float mFlingFriction = ViewConfiguration.getScrollFriction(); 92 93 private static final int DEFAULT_DURATION = 250; 94 private static final int SCROLL_MODE = 0; 95 private static final int FLING_MODE = 1; 96 97 private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); 98 private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) 99 private static final float START_TENSION = 0.5f; 100 private static final float END_TENSION = 1.0f; 101 private static final float P1 = START_TENSION * INFLEXION; 102 private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); 103 104 private static final int NB_SAMPLES = 100; 105 private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; 106 private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; 107 108 private float mDeceleration; 109 private final float mPpi; 110 111 // A context-specific coefficient adjusted to physical values. 112 private float mPhysicalCoeff; 113 114 static { 115 float x_min = 0.0f; 116 float y_min = 0.0f; 117 for (int i = 0; i < NB_SAMPLES; i++) { 118 final float alpha = (float) i / NB_SAMPLES; 119 120 float x_max = 1.0f; 121 float x, tx, coef; 122 while (true) { 123 x = x_min + (x_max - x_min) / 2.0f; 124 coef = 3.0f * x * (1.0f - x); 125 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; 126 if (Math.abs(tx - alpha) < 1E-5) break; 127 if (tx > alpha) x_max = x; 128 else x_min = x; 129 } 130 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; 131 132 float y_max = 1.0f; 133 float y, dy; 134 while (true) { 135 y = y_min + (y_max - y_min) / 2.0f; 136 coef = 3.0f * y * (1.0f - y); 137 dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; 138 if (Math.abs(dy - alpha) < 1E-5) break; 139 if (dy > alpha) y_max = y; 140 else y_min = y; 141 } 142 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; 143 } 144 SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; 145 } 146 147 /** 148 * Create a Scroller with the default duration and interpolator. 149 */ Scroller(Context context)150 public Scroller(Context context) { 151 this(context, null); 152 } 153 154 /** 155 * Create a Scroller with the specified interpolator. If the interpolator is 156 * null, the default (viscous) interpolator will be used. "Flywheel" behavior will 157 * be in effect for apps targeting Honeycomb or newer. 158 */ Scroller(Context context, Interpolator interpolator)159 public Scroller(Context context, Interpolator interpolator) { 160 this(context, interpolator, 161 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); 162 } 163 164 /** 165 * Create a Scroller with the specified interpolator. If the interpolator is 166 * null, the default (viscous) interpolator will be used. Specify whether or 167 * not to support progressive "flywheel" behavior in flinging. 168 */ Scroller(Context context, Interpolator interpolator, boolean flywheel)169 public Scroller(Context context, Interpolator interpolator, boolean flywheel) { 170 mFinished = true; 171 if (interpolator == null) { 172 mInterpolator = new ViscousFluidInterpolator(); 173 } else { 174 mInterpolator = interpolator; 175 } 176 mPpi = context.getResources().getDisplayMetrics().density * 160.0f; 177 mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); 178 mFlywheel = flywheel; 179 180 mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning 181 } 182 183 /** 184 * The amount of friction applied to flings. The default value 185 * is {@link ViewConfiguration#getScrollFriction}. 186 * 187 * @param friction A scalar dimension-less value representing the coefficient of 188 * friction. 189 */ setFriction(float friction)190 public final void setFriction(float friction) { 191 mDeceleration = computeDeceleration(friction); 192 mFlingFriction = friction; 193 } 194 computeDeceleration(float friction)195 private float computeDeceleration(float friction) { 196 return SensorManager.GRAVITY_EARTH // g (m/s^2) 197 * 39.37f // inch/meter 198 * mPpi // pixels per inch 199 * friction; 200 } 201 202 /** 203 * 204 * Returns whether the scroller has finished scrolling. 205 * 206 * @return True if the scroller has finished scrolling, false otherwise. 207 */ isFinished()208 public final boolean isFinished() { 209 return mFinished; 210 } 211 212 /** 213 * Force the finished field to a particular value. 214 * 215 * @param finished The new finished value. 216 */ forceFinished(boolean finished)217 public final void forceFinished(boolean finished) { 218 mFinished = finished; 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 */ getDuration()226 public final int getDuration() { 227 return mDuration; 228 } 229 230 /** 231 * Returns the current X offset in the scroll. 232 * 233 * @return The new X offset as an absolute distance from the origin. 234 */ getCurrX()235 public final int getCurrX() { 236 return mCurrX; 237 } 238 239 /** 240 * Returns the current Y offset in the scroll. 241 * 242 * @return The new Y offset as an absolute distance from the origin. 243 */ getCurrY()244 public final int getCurrY() { 245 return mCurrY; 246 } 247 248 /** 249 * Returns the current velocity. 250 * 251 * @return The original velocity less the deceleration. Result may be 252 * negative. 253 */ getCurrVelocity()254 public float getCurrVelocity() { 255 return mMode == FLING_MODE ? 256 mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f; 257 } 258 259 /** 260 * Returns the start X offset in the scroll. 261 * 262 * @return The start X offset as an absolute distance from the origin. 263 */ getStartX()264 public final int getStartX() { 265 return mStartX; 266 } 267 268 /** 269 * Returns the start Y offset in the scroll. 270 * 271 * @return The start Y offset as an absolute distance from the origin. 272 */ getStartY()273 public final int getStartY() { 274 return mStartY; 275 } 276 277 /** 278 * Returns where the scroll will end. Valid only for "fling" scrolls. 279 * 280 * @return The final X offset as an absolute distance from the origin. 281 */ getFinalX()282 public final int getFinalX() { 283 return mFinalX; 284 } 285 286 /** 287 * Returns where the scroll will end. Valid only for "fling" scrolls. 288 * 289 * @return The final Y offset as an absolute distance from the origin. 290 */ getFinalY()291 public final int getFinalY() { 292 return mFinalY; 293 } 294 295 /** 296 * Call this when you want to know the new location. If it returns true, 297 * the animation is not yet finished. 298 */ computeScrollOffset()299 public boolean computeScrollOffset() { 300 if (mFinished) { 301 return false; 302 } 303 304 int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 305 306 if (timePassed < mDuration) { 307 switch (mMode) { 308 case SCROLL_MODE: 309 final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); 310 mCurrX = mStartX + Math.round(x * mDeltaX); 311 mCurrY = mStartY + Math.round(x * mDeltaY); 312 break; 313 case FLING_MODE: 314 final float t = (float) timePassed / mDuration; 315 final int index = (int) (NB_SAMPLES * t); 316 float distanceCoef = 1.f; 317 float velocityCoef = 0.f; 318 if (index < NB_SAMPLES) { 319 final float t_inf = (float) index / NB_SAMPLES; 320 final float t_sup = (float) (index + 1) / NB_SAMPLES; 321 final float d_inf = SPLINE_POSITION[index]; 322 final float d_sup = SPLINE_POSITION[index + 1]; 323 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); 324 distanceCoef = d_inf + (t - t_inf) * velocityCoef; 325 } 326 327 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; 328 329 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); 330 // Pin to mMinX <= mCurrX <= mMaxX 331 mCurrX = Math.min(mCurrX, mMaxX); 332 mCurrX = Math.max(mCurrX, mMinX); 333 334 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); 335 // Pin to mMinY <= mCurrY <= mMaxY 336 mCurrY = Math.min(mCurrY, mMaxY); 337 mCurrY = Math.max(mCurrY, mMinY); 338 339 if (mCurrX == mFinalX && mCurrY == mFinalY) { 340 mFinished = true; 341 } 342 343 break; 344 } 345 } 346 else { 347 mCurrX = mFinalX; 348 mCurrY = mFinalY; 349 mFinished = true; 350 } 351 return true; 352 } 353 354 /** 355 * Start scrolling by providing a starting point and the distance to travel. 356 * The scroll will use the default value of 250 milliseconds for the 357 * duration. 358 * 359 * @param startX Starting horizontal scroll offset in pixels. Positive 360 * numbers will scroll the content to the left. 361 * @param startY Starting vertical scroll offset in pixels. Positive numbers 362 * will scroll the content up. 363 * @param dx Horizontal distance to travel. Positive numbers will scroll the 364 * content to the left. 365 * @param dy Vertical distance to travel. Positive numbers will scroll the 366 * content up. 367 */ startScroll(int startX, int startY, int dx, int dy)368 public void startScroll(int startX, int startY, int dx, int dy) { 369 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 370 } 371 372 /** 373 * Start scrolling by providing a starting point, the distance to travel, 374 * and the duration of the scroll. 375 * 376 * @param startX Starting horizontal scroll offset in pixels. Positive 377 * numbers will scroll the content to the left. 378 * @param startY Starting vertical scroll offset in pixels. Positive numbers 379 * will scroll the content up. 380 * @param dx Horizontal distance to travel. Positive numbers will scroll the 381 * content to the left. 382 * @param dy Vertical distance to travel. Positive numbers will scroll the 383 * content up. 384 * @param duration Duration of the scroll in milliseconds. 385 */ startScroll(int startX, int startY, int dx, int dy, int duration)386 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 387 mMode = SCROLL_MODE; 388 mFinished = false; 389 mDuration = duration; 390 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 391 mStartX = startX; 392 mStartY = startY; 393 mFinalX = startX + dx; 394 mFinalY = startY + dy; 395 mDeltaX = dx; 396 mDeltaY = dy; 397 mDurationReciprocal = 1.0f / (float) mDuration; 398 } 399 400 /** 401 * Start scrolling based on a fling gesture. The distance travelled will 402 * depend on the initial velocity of the fling. 403 * 404 * @param startX Starting point of the scroll (X) 405 * @param startY Starting point of the scroll (Y) 406 * @param velocityX Initial velocity of the fling (X) measured in pixels per 407 * second. 408 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 409 * second 410 * @param minX Minimum X value. The scroller will not scroll past this 411 * point. 412 * @param maxX Maximum X value. The scroller will not scroll past this 413 * point. 414 * @param minY Minimum Y value. The scroller will not scroll past this 415 * point. 416 * @param maxY Maximum Y value. The scroller will not scroll past this 417 * point. 418 */ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)419 public void fling(int startX, int startY, int velocityX, int velocityY, 420 int minX, int maxX, int minY, int maxY) { 421 // Continue a scroll or fling in progress 422 if (mFlywheel && !mFinished) { 423 float oldVel = getCurrVelocity(); 424 425 float dx = (float) (mFinalX - mStartX); 426 float dy = (float) (mFinalY - mStartY); 427 float hyp = (float) Math.hypot(dx, dy); 428 429 float ndx = dx / hyp; 430 float ndy = dy / hyp; 431 432 float oldVelocityX = ndx * oldVel; 433 float oldVelocityY = ndy * oldVel; 434 if (Math.signum(velocityX) == Math.signum(oldVelocityX) && 435 Math.signum(velocityY) == Math.signum(oldVelocityY)) { 436 velocityX += oldVelocityX; 437 velocityY += oldVelocityY; 438 } 439 } 440 441 mMode = FLING_MODE; 442 mFinished = false; 443 444 float velocity = (float) Math.hypot(velocityX, velocityY); 445 446 mVelocity = velocity; 447 mDuration = getSplineFlingDuration(velocity); 448 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 449 mStartX = startX; 450 mStartY = startY; 451 452 float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; 453 float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; 454 455 double totalDistance = getSplineFlingDistance(velocity); 456 mDistance = (int) (totalDistance * Math.signum(velocity)); 457 458 mMinX = minX; 459 mMaxX = maxX; 460 mMinY = minY; 461 mMaxY = maxY; 462 463 mFinalX = startX + (int) Math.round(totalDistance * coeffX); 464 // Pin to mMinX <= mFinalX <= mMaxX 465 mFinalX = Math.min(mFinalX, mMaxX); 466 mFinalX = Math.max(mFinalX, mMinX); 467 468 mFinalY = startY + (int) Math.round(totalDistance * coeffY); 469 // Pin to mMinY <= mFinalY <= mMaxY 470 mFinalY = Math.min(mFinalY, mMaxY); 471 mFinalY = Math.max(mFinalY, mMinY); 472 } 473 getSplineDeceleration(float velocity)474 private double getSplineDeceleration(float velocity) { 475 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); 476 } 477 getSplineFlingDuration(float velocity)478 private int getSplineFlingDuration(float velocity) { 479 final double l = getSplineDeceleration(velocity); 480 final double decelMinusOne = DECELERATION_RATE - 1.0; 481 return (int) (1000.0 * Math.exp(l / decelMinusOne)); 482 } 483 getSplineFlingDistance(float velocity)484 private double getSplineFlingDistance(float velocity) { 485 final double l = getSplineDeceleration(velocity); 486 final double decelMinusOne = DECELERATION_RATE - 1.0; 487 return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); 488 } 489 490 /** 491 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 492 * aborting the animating cause the scroller to move to the final x and y 493 * position 494 * 495 * @see #forceFinished(boolean) 496 */ abortAnimation()497 public void abortAnimation() { 498 mCurrX = mFinalX; 499 mCurrY = mFinalY; 500 mFinished = true; 501 } 502 503 /** 504 * Extend the scroll animation. This allows a running animation to scroll 505 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 506 * 507 * @param extend Additional time to scroll in milliseconds. 508 * @see #setFinalX(int) 509 * @see #setFinalY(int) 510 */ extendDuration(int extend)511 public void extendDuration(int extend) { 512 int passed = timePassed(); 513 mDuration = passed + extend; 514 mDurationReciprocal = 1.0f / mDuration; 515 mFinished = false; 516 } 517 518 /** 519 * Returns the time elapsed since the beginning of the scrolling. 520 * 521 * @return The elapsed time in milliseconds. 522 */ timePassed()523 public int timePassed() { 524 return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 525 } 526 527 /** 528 * Sets the final position (X) for this scroller. 529 * 530 * @param newX The new X offset as an absolute distance from the origin. 531 * @see #extendDuration(int) 532 * @see #setFinalY(int) 533 */ setFinalX(int newX)534 public void setFinalX(int newX) { 535 mFinalX = newX; 536 mDeltaX = mFinalX - mStartX; 537 mFinished = false; 538 } 539 540 /** 541 * Sets the final position (Y) for this scroller. 542 * 543 * @param newY The new Y offset as an absolute distance from the origin. 544 * @see #extendDuration(int) 545 * @see #setFinalX(int) 546 */ setFinalY(int newY)547 public void setFinalY(int newY) { 548 mFinalY = newY; 549 mDeltaY = mFinalY - mStartY; 550 mFinished = false; 551 } 552 553 /** 554 * @hide 555 */ isScrollingInDirection(float xvel, float yvel)556 public boolean isScrollingInDirection(float xvel, float yvel) { 557 return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && 558 Math.signum(yvel) == Math.signum(mFinalY - mStartY); 559 } 560 561 static class ViscousFluidInterpolator implements Interpolator { 562 /** Controls the viscous fluid effect (how much of it). */ 563 private static final float VISCOUS_FLUID_SCALE = 8.0f; 564 565 private static final float VISCOUS_FLUID_NORMALIZE; 566 private static final float VISCOUS_FLUID_OFFSET; 567 568 static { 569 570 // must be set to 1.0 (used in viscousFluid()) 571 VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f); 572 // account for very small floating-point error 573 VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f); 574 } 575 viscousFluid(float x)576 private static float viscousFluid(float x) { 577 x *= VISCOUS_FLUID_SCALE; 578 if (x < 1.0f) { 579 x -= (1.0f - (float)Math.exp(-x)); 580 } else { 581 float start = 0.36787944117f; // 1/e == exp(-1) 582 x = 1.0f - (float)Math.exp(1.0f - x); 583 x = start + x * (1.0f - start); 584 } 585 return x; 586 } 587 588 @Override getInterpolation(float input)589 public float getInterpolation(float input) { 590 final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); 591 if (interpolated > 0) { 592 return interpolated + VISCOUS_FLUID_OFFSET; 593 } 594 return interpolated; 595 } 596 } 597 } 598