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