Scroller.java revision 04cdb78c6a2ac66505d0b29033353cfd7e2c3d78
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 19import android.content.Context; 20import android.hardware.SensorManager; 21import android.os.Build; 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 * can be passed in the constructor and specifies the maximum time that 31 * the scrolling animation should take. Past this time, the scrolling is 32 * automatically moved to its final stage and computeScrollOffset() 33 * will always return false to indicate that scrolling is over. 34 */ 35public class Scroller { 36 private int mMode; 37 38 private int mStartX; 39 private int mStartY; 40 private int mFinalX; 41 private int mFinalY; 42 43 private int mMinX; 44 private int mMaxX; 45 private int mMinY; 46 private int mMaxY; 47 48 private int mCurrX; 49 private int mCurrY; 50 private long mStartTime; 51 private int mDuration; 52 private float mDurationReciprocal; 53 private float mDeltaX; 54 private float mDeltaY; 55 private float mViscousFluidScale; 56 private float mViscousFluidNormalize; 57 private boolean mFinished; 58 private Interpolator mInterpolator; 59 private boolean mFlywheel; 60 61 private float mCoeffX = 0.0f; 62 private float mCoeffY = 1.0f; 63 private float mVelocity; 64 65 private static final int DEFAULT_DURATION = 250; 66 private static final int SCROLL_MODE = 0; 67 private static final int FLING_MODE = 1; 68 69 private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9)); 70 private static float ALPHA = 800; // pixels / seconds 71 private static float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance) 72 private static float END_TENSION = 1.0f - START_TENSION; 73 private static final int NB_SAMPLES = 100; 74 private static final float[] SPLINE = new float[NB_SAMPLES + 1]; 75 76 private float mDeceleration; 77 private final float mPpi; 78 79 static { 80 float x_min = 0.0f; 81 for (int i = 0; i <= NB_SAMPLES; i++) { 82 final float t = (float) i / NB_SAMPLES; 83 float x_max = 1.0f; 84 float x, tx, coef; 85 while (true) { 86 x = x_min + (x_max - x_min) / 2.0f; 87 coef = 3.0f * x * (1.0f - x); 88 tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x; 89 if (Math.abs(tx - t) < 1E-5) break; 90 if (tx > t) x_max = x; 91 else x_min = x; 92 } 93 final float d = coef + x * x * x; 94 SPLINE[i] = d; 95 } 96 SPLINE[NB_SAMPLES] = 1.0f; 97 } 98 99 /** 100 * Create a Scroller with the default duration and interpolator. 101 */ 102 public Scroller(Context context) { 103 this(context, null); 104 } 105 106 public Scroller(Context context, Interpolator interpolator) { 107 this(context, interpolator, 108 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); 109 } 110 111 /** 112 * Create a Scroller with the specified interpolator. If the interpolator is 113 * null, the default (viscous) interpolator will be used. 114 */ 115 public Scroller(Context context, Interpolator interpolator, boolean flywheel) { 116 mFinished = true; 117 mInterpolator = interpolator; 118 mPpi = context.getResources().getDisplayMetrics().density * 160.0f; 119 mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); 120 mFlywheel = flywheel; 121 } 122 123 /** 124 * The amount of friction applied to flings. The default value 125 * is {@link ViewConfiguration#getScrollFriction}. 126 * 127 * @param friction A scalar dimension-less value representing the coefficient of 128 * friction. 129 */ 130 public final void setFriction(float friction) { 131 mDeceleration = computeDeceleration(friction); 132 } 133 134 private float computeDeceleration(float friction) { 135 return SensorManager.GRAVITY_EARTH // g (m/s^2) 136 * 39.37f // inch/meter 137 * mPpi // pixels per inch 138 * friction; 139 } 140 141 /** 142 * 143 * Returns whether the scroller has finished scrolling. 144 * 145 * @return True if the scroller has finished scrolling, false otherwise. 146 */ 147 public final boolean isFinished() { 148 return mFinished; 149 } 150 151 /** 152 * Force the finished field to a particular value. 153 * 154 * @param finished The new finished value. 155 */ 156 public final void forceFinished(boolean finished) { 157 mFinished = finished; 158 } 159 160 /** 161 * Returns how long the scroll event will take, in milliseconds. 162 * 163 * @return The duration of the scroll in milliseconds. 164 */ 165 public final int getDuration() { 166 return mDuration; 167 } 168 169 /** 170 * Returns the current X offset in the scroll. 171 * 172 * @return The new X offset as an absolute distance from the origin. 173 */ 174 public final int getCurrX() { 175 return mCurrX; 176 } 177 178 /** 179 * Returns the current Y offset in the scroll. 180 * 181 * @return The new Y offset as an absolute distance from the origin. 182 */ 183 public final int getCurrY() { 184 return mCurrY; 185 } 186 187 /** 188 * @hide 189 * Returns the current velocity. 190 * 191 * @return The original velocity less the deceleration. Result may be 192 * negative. 193 */ 194 public float getCurrVelocity() { 195 return mVelocity - mDeceleration * timePassed() / 2000.0f; 196 } 197 198 /** 199 * Returns the start X offset in the scroll. 200 * 201 * @return The start X offset as an absolute distance from the origin. 202 */ 203 public final int getStartX() { 204 return mStartX; 205 } 206 207 /** 208 * Returns the start Y offset in the scroll. 209 * 210 * @return The start Y offset as an absolute distance from the origin. 211 */ 212 public final int getStartY() { 213 return mStartY; 214 } 215 216 /** 217 * Returns where the scroll will end. Valid only for "fling" scrolls. 218 * 219 * @return The final X offset as an absolute distance from the origin. 220 */ 221 public final int getFinalX() { 222 return mFinalX; 223 } 224 225 /** 226 * Returns where the scroll will end. Valid only for "fling" scrolls. 227 * 228 * @return The final Y offset as an absolute distance from the origin. 229 */ 230 public final int getFinalY() { 231 return mFinalY; 232 } 233 234 /** 235 * Call this when you want to know the new location. If it returns true, 236 * the animation is not yet finished. loc will be altered to provide the 237 * new location. 238 */ 239 public boolean computeScrollOffset() { 240 if (mFinished) { 241 return false; 242 } 243 244 int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 245 246 if (timePassed < mDuration) { 247 switch (mMode) { 248 case SCROLL_MODE: 249 float x = timePassed * mDurationReciprocal; 250 251 if (mInterpolator == null) 252 x = viscousFluid(x); 253 else 254 x = mInterpolator.getInterpolation(x); 255 256 mCurrX = mStartX + Math.round(x * mDeltaX); 257 mCurrY = mStartY + Math.round(x * mDeltaY); 258 break; 259 case FLING_MODE: 260 final float t = (float) timePassed / mDuration; 261 final int index = (int) (NB_SAMPLES * t); 262 final float t_inf = (float) index / NB_SAMPLES; 263 final float t_sup = (float) (index + 1) / NB_SAMPLES; 264 final float d_inf = SPLINE[index]; 265 final float d_sup = SPLINE[index + 1]; 266 final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf); 267 268 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); 269 // Pin to mMinX <= mCurrX <= mMaxX 270 mCurrX = Math.min(mCurrX, mMaxX); 271 mCurrX = Math.max(mCurrX, mMinX); 272 273 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); 274 // Pin to mMinY <= mCurrY <= mMaxY 275 mCurrY = Math.min(mCurrY, mMaxY); 276 mCurrY = Math.max(mCurrY, mMinY); 277 278 if (mCurrX == mFinalX && mCurrY == mFinalY) { 279 mFinished = true; 280 } 281 282 break; 283 } 284 } 285 else { 286 mCurrX = mFinalX; 287 mCurrY = mFinalY; 288 mFinished = true; 289 } 290 return true; 291 } 292 293 /** 294 * Start scrolling by providing a starting point and the distance to travel. 295 * The scroll will use the default value of 250 milliseconds for the 296 * duration. 297 * 298 * @param startX Starting horizontal scroll offset in pixels. Positive 299 * numbers will scroll the content to the left. 300 * @param startY Starting vertical scroll offset in pixels. Positive numbers 301 * will scroll the content up. 302 * @param dx Horizontal distance to travel. Positive numbers will scroll the 303 * content to the left. 304 * @param dy Vertical distance to travel. Positive numbers will scroll the 305 * content up. 306 */ 307 public void startScroll(int startX, int startY, int dx, int dy) { 308 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 309 } 310 311 /** 312 * Start scrolling by providing a starting point and the distance to travel. 313 * 314 * @param startX Starting horizontal scroll offset in pixels. Positive 315 * numbers will scroll the content to the left. 316 * @param startY Starting vertical scroll offset in pixels. Positive numbers 317 * will scroll the content up. 318 * @param dx Horizontal distance to travel. Positive numbers will scroll the 319 * content to the left. 320 * @param dy Vertical distance to travel. Positive numbers will scroll the 321 * content up. 322 * @param duration Duration of the scroll in milliseconds. 323 */ 324 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 325 mMode = SCROLL_MODE; 326 mFinished = false; 327 mDuration = duration; 328 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 329 mStartX = startX; 330 mStartY = startY; 331 mFinalX = startX + dx; 332 mFinalY = startY + dy; 333 mDeltaX = dx; 334 mDeltaY = dy; 335 mDurationReciprocal = 1.0f / mDuration; 336 // This controls the viscous fluid effect (how much of it) 337 mViscousFluidScale = 8.0f; 338 // must be set to 1.0 (used in viscousFluid()) 339 mViscousFluidNormalize = 1.0f; 340 mViscousFluidNormalize = 1.0f / viscousFluid(1.0f); 341 } 342 343 /** 344 * Start scrolling based on a fling gesture. The distance travelled will 345 * depend on the initial velocity of the fling. 346 * 347 * @param startX Starting point of the scroll (X) 348 * @param startY Starting point of the scroll (Y) 349 * @param velocityX Initial velocity of the fling (X) measured in pixels per 350 * second. 351 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 352 * second 353 * @param minX Minimum X value. The scroller will not scroll past this 354 * point. 355 * @param maxX Maximum X value. The scroller will not scroll past this 356 * point. 357 * @param minY Minimum Y value. The scroller will not scroll past this 358 * point. 359 * @param maxY Maximum Y value. The scroller will not scroll past this 360 * point. 361 */ 362 public void fling(int startX, int startY, int velocityX, int velocityY, 363 int minX, int maxX, int minY, int maxY) { 364 // Continue a scroll or fling in progress 365 if (mFlywheel && !mFinished) { 366 float oldVel = getCurrVelocity(); 367 368 float dx = (float) (mFinalX - mStartX); 369 float dy = (float) (mFinalY - mStartY); 370 float hyp = FloatMath.sqrt(dx * dx + dy * dy); 371 372 float ndx = dx / hyp; 373 float ndy = dy / hyp; 374 375 float oldVelocityX = ndx * oldVel; 376 float oldVelocityY = ndy * oldVel; 377 if (Math.signum(velocityX) == Math.signum(oldVelocityX) && 378 Math.signum(velocityY) == Math.signum(oldVelocityY)) { 379 velocityX += oldVelocityX; 380 velocityY += oldVelocityY; 381 } 382 } 383 384 mMode = FLING_MODE; 385 mFinished = false; 386 387 float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); 388 389 mVelocity = velocity; 390 final double l = Math.log(START_TENSION * velocity / ALPHA); 391 mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0))); 392 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 393 mStartX = startX; 394 mStartY = startY; 395 396 mCoeffX = velocity == 0 ? 1.0f : velocityX / velocity; 397 mCoeffY = velocity == 0 ? 1.0f : velocityY / velocity; 398 399 int totalDistance = 400 (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)); 401 402 mMinX = minX; 403 mMaxX = maxX; 404 mMinY = minY; 405 mMaxY = maxY; 406 407 mFinalX = startX + Math.round(totalDistance * mCoeffX); 408 // Pin to mMinX <= mFinalX <= mMaxX 409 mFinalX = Math.min(mFinalX, mMaxX); 410 mFinalX = Math.max(mFinalX, mMinX); 411 412 mFinalY = startY + Math.round(totalDistance * mCoeffY); 413 // Pin to mMinY <= mFinalY <= mMaxY 414 mFinalY = Math.min(mFinalY, mMaxY); 415 mFinalY = Math.max(mFinalY, mMinY); 416 } 417 418 419 420 private float viscousFluid(float x) 421 { 422 x *= mViscousFluidScale; 423 if (x < 1.0f) { 424 x -= (1.0f - (float)Math.exp(-x)); 425 } else { 426 float start = 0.36787944117f; // 1/e == exp(-1) 427 x = 1.0f - (float)Math.exp(1.0f - x); 428 x = start + x * (1.0f - start); 429 } 430 x *= mViscousFluidNormalize; 431 return x; 432 } 433 434 /** 435 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 436 * aborting the animating cause the scroller to move to the final x and y 437 * position 438 * 439 * @see #forceFinished(boolean) 440 */ 441 public void abortAnimation() { 442 mCurrX = mFinalX; 443 mCurrY = mFinalY; 444 mFinished = true; 445 } 446 447 /** 448 * Extend the scroll animation. This allows a running animation to scroll 449 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 450 * 451 * @param extend Additional time to scroll in milliseconds. 452 * @see #setFinalX(int) 453 * @see #setFinalY(int) 454 */ 455 public void extendDuration(int extend) { 456 int passed = timePassed(); 457 mDuration = passed + extend; 458 mDurationReciprocal = 1.0f / mDuration; 459 mFinished = false; 460 } 461 462 /** 463 * Returns the time elapsed since the beginning of the scrolling. 464 * 465 * @return The elapsed time in milliseconds. 466 */ 467 public int timePassed() { 468 return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 469 } 470 471 /** 472 * Sets the final position (X) for this scroller. 473 * 474 * @param newX The new X offset as an absolute distance from the origin. 475 * @see #extendDuration(int) 476 * @see #setFinalY(int) 477 */ 478 public void setFinalX(int newX) { 479 mFinalX = newX; 480 mDeltaX = mFinalX - mStartX; 481 mFinished = false; 482 } 483 484 /** 485 * Sets the final position (Y) for this scroller. 486 * 487 * @param newY The new Y offset as an absolute distance from the origin. 488 * @see #extendDuration(int) 489 * @see #setFinalX(int) 490 */ 491 public void setFinalY(int newY) { 492 mFinalY = newY; 493 mDeltaY = mFinalY - mStartY; 494 mFinished = false; 495 } 496 497 /** 498 * @hide 499 */ 500 public boolean isScrollingInDirection(float xvel, float yvel) { 501 return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && 502 Math.signum(yvel) == Math.signum(mFinalY - mStartY); 503 } 504} 505