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 */ Scroller(Context context)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 */ Scroller(Context context, Interpolator interpolator)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 */ Scroller(Context context, Interpolator interpolator, boolean flywheel)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 */ setFriction(float friction)141 public final void setFriction(float friction) { 142 mDeceleration = computeDeceleration(friction); 143 } 144 computeDeceleration(float friction)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 */ isFinished()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 */ forceFinished(boolean finished)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 */ getDuration()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 */ getCurrX()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 */ getCurrY()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 */ getCurrVelocity()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 */ getStartX()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 */ getStartY()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 */ getFinalX()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 */ getFinalY()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 */ computeScrollOffset()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 */ startScroll(int startX, int startY, int dx, int dy)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 */ startScroll(int startX, int startY, int dx, int dy, int duration)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 */ fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)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 viscousFluid(float x)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 */ abortAnimation()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 */ extendDuration(int extend)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 */ timePassed()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 */ setFinalX(int newX)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 */ setFinalY(int newY)494 public void setFinalY(int newY) { 495 mFinalY = newY; 496 mDeltaY = mFinalY - mStartY; 497 mFinished = false; 498 } 499 500 /** 501 * @hide 502 */ isScrollingInDirection(float xvel, float yvel)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