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 */ 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 */ 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 */ 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 */ 196 public final void setFriction(float friction) { 197 mDeceleration = computeDeceleration(friction); 198 mFlingFriction = friction; 199 } 200 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 486 private double getSplineDeceleration(float velocity) { 487 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); 488 } 489 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 573 public void setFinalY(int newY) { 574 mFinalY = newY; 575 mDeltaY = mFinalY - mStartY; 576 mFinished = false; 577 } 578 579 /** 580 * @hide 581 */ 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