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.util.FloatMath; 23 import android.view.ViewConfiguration; 24 import android.view.animation.AnimationUtils; 25 import 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 */ 63 public 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 */ Scroller(Context context)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 */ Scroller(Context context, Interpolator interpolator)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 */ Scroller(Context context, Interpolator interpolator, boolean flywheel)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 */ setFriction(float friction)191 public final void setFriction(float friction) { 192 mDeceleration = computeDeceleration(friction); 193 mFlingFriction = friction; 194 } 195 computeDeceleration(float friction)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 */ isFinished()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 */ forceFinished(boolean finished)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 */ getDuration()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 */ getCurrX()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 */ getCurrY()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 */ getCurrVelocity()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 */ getStartX()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 */ getStartY()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 */ getFinalX()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 */ getFinalY()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 */ computeScrollOffset()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 */ startScroll(int startX, int startY, int dx, int dy)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 */ startScroll(int startX, int startY, int dx, int dy, int duration)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 */ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)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 getSplineDeceleration(float velocity)475 private double getSplineDeceleration(float velocity) { 476 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); 477 } 478 getSplineFlingDuration(float velocity)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 getSplineFlingDistance(float velocity)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 */ abortAnimation()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 */ extendDuration(int extend)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 */ timePassed()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 */ setFinalX(int newX)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 */ setFinalY(int newY)548 public void setFinalY(int newY) { 549 mFinalY = newY; 550 mDeltaY = mFinalY - mStartY; 551 mFinished = false; 552 } 553 554 /** 555 * @hide 556 */ isScrollingInDirection(float xvel, float yvel)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 viscousFluid(float x)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 getInterpolation(float input)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