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