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 com.android.gallery3d.common; 18 19 import android.content.Context; 20 import android.hardware.SensorManager; 21 import android.os.Build; 22 import android.util.FloatMath; 23 import android.view.ViewConfiguration; 24 import android.view.animation.AnimationUtils; 25 import 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 */ 35 public 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 * Returns the current velocity. 200 * 201 * @return The original velocity less the deceleration. Result may be 202 * negative. 203 */ 204 public float getCurrVelocity() { 205 return mVelocity - mDeceleration * timePassed() / 2000.0f; 206 } 207 208 /** 209 * Returns the start X offset in the scroll. 210 * 211 * @return The start X offset as an absolute distance from the origin. 212 */ 213 public final int getStartX() { 214 return mStartX; 215 } 216 217 /** 218 * Returns the start Y offset in the scroll. 219 * 220 * @return The start Y offset as an absolute distance from the origin. 221 */ 222 public final int getStartY() { 223 return mStartY; 224 } 225 226 /** 227 * Returns where the scroll will end. Valid only for "fling" scrolls. 228 * 229 * @return The final X offset as an absolute distance from the origin. 230 */ 231 public final int getFinalX() { 232 return mFinalX; 233 } 234 235 /** 236 * Returns where the scroll will end. Valid only for "fling" scrolls. 237 * 238 * @return The final Y offset as an absolute distance from the origin. 239 */ 240 public final int getFinalY() { 241 return mFinalY; 242 } 243 244 /** 245 * Call this when you want to know the new location. If it returns true, 246 * the animation is not yet finished. loc will be altered to provide the 247 * new location. 248 */ 249 public boolean computeScrollOffset() { 250 if (mFinished) { 251 return false; 252 } 253 254 int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 255 256 if (timePassed < mDuration) { 257 switch (mMode) { 258 case SCROLL_MODE: 259 float x = timePassed * mDurationReciprocal; 260 261 if (mInterpolator == null) 262 x = viscousFluid(x); 263 else 264 x = mInterpolator.getInterpolation(x); 265 266 mCurrX = mStartX + Math.round(x * mDeltaX); 267 mCurrY = mStartY + Math.round(x * mDeltaY); 268 break; 269 case FLING_MODE: 270 final float t = (float) timePassed / mDuration; 271 final int index = (int) (NB_SAMPLES * t); 272 final float t_inf = (float) index / NB_SAMPLES; 273 final float t_sup = (float) (index + 1) / NB_SAMPLES; 274 final float d_inf = SPLINE[index]; 275 final float d_sup = SPLINE[index + 1]; 276 final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf); 277 278 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); 279 // Pin to mMinX <= mCurrX <= mMaxX 280 mCurrX = Math.min(mCurrX, mMaxX); 281 mCurrX = Math.max(mCurrX, mMinX); 282 283 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); 284 // Pin to mMinY <= mCurrY <= mMaxY 285 mCurrY = Math.min(mCurrY, mMaxY); 286 mCurrY = Math.max(mCurrY, mMinY); 287 288 if (mCurrX == mFinalX && mCurrY == mFinalY) { 289 mFinished = true; 290 } 291 292 break; 293 } 294 } 295 else { 296 mCurrX = mFinalX; 297 mCurrY = mFinalY; 298 mFinished = true; 299 } 300 return true; 301 } 302 303 /** 304 * Start scrolling by providing a starting point and the distance to travel. 305 * The scroll will use the default value of 250 milliseconds for the 306 * duration. 307 * 308 * @param startX Starting horizontal scroll offset in pixels. Positive 309 * numbers will scroll the content to the left. 310 * @param startY Starting vertical scroll offset in pixels. Positive numbers 311 * will scroll the content up. 312 * @param dx Horizontal distance to travel. Positive numbers will scroll the 313 * content to the left. 314 * @param dy Vertical distance to travel. Positive numbers will scroll the 315 * content up. 316 */ 317 public void startScroll(int startX, int startY, int dx, int dy) { 318 startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 319 } 320 321 /** 322 * Start scrolling by providing a starting point and the distance to travel. 323 * 324 * @param startX Starting horizontal scroll offset in pixels. Positive 325 * numbers will scroll the content to the left. 326 * @param startY Starting vertical scroll offset in pixels. Positive numbers 327 * will scroll the content up. 328 * @param dx Horizontal distance to travel. Positive numbers will scroll the 329 * content to the left. 330 * @param dy Vertical distance to travel. Positive numbers will scroll the 331 * content up. 332 * @param duration Duration of the scroll in milliseconds. 333 */ 334 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 335 mMode = SCROLL_MODE; 336 mFinished = false; 337 mDuration = duration; 338 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 339 mStartX = startX; 340 mStartY = startY; 341 mFinalX = startX + dx; 342 mFinalY = startY + dy; 343 mDeltaX = dx; 344 mDeltaY = dy; 345 mDurationReciprocal = 1.0f / mDuration; 346 } 347 348 /** 349 * Start scrolling based on a fling gesture. The distance travelled will 350 * depend on the initial velocity of the fling. 351 * 352 * @param startX Starting point of the scroll (X) 353 * @param startY Starting point of the scroll (Y) 354 * @param velocityX Initial velocity of the fling (X) measured in pixels per 355 * second. 356 * @param velocityY Initial velocity of the fling (Y) measured in pixels per 357 * second 358 * @param minX Minimum X value. The scroller will not scroll past this 359 * point. 360 * @param maxX Maximum X value. The scroller will not scroll past this 361 * point. 362 * @param minY Minimum Y value. The scroller will not scroll past this 363 * point. 364 * @param maxY Maximum Y value. The scroller will not scroll past this 365 * point. 366 */ 367 public void fling(int startX, int startY, int velocityX, int velocityY, 368 int minX, int maxX, int minY, int maxY) { 369 // Continue a scroll or fling in progress 370 if (mFlywheel && !mFinished) { 371 float oldVel = getCurrVelocity(); 372 373 float dx = mFinalX - mStartX; 374 float dy = mFinalY - mStartY; 375 float hyp = FloatMath.sqrt(dx * dx + dy * dy); 376 377 float ndx = dx / hyp; 378 float ndy = dy / hyp; 379 380 float oldVelocityX = ndx * oldVel; 381 float oldVelocityY = ndy * oldVel; 382 if (Math.signum(velocityX) == Math.signum(oldVelocityX) && 383 Math.signum(velocityY) == Math.signum(oldVelocityY)) { 384 velocityX += oldVelocityX; 385 velocityY += oldVelocityY; 386 } 387 } 388 389 mMode = FLING_MODE; 390 mFinished = false; 391 392 float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); 393 394 mVelocity = velocity; 395 final double l = Math.log(START_TENSION * velocity / ALPHA); 396 mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0))); 397 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 398 mStartX = startX; 399 mStartY = startY; 400 401 float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; 402 float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; 403 404 int totalDistance = 405 (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)); 406 407 mMinX = minX; 408 mMaxX = maxX; 409 mMinY = minY; 410 mMaxY = maxY; 411 412 mFinalX = startX + Math.round(totalDistance * coeffX); 413 // Pin to mMinX <= mFinalX <= mMaxX 414 mFinalX = Math.min(mFinalX, mMaxX); 415 mFinalX = Math.max(mFinalX, mMinX); 416 417 mFinalY = startY + Math.round(totalDistance * coeffY); 418 // Pin to mMinY <= mFinalY <= mMaxY 419 mFinalY = Math.min(mFinalY, mMaxY); 420 mFinalY = Math.max(mFinalY, mMinY); 421 } 422 423 static float viscousFluid(float x) 424 { 425 x *= sViscousFluidScale; 426 if (x < 1.0f) { 427 x -= (1.0f - (float)Math.exp(-x)); 428 } else { 429 float start = 0.36787944117f; // 1/e == exp(-1) 430 x = 1.0f - (float)Math.exp(1.0f - x); 431 x = start + x * (1.0f - start); 432 } 433 x *= sViscousFluidNormalize; 434 return x; 435 } 436 437 /** 438 * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 439 * aborting the animating cause the scroller to move to the final x and y 440 * position 441 * 442 * @see #forceFinished(boolean) 443 */ 444 public void abortAnimation() { 445 mCurrX = mFinalX; 446 mCurrY = mFinalY; 447 mFinished = true; 448 } 449 450 /** 451 * Extend the scroll animation. This allows a running animation to scroll 452 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 453 * 454 * @param extend Additional time to scroll in milliseconds. 455 * @see #setFinalX(int) 456 * @see #setFinalY(int) 457 */ 458 public void extendDuration(int extend) { 459 int passed = timePassed(); 460 mDuration = passed + extend; 461 mDurationReciprocal = 1.0f / mDuration; 462 mFinished = false; 463 } 464 465 /** 466 * Returns the time elapsed since the beginning of the scrolling. 467 * 468 * @return The elapsed time in milliseconds. 469 */ 470 public int timePassed() { 471 return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 472 } 473 474 /** 475 * Sets the final position (X) for this scroller. 476 * 477 * @param newX The new X offset as an absolute distance from the origin. 478 * @see #extendDuration(int) 479 * @see #setFinalY(int) 480 */ 481 public void setFinalX(int newX) { 482 mFinalX = newX; 483 mDeltaX = mFinalX - mStartX; 484 mFinished = false; 485 } 486 487 /** 488 * Sets the final position (Y) for this scroller. 489 * 490 * @param newY The new Y offset as an absolute distance from the origin. 491 * @see #extendDuration(int) 492 * @see #setFinalX(int) 493 */ 494 public void setFinalY(int newY) { 495 mFinalY = newY; 496 mDeltaY = mFinalY - mStartY; 497 mFinished = false; 498 } 499 500 /** 501 * @hide 502 */ 503 public boolean isScrollingInDirection(float xvel, float yvel) { 504 return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && 505 Math.signum(yvel) == Math.signum(mFinalY - mStartY); 506 } 507 } 508