Scroller.java revision 0ee0a2ea57197cb2f03905454098d9a7a309f77b
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 17package android.widget; 18 19 20import android.content.Context; 21import android.hardware.SensorManager; 22import android.util.FloatMath; 23import android.view.ViewConfiguration; 24import android.view.animation.AnimationUtils; 25import android.view.animation.Interpolator; 26 27 28/** 29 * This class encapsulates scrolling. The duration of the scroll 30 * is either specified along with the distance or depends on the initial fling velocity. 31 * Past this time, the scrolling is automatically moved to its final stage and 32 * computeScrollOffset() will always return false to indicate that scrolling is over. 33 */ 34public class Scroller { 35 int mMode; 36 37 MagneticScroller mScrollerX; 38 MagneticScroller mScrollerY; 39 40 private final Interpolator mInterpolator; 41 42 static final int DEFAULT_DURATION = 250; 43 static final int SCROLL_MODE = 0; 44 static final int FLING_MODE = 1; 45 46 // This controls the viscous fluid effect (how much of it) 47 private final static float VISCOUS_FLUID_SCALE = 8.0f; 48 private static float VISCOUS_FLUID_NORMALIZE; 49 50 static { 51 // Set a neutral value that will be used in the next call to viscousFluid(). 52 VISCOUS_FLUID_NORMALIZE = 1.0f; 53 VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f); 54 } 55 56 /** 57 * Create a Scroller with a viscous fluid scroll interpolator. 58 */ 59 public Scroller(Context context) { 60 this(context, null); 61 } 62 63 /** 64 * Create a Scroller with the specified interpolator. If the interpolator is 65 * null, the default (viscous) interpolator will be used. 66 */ 67 public Scroller(Context context, Interpolator interpolator) { 68 instantiateScrollers(); 69 MagneticScroller.initializeFromContext(context); 70 71 mInterpolator = interpolator; 72 } 73 74 void instantiateScrollers() { 75 mScrollerX = new MagneticScroller(); 76 mScrollerY = new MagneticScroller(); 77 } 78 79 /** 80 * 81 * Returns whether the scroller has finished scrolling. 82 * 83 * @return True if the scroller has finished scrolling, false otherwise. 84 */ 85 public final boolean isFinished() { 86 return mScrollerX.mFinished && mScrollerY.mFinished; 87 } 88 89 /** 90 * Force the finished field to a particular value. 91 * 92 * @param finished The new finished value. 93 */ 94 public final void forceFinished(boolean finished) { 95 mScrollerX.mFinished = mScrollerY.mFinished = finished; 96 } 97 98 /** 99 * Returns how long the scroll event will take, in milliseconds. 100 * 101 * @return The duration of the scroll in milliseconds. 102 */ 103 public final int getDuration() { 104 return Math.max(mScrollerX.mDuration, mScrollerY.mDuration); 105 } 106 107 /** 108 * Returns the current X offset in the scroll. 109 * 110 * @return The new X offset as an absolute distance from the origin. 111 */ 112 public final int getCurrX() { 113 return mScrollerX.mCurrentPosition; 114 } 115 116 /** 117 * Returns the current Y offset in the scroll. 118 * 119 * @return The new Y offset as an absolute distance from the origin. 120 */ 121 public final int getCurrY() { 122 return mScrollerY.mCurrentPosition; 123 } 124 125 /** 126 * @hide 127 * Returns the current velocity. 128 * 129 * @return The original velocity less the deceleration, norm of the X and Y velocity vector. 130 */ 131 public float getCurrVelocity() { 132 float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity; 133 squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity; 134 return FloatMath.sqrt(squaredNorm); 135 } 136 137 /** 138 * Returns the start X offset in the scroll. 139 * 140 * @return The start X offset as an absolute distance from the origin. 141 */ 142 public final int getStartX() { 143 return mScrollerX.mStart; 144 } 145 146 /** 147 * Returns the start Y offset in the scroll. 148 * 149 * @return The start Y offset as an absolute distance from the origin. 150 */ 151 public final int getStartY() { 152 return mScrollerY.mStart; 153 } 154 155 /** 156 * Returns where the scroll will end. Valid only for "fling" scrolls. 157 * 158 * @return The final X offset as an absolute distance from the origin. 159 */ 160 public final int getFinalX() { 161 return mScrollerX.mFinal; 162 } 163 164 /** 165 * Returns where the scroll will end. Valid only for "fling" scrolls. 166 * 167 * @return The final Y offset as an absolute distance from the origin. 168 */ 169 public final int getFinalY() { 170 return mScrollerY.mFinal; 171 } 172 173 /** 174 * Call this when you want to know the new location. If it returns true, the 175 * animation is not yet finished. 176 */ 177 public boolean computeScrollOffset() { 178 if (isFinished()) { 179 return false; 180 } 181 182 switch (mMode) { 183 case SCROLL_MODE: 184 long time = AnimationUtils.currentAnimationTimeMillis(); 185 // Any scroller can be used for time, since they were started 186 // together in scroll mode. We use X here. 187 final long elapsedTime = time - mScrollerX.mStartTime; 188 189 final int duration = mScrollerX.mDuration; 190 if (elapsedTime < duration) { 191 float q = (float) (elapsedTime) / duration; 192 193 if (mInterpolator == null) 194 q = viscousFluid(q); 195 else 196 q = mInterpolator.getInterpolation(q); 197 198 mScrollerX.updateScroll(q); 199 mScrollerY.updateScroll(q); 200 } else { 201 abortAnimation(); 202 } 203 break; 204 205 case FLING_MODE: 206 if (!mScrollerX.mFinished) { 207 if (!mScrollerX.update()) { 208 if (!mScrollerX.continueWhenFinished()) { 209 mScrollerX.finish(); 210 } 211 } 212 } 213 214 if (!mScrollerY.mFinished) { 215 if (!mScrollerY.update()) { 216 if (!mScrollerY.continueWhenFinished()) { 217 mScrollerY.finish(); 218 } 219 } 220 } 221 222 break; 223 } 224 225 return true; 226 } 227 228 /** 229 * Start scrolling by providing a starting point and the distance to travel. 230 * The scroll will use the default value of 250 milliseconds for the 231 * duration. 232 * 233 * @param startX Starting horizontal scroll offset in pixels. Positive 234 * numbers will scroll the content to the left. 235 * @param startY Starting vertical scroll offset in pixels. Positive numbers 236 * will scroll the content up. 237 * @param dx Horizontal distance to travel. Positive numbers will scroll the 238 * content to the left. 239 * @param dy Vertical distance to travel. Positive numbers will scroll the 240 * content up. 241 */ 242 public void startScroll(int startX, int startY, int dx, int dy) { 243 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 244 } 245 246 /** 247 * Start scrolling by providing a starting point and the distance to travel. 248 * 249 * @param startX Starting horizontal scroll offset in pixels. Positive 250 * numbers will scroll the content to the left. 251 * @param startY Starting vertical scroll offset in pixels. Positive numbers 252 * will scroll the content up. 253 * @param dx Horizontal distance to travel. Positive numbers will scroll the 254 * content to the left. 255 * @param dy Vertical distance to travel. Positive numbers will scroll the 256 * content up. 257 * @param duration Duration of the scroll in milliseconds. 258 */ 259 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 260 mMode = SCROLL_MODE; 261 mScrollerX.startScroll(startX, dx, duration); 262 mScrollerY.startScroll(startY, dy, duration); 263 } 264 265 /** 266 * Start scrolling based on a fling gesture. The distance traveled will 267 * depend on the initial velocity of the fling. Velocity is slowed down by a 268 * constant deceleration until it reaches 0 or the limits are reached. 269 * 270 * @param startX Starting point of the scroll (X) 271 * @param startY Starting point of the scroll (Y) 272 * @param velocityX Initial velocity of the fling (X) measured in pixels per 273 * second. 274 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 275 * second. 276 * @param minX Minimum X value. The scroller will not scroll past this 277 * point. 278 * @param maxX Maximum X value. The scroller will not scroll past this 279 * point. 280 * @param minY Minimum Y value. The scroller will not scroll past this 281 * point. 282 * @param maxY Maximum Y value. The scroller will not scroll past this 283 * point. 284 */ 285 public void fling(int startX, int startY, int velocityX, int velocityY, 286 int minX, int maxX, int minY, int maxY) { 287 mMode = FLING_MODE; 288 mScrollerX.fling(startX, velocityX, minX, maxX); 289 mScrollerY.fling(startY, velocityY, minY, maxY); 290 } 291 292 private static float viscousFluid(float x) { 293 x *= VISCOUS_FLUID_SCALE; 294 if (x < 1.0f) { 295 x -= (1.0f - (float)Math.exp(-x)); 296 } else { 297 float start = 0.36787944117f; // 1/e == exp(-1) 298 x = 1.0f - (float)Math.exp(1.0f - x); 299 x = start + x * (1.0f - start); 300 } 301 x *= VISCOUS_FLUID_NORMALIZE; 302 return x; 303 } 304 305 /** 306 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 307 * aborting the animating cause the scroller to move to the final x and y 308 * position 309 * 310 * @see #forceFinished(boolean) 311 */ 312 public void abortAnimation() { 313 mScrollerX.finish(); 314 mScrollerY.finish(); 315 } 316 317 /** 318 * Extend the scroll animation. This allows a running animation to scroll 319 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 320 * 321 * @param extend Additional time to scroll in milliseconds. 322 * @see #setFinalX(int) 323 * @see #setFinalY(int) 324 */ 325 public void extendDuration(int extend) { 326 mScrollerX.extendDuration(extend); 327 mScrollerY.extendDuration(extend); 328 } 329 330 /** 331 * Returns the time elapsed since the beginning of the scrolling. 332 * 333 * @return The elapsed time in milliseconds. 334 */ 335 public int timePassed() { 336 final long time = AnimationUtils.currentAnimationTimeMillis(); 337 final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime); 338 return (int) (time - startTime); 339 } 340 341 /** 342 * Sets the final position (X) for this scroller. 343 * 344 * @param newX The new X offset as an absolute distance from the origin. 345 * @see #extendDuration(int) 346 * @see #setFinalY(int) 347 */ 348 public void setFinalX(int newX) { 349 mScrollerX.setFinalPosition(newX); 350 } 351 352 /** 353 * Sets the final position (Y) for this scroller. 354 * 355 * @param newY The new Y offset as an absolute distance from the origin. 356 * @see #extendDuration(int) 357 * @see #setFinalX(int) 358 */ 359 public void setFinalY(int newY) { 360 mScrollerY.setFinalPosition(newY); 361 } 362 363 static class MagneticScroller { 364 // Initial position 365 int mStart; 366 367 // Current position 368 int mCurrentPosition; 369 370 // Final position 371 int mFinal; 372 373 // Initial velocity 374 int mVelocity; 375 376 // Current velocity 377 float mCurrVelocity; 378 379 // Constant current deceleration 380 float mDeceleration; 381 382 // Animation starting time, in system milliseconds 383 long mStartTime; 384 385 // Animation duration, in milliseconds 386 int mDuration; 387 388 // Whether the animation is currently in progress 389 boolean mFinished; 390 391 // Constant gravity value, used to scale deceleration 392 static float GRAVITY; 393 394 static void initializeFromContext(Context context) { 395 final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; 396 GRAVITY = SensorManager.GRAVITY_EARTH // g (m/s^2) 397 * 39.37f // inch/meter 398 * ppi // pixels per inch 399 * ViewConfiguration.getScrollFriction(); 400 } 401 402 MagneticScroller() { 403 mFinished = true; 404 } 405 406 void updateScroll(float q) { 407 mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); 408 } 409 410 /* 411 * Update the current position and velocity for current time. Returns 412 * true if update has been done and false if animation duration has been 413 * reached. 414 */ 415 boolean update() { 416 final long time = AnimationUtils.currentAnimationTimeMillis(); 417 final long duration = time - mStartTime; 418 419 if (duration > mDuration) { 420 return false; 421 } 422 423 final float t = duration / 1000.0f; 424 mCurrVelocity = mVelocity + mDeceleration * t; 425 final float distance = mVelocity * t + mDeceleration * t * t / 2.0f; 426 mCurrentPosition = mStart + (int) distance; 427 428 return true; 429 } 430 431 /* 432 * Get a signed deceleration that will reduce the velocity. 433 */ 434 static float getDeceleration(int velocity) { 435 return velocity > 0 ? -GRAVITY : GRAVITY; 436 } 437 438 /* 439 * Returns the time (in milliseconds) it will take to go from start to end. 440 */ 441 static int computeDuration(int start, int end, float initialVelocity, float deceleration) { 442 final int distance = start - end; 443 final float discriminant = initialVelocity * initialVelocity - 2.0f * deceleration 444 * distance; 445 if (discriminant >= 0.0f) { 446 float delta = (float) Math.sqrt(discriminant); 447 if (deceleration < 0.0f) { 448 delta = -delta; 449 } 450 return (int) (1000.0f * (-initialVelocity - delta) / deceleration); 451 } 452 453 // End position can not be reached 454 return 0; 455 } 456 457 void startScroll(int start, int distance, int duration) { 458 mFinished = false; 459 460 mStart = start; 461 mFinal = start + distance; 462 463 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 464 mDuration = duration; 465 466 // Unused 467 mDeceleration = 0.0f; 468 mVelocity = 0; 469 } 470 471 void fling(int start, int velocity, int min, int max) { 472 mFinished = false; 473 474 mStart = start; 475 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 476 477 mVelocity = velocity; 478 479 mDeceleration = getDeceleration(velocity); 480 481 // A start from an invalid position immediately brings back to a valid position 482 if (mStart < min) { 483 mDuration = 0; 484 mFinal = min; 485 return; 486 } 487 488 if (mStart > max) { 489 mDuration = 0; 490 mFinal = max; 491 return; 492 } 493 494 // Duration are expressed in milliseconds 495 mDuration = (int) (-1000.0f * velocity / mDeceleration); 496 497 mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration)); 498 499 // Clamp to a valid final position 500 if (mFinal < min) { 501 mFinal = min; 502 mDuration = computeDuration(mStart, min, mVelocity, mDeceleration); 503 } 504 505 if (mFinal > max) { 506 mFinal = max; 507 mDuration = computeDuration(mStart, max, mVelocity, mDeceleration); 508 } 509 } 510 511 void finish() { 512 mCurrentPosition = mFinal; 513 // Not reset since WebView relies on this value for fast fling. 514 // mCurrVelocity = 0.0f; 515 mFinished = true; 516 } 517 518 boolean continueWhenFinished() { 519 return false; 520 } 521 522 void setFinalPosition(int position) { 523 mFinal = position; 524 mFinished = false; 525 } 526 527 void extendDuration(int extend) { 528 final long time = AnimationUtils.currentAnimationTimeMillis(); 529 final int elapsedTime = (int) (time - mStartTime); 530 mDuration = elapsedTime + extend; 531 mFinished = false; 532 } 533 } 534} 535