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 */ 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 */ 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 */ 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 */ 190 public final void setFriction(float friction) { 191 mDeceleration = computeDeceleration(friction); 192 mFlingFriction = friction; 193 } 194 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 474 private double getSplineDeceleration(float velocity) { 475 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); 476 } 477 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 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 */ 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 */ 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 */ 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 */ 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 */ 547 public void setFinalY(int newY) { 548 mFinalY = newY; 549 mDeltaY = mFinalY - mStartY; 550 mFinished = false; 551 } 552 553 /** 554 * @hide 555 */ 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 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 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