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 int mMode; 65 66 private int mStartX; 67 private int mStartY; 68 private int mFinalX; 69 private int mFinalY; 70 71 private int mMinX; 72 private int mMaxX; 73 private int mMinY; 74 private int mMaxY; 75 76 private int mCurrX; 77 private int mCurrY; 78 private long mStartTime; 79 private int mDuration; 80 private float mDurationReciprocal; 81 private float mDeltaX; 82 private float mDeltaY; 83 private boolean mFinished; 84 private Interpolator mInterpolator; 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 // This controls the viscous fluid effect (how much of it) 147 sViscousFluidScale = 8.0f; 148 // must be set to 1.0 (used in viscousFluid()) 149 sViscousFluidNormalize = 1.0f; 150 sViscousFluidNormalize = 1.0f / viscousFluid(1.0f); 151 152 } 153 154 private static float sViscousFluidScale; 155 private static float sViscousFluidNormalize; 156 157 /** 158 * Create a Scroller with the default duration and interpolator. 159 */ Scroller(Context context)160 public Scroller(Context context) { 161 this(context, null); 162 } 163 164 /** 165 * Create a Scroller with the specified interpolator. If the interpolator is 166 * null, the default (viscous) interpolator will be used. "Flywheel" behavior will 167 * be in effect for apps targeting Honeycomb or newer. 168 */ Scroller(Context context, Interpolator interpolator)169 public Scroller(Context context, Interpolator interpolator) { 170 this(context, interpolator, 171 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); 172 } 173 174 /** 175 * Create a Scroller with the specified interpolator. If the interpolator is 176 * null, the default (viscous) interpolator will be used. Specify whether or 177 * not to support progressive "flywheel" behavior in flinging. 178 */ Scroller(Context context, Interpolator interpolator, boolean flywheel)179 public Scroller(Context context, Interpolator interpolator, boolean flywheel) { 180 mFinished = true; 181 mInterpolator = interpolator; 182 mPpi = context.getResources().getDisplayMetrics().density * 160.0f; 183 mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); 184 mFlywheel = flywheel; 185 186 mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning 187 } 188 189 /** 190 * The amount of friction applied to flings. The default value 191 * is {@link ViewConfiguration#getScrollFriction}. 192 * 193 * @param friction A scalar dimension-less value representing the coefficient of 194 * friction. 195 */ setFriction(float friction)196 public final void setFriction(float friction) { 197 mDeceleration = computeDeceleration(friction); 198 mFlingFriction = friction; 199 } 200 computeDeceleration(float friction)201 private float computeDeceleration(float friction) { 202 return SensorManager.GRAVITY_EARTH // g (m/s^2) 203 * 39.37f // inch/meter 204 * mPpi // pixels per inch 205 * friction; 206 } 207 208 /** 209 * 210 * Returns whether the scroller has finished scrolling. 211 * 212 * @return True if the scroller has finished scrolling, false otherwise. 213 */ isFinished()214 public final boolean isFinished() { 215 return mFinished; 216 } 217 218 /** 219 * Force the finished field to a particular value. 220 * 221 * @param finished The new finished value. 222 */ forceFinished(boolean finished)223 public final void forceFinished(boolean finished) { 224 mFinished = finished; 225 } 226 227 /** 228 * Returns how long the scroll event will take, in milliseconds. 229 * 230 * @return The duration of the scroll in milliseconds. 231 */ getDuration()232 public final int getDuration() { 233 return mDuration; 234 } 235 236 /** 237 * Returns the current X offset in the scroll. 238 * 239 * @return The new X offset as an absolute distance from the origin. 240 */ getCurrX()241 public final int getCurrX() { 242 return mCurrX; 243 } 244 245 /** 246 * Returns the current Y offset in the scroll. 247 * 248 * @return The new Y offset as an absolute distance from the origin. 249 */ getCurrY()250 public final int getCurrY() { 251 return mCurrY; 252 } 253 254 /** 255 * Returns the current velocity. 256 * 257 * @return The original velocity less the deceleration. Result may be 258 * negative. 259 */ getCurrVelocity()260 public float getCurrVelocity() { 261 return mMode == FLING_MODE ? 262 mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f; 263 } 264 265 /** 266 * Returns the start X offset in the scroll. 267 * 268 * @return The start X offset as an absolute distance from the origin. 269 */ getStartX()270 public final int getStartX() { 271 return mStartX; 272 } 273 274 /** 275 * Returns the start Y offset in the scroll. 276 * 277 * @return The start Y offset as an absolute distance from the origin. 278 */ getStartY()279 public final int getStartY() { 280 return mStartY; 281 } 282 283 /** 284 * Returns where the scroll will end. Valid only for "fling" scrolls. 285 * 286 * @return The final X offset as an absolute distance from the origin. 287 */ getFinalX()288 public final int getFinalX() { 289 return mFinalX; 290 } 291 292 /** 293 * Returns where the scroll will end. Valid only for "fling" scrolls. 294 * 295 * @return The final Y offset as an absolute distance from the origin. 296 */ getFinalY()297 public final int getFinalY() { 298 return mFinalY; 299 } 300 301 /** 302 * Call this when you want to know the new location. If it returns true, 303 * the animation is not yet finished. 304 */ computeScrollOffset()305 public boolean computeScrollOffset() { 306 if (mFinished) { 307 return false; 308 } 309 310 int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 311 312 if (timePassed < mDuration) { 313 switch (mMode) { 314 case SCROLL_MODE: 315 float x = timePassed * mDurationReciprocal; 316 317 if (mInterpolator == null) 318 x = viscousFluid(x); 319 else 320 x = mInterpolator.getInterpolation(x); 321 322 mCurrX = mStartX + Math.round(x * mDeltaX); 323 mCurrY = mStartY + Math.round(x * mDeltaY); 324 break; 325 case FLING_MODE: 326 final float t = (float) timePassed / mDuration; 327 final int index = (int) (NB_SAMPLES * t); 328 float distanceCoef = 1.f; 329 float velocityCoef = 0.f; 330 if (index < NB_SAMPLES) { 331 final float t_inf = (float) index / NB_SAMPLES; 332 final float t_sup = (float) (index + 1) / NB_SAMPLES; 333 final float d_inf = SPLINE_POSITION[index]; 334 final float d_sup = SPLINE_POSITION[index + 1]; 335 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); 336 distanceCoef = d_inf + (t - t_inf) * velocityCoef; 337 } 338 339 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; 340 341 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); 342 // Pin to mMinX <= mCurrX <= mMaxX 343 mCurrX = Math.min(mCurrX, mMaxX); 344 mCurrX = Math.max(mCurrX, mMinX); 345 346 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); 347 // Pin to mMinY <= mCurrY <= mMaxY 348 mCurrY = Math.min(mCurrY, mMaxY); 349 mCurrY = Math.max(mCurrY, mMinY); 350 351 if (mCurrX == mFinalX && mCurrY == mFinalY) { 352 mFinished = true; 353 } 354 355 break; 356 } 357 } 358 else { 359 mCurrX = mFinalX; 360 mCurrY = mFinalY; 361 mFinished = true; 362 } 363 return true; 364 } 365 366 /** 367 * Start scrolling by providing a starting point and the distance to travel. 368 * The scroll will use the default value of 250 milliseconds for the 369 * duration. 370 * 371 * @param startX Starting horizontal scroll offset in pixels. Positive 372 * numbers will scroll the content to the left. 373 * @param startY Starting vertical scroll offset in pixels. Positive numbers 374 * will scroll the content up. 375 * @param dx Horizontal distance to travel. Positive numbers will scroll the 376 * content to the left. 377 * @param dy Vertical distance to travel. Positive numbers will scroll the 378 * content up. 379 */ startScroll(int startX, int startY, int dx, int dy)380 public void startScroll(int startX, int startY, int dx, int dy) { 381 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 382 } 383 384 /** 385 * Start scrolling by providing a starting point, the distance to travel, 386 * and the duration of the scroll. 387 * 388 * @param startX Starting horizontal scroll offset in pixels. Positive 389 * numbers will scroll the content to the left. 390 * @param startY Starting vertical scroll offset in pixels. Positive numbers 391 * will scroll the content up. 392 * @param dx Horizontal distance to travel. Positive numbers will scroll the 393 * content to the left. 394 * @param dy Vertical distance to travel. Positive numbers will scroll the 395 * content up. 396 * @param duration Duration of the scroll in milliseconds. 397 */ startScroll(int startX, int startY, int dx, int dy, int duration)398 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 399 mMode = SCROLL_MODE; 400 mFinished = false; 401 mDuration = duration; 402 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 403 mStartX = startX; 404 mStartY = startY; 405 mFinalX = startX + dx; 406 mFinalY = startY + dy; 407 mDeltaX = dx; 408 mDeltaY = dy; 409 mDurationReciprocal = 1.0f / (float) mDuration; 410 } 411 412 /** 413 * Start scrolling based on a fling gesture. The distance travelled will 414 * depend on the initial velocity of the fling. 415 * 416 * @param startX Starting point of the scroll (X) 417 * @param startY Starting point of the scroll (Y) 418 * @param velocityX Initial velocity of the fling (X) measured in pixels per 419 * second. 420 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 421 * second 422 * @param minX Minimum X value. The scroller will not scroll past this 423 * point. 424 * @param maxX Maximum X value. The scroller will not scroll past this 425 * point. 426 * @param minY Minimum Y value. The scroller will not scroll past this 427 * point. 428 * @param maxY Maximum Y value. The scroller will not scroll past this 429 * point. 430 */ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)431 public void fling(int startX, int startY, int velocityX, int velocityY, 432 int minX, int maxX, int minY, int maxY) { 433 // Continue a scroll or fling in progress 434 if (mFlywheel && !mFinished) { 435 float oldVel = getCurrVelocity(); 436 437 float dx = (float) (mFinalX - mStartX); 438 float dy = (float) (mFinalY - mStartY); 439 float hyp = FloatMath.sqrt(dx * dx + dy * dy); 440 441 float ndx = dx / hyp; 442 float ndy = dy / hyp; 443 444 float oldVelocityX = ndx * oldVel; 445 float oldVelocityY = ndy * oldVel; 446 if (Math.signum(velocityX) == Math.signum(oldVelocityX) && 447 Math.signum(velocityY) == Math.signum(oldVelocityY)) { 448 velocityX += oldVelocityX; 449 velocityY += oldVelocityY; 450 } 451 } 452 453 mMode = FLING_MODE; 454 mFinished = false; 455 456 float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); 457 458 mVelocity = velocity; 459 mDuration = getSplineFlingDuration(velocity); 460 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 461 mStartX = startX; 462 mStartY = startY; 463 464 float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; 465 float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; 466 467 double totalDistance = getSplineFlingDistance(velocity); 468 mDistance = (int) (totalDistance * Math.signum(velocity)); 469 470 mMinX = minX; 471 mMaxX = maxX; 472 mMinY = minY; 473 mMaxY = maxY; 474 475 mFinalX = startX + (int) Math.round(totalDistance * coeffX); 476 // Pin to mMinX <= mFinalX <= mMaxX 477 mFinalX = Math.min(mFinalX, mMaxX); 478 mFinalX = Math.max(mFinalX, mMinX); 479 480 mFinalY = startY + (int) Math.round(totalDistance * coeffY); 481 // Pin to mMinY <= mFinalY <= mMaxY 482 mFinalY = Math.min(mFinalY, mMaxY); 483 mFinalY = Math.max(mFinalY, mMinY); 484 } 485 getSplineDeceleration(float velocity)486 private double getSplineDeceleration(float velocity) { 487 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); 488 } 489 getSplineFlingDuration(float velocity)490 private int getSplineFlingDuration(float velocity) { 491 final double l = getSplineDeceleration(velocity); 492 final double decelMinusOne = DECELERATION_RATE - 1.0; 493 return (int) (1000.0 * Math.exp(l / decelMinusOne)); 494 } 495 getSplineFlingDistance(float velocity)496 private double getSplineFlingDistance(float velocity) { 497 final double l = getSplineDeceleration(velocity); 498 final double decelMinusOne = DECELERATION_RATE - 1.0; 499 return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); 500 } 501 viscousFluid(float x)502 static float viscousFluid(float x) 503 { 504 x *= sViscousFluidScale; 505 if (x < 1.0f) { 506 x -= (1.0f - (float)Math.exp(-x)); 507 } else { 508 float start = 0.36787944117f; // 1/e == exp(-1) 509 x = 1.0f - (float)Math.exp(1.0f - x); 510 x = start + x * (1.0f - start); 511 } 512 x *= sViscousFluidNormalize; 513 return x; 514 } 515 516 /** 517 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 518 * aborting the animating cause the scroller to move to the final x and y 519 * position 520 * 521 * @see #forceFinished(boolean) 522 */ abortAnimation()523 public void abortAnimation() { 524 mCurrX = mFinalX; 525 mCurrY = mFinalY; 526 mFinished = true; 527 } 528 529 /** 530 * Extend the scroll animation. This allows a running animation to scroll 531 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 532 * 533 * @param extend Additional time to scroll in milliseconds. 534 * @see #setFinalX(int) 535 * @see #setFinalY(int) 536 */ extendDuration(int extend)537 public void extendDuration(int extend) { 538 int passed = timePassed(); 539 mDuration = passed + extend; 540 mDurationReciprocal = 1.0f / mDuration; 541 mFinished = false; 542 } 543 544 /** 545 * Returns the time elapsed since the beginning of the scrolling. 546 * 547 * @return The elapsed time in milliseconds. 548 */ timePassed()549 public int timePassed() { 550 return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 551 } 552 553 /** 554 * Sets the final position (X) for this scroller. 555 * 556 * @param newX The new X offset as an absolute distance from the origin. 557 * @see #extendDuration(int) 558 * @see #setFinalY(int) 559 */ setFinalX(int newX)560 public void setFinalX(int newX) { 561 mFinalX = newX; 562 mDeltaX = mFinalX - mStartX; 563 mFinished = false; 564 } 565 566 /** 567 * Sets the final position (Y) for this scroller. 568 * 569 * @param newY The new Y offset as an absolute distance from the origin. 570 * @see #extendDuration(int) 571 * @see #setFinalX(int) 572 */ setFinalY(int newY)573 public void setFinalY(int newY) { 574 mFinalY = newY; 575 mDeltaY = mFinalY - mStartY; 576 mFinished = false; 577 } 578 579 /** 580 * @hide 581 */ isScrollingInDirection(float xvel, float yvel)582 public boolean isScrollingInDirection(float xvel, float yvel) { 583 return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && 584 Math.signum(yvel) == Math.signum(mFinalY - mStartY); 585 } 586 } 587