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