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