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