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