blob: f2181991b4c45dc41ab432b2ed570033ea520096 [file] [log] [blame]
Adam Powell637d3372010-08-25 14:37:03 -07001/*
2 * Copyright (C) 2010 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;
Gilles Debunnefff4ab02010-11-23 15:34:37 -080020import android.hardware.SensorManager;
21import android.util.FloatMath;
22import android.util.Log;
Adam Powell637d3372010-08-25 14:37:03 -070023import android.view.ViewConfiguration;
24import android.view.animation.AnimationUtils;
Gilles Debunnefff4ab02010-11-23 15:34:37 -080025import android.view.animation.Interpolator;
Adam Powell637d3372010-08-25 14:37:03 -070026
27/**
28 * This class encapsulates scrolling with the ability to overshoot the bounds
29 * of a scrolling operation. This class is a drop-in replacement for
30 * {@link android.widget.Scroller} in most cases.
31 */
32public class OverScroller {
Gilles Debunnefff4ab02010-11-23 15:34:37 -080033 private int mMode;
Adam Powell637d3372010-08-25 14:37:03 -070034
Gilles Debunnefff4ab02010-11-23 15:34:37 -080035 private final SplineOverScroller mScrollerX;
36 private final SplineOverScroller mScrollerY;
Adam Powell637d3372010-08-25 14:37:03 -070037
Adam Powell0b8acd82012-04-25 20:29:23 -070038 private Interpolator mInterpolator;
Gilles Debunnefff4ab02010-11-23 15:34:37 -080039
Adam Powell637d3372010-08-25 14:37:03 -070040 private final boolean mFlywheel;
41
Adam Powell637d3372010-08-25 14:37:03 -070042 private static final int DEFAULT_DURATION = 250;
43 private static final int SCROLL_MODE = 0;
44 private static final int FLING_MODE = 1;
45
Gilles Debunnefff4ab02010-11-23 15:34:37 -080046 /**
Gilles Debunnec35a8292010-12-10 14:36:17 -080047 * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel.
Gilles Debunnefff4ab02010-11-23 15:34:37 -080048 * @param context
49 */
50 public OverScroller(Context context) {
51 this(context, null);
Adam Powell637d3372010-08-25 14:37:03 -070052 }
53
Gilles Debunnefff4ab02010-11-23 15:34:37 -080054 /**
Gilles Debunnec35a8292010-12-10 14:36:17 -080055 * Creates an OverScroller with flywheel enabled.
Gilles Debunnefff4ab02010-11-23 15:34:37 -080056 * @param context The context of this application.
57 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
58 * be used.
59 */
60 public OverScroller(Context context, Interpolator interpolator) {
Gilles Debunnec35a8292010-12-10 14:36:17 -080061 this(context, interpolator, true);
62 }
63
64 /**
65 * Creates an OverScroller.
66 * @param context The context of this application.
67 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
68 * be used.
69 * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
Wink Saville086c0612010-12-11 09:01:31 -080070 * @hide
Gilles Debunnec35a8292010-12-10 14:36:17 -080071 */
72 public OverScroller(Context context, Interpolator interpolator, boolean flywheel) {
73 mInterpolator = interpolator;
74 mFlywheel = flywheel;
Adam Powell990dfc6f2012-08-09 16:45:59 -070075 mScrollerX = new SplineOverScroller(context);
76 mScrollerY = new SplineOverScroller(context);
Gilles Debunnefff4ab02010-11-23 15:34:37 -080077 }
78
79 /**
80 * Creates an OverScroller with flywheel enabled.
81 * @param context The context of this application.
82 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
83 * be used.
84 * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
85 * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
Gilles Debunnec35a8292010-12-10 14:36:17 -080086 * means no bounce. This behavior is no longer supported and this coefficient has no effect.
87 * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
88 * behavior is no longer supported and this coefficient has no effect.
Wink Saville086c0612010-12-11 09:01:31 -080089 * !deprecated Use {!link #OverScroller(Context, Interpolator, boolean)} instead.
Gilles Debunnefff4ab02010-11-23 15:34:37 -080090 */
91 public OverScroller(Context context, Interpolator interpolator,
92 float bounceCoefficientX, float bounceCoefficientY) {
Gilles Debunnec35a8292010-12-10 14:36:17 -080093 this(context, interpolator, true);
Adam Powell637d3372010-08-25 14:37:03 -070094 }
95
96 /**
97 * Creates an OverScroller.
98 * @param context The context of this application.
99 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
100 * be used.
101 * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
102 * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
Gilles Debunnec35a8292010-12-10 14:36:17 -0800103 * means no bounce. This behavior is no longer supported and this coefficient has no effect.
104 * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
105 * behavior is no longer supported and this coefficient has no effect.
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800106 * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
Wink Saville086c0612010-12-11 09:01:31 -0800107 * !deprecated Use {!link OverScroller(Context, Interpolator, boolean)} instead.
Adam Powell637d3372010-08-25 14:37:03 -0700108 */
109 public OverScroller(Context context, Interpolator interpolator,
110 float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
Gilles Debunnec35a8292010-12-10 14:36:17 -0800111 this(context, interpolator, flywheel);
Adam Powell637d3372010-08-25 14:37:03 -0700112 }
113
Adam Powell0b8acd82012-04-25 20:29:23 -0700114 void setInterpolator(Interpolator interpolator) {
115 mInterpolator = interpolator;
116 }
117
Adam Powell637d3372010-08-25 14:37:03 -0700118 /**
119 * The amount of friction applied to flings. The default value
120 * is {@link ViewConfiguration#getScrollFriction}.
121 *
122 * @param friction A scalar dimension-less value representing the coefficient of
123 * friction.
124 */
125 public final void setFriction(float friction) {
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800126 mScrollerX.setFriction(friction);
127 mScrollerY.setFriction(friction);
Adam Powell637d3372010-08-25 14:37:03 -0700128 }
129
130 /**
131 *
132 * Returns whether the scroller has finished scrolling.
133 *
134 * @return True if the scroller has finished scrolling, false otherwise.
135 */
136 public final boolean isFinished() {
137 return mScrollerX.mFinished && mScrollerY.mFinished;
138 }
139
140 /**
141 * Force the finished field to a particular value. Contrary to
142 * {@link #abortAnimation()}, forcing the animation to finished
143 * does NOT cause the scroller to move to the final x and y
144 * position.
145 *
146 * @param finished The new finished value.
147 */
148 public final void forceFinished(boolean finished) {
149 mScrollerX.mFinished = mScrollerY.mFinished = finished;
150 }
151
152 /**
153 * Returns the current X offset in the scroll.
154 *
155 * @return The new X offset as an absolute distance from the origin.
156 */
157 public final int getCurrX() {
158 return mScrollerX.mCurrentPosition;
159 }
160
161 /**
162 * Returns the current Y offset in the scroll.
163 *
164 * @return The new Y offset as an absolute distance from the origin.
165 */
166 public final int getCurrY() {
167 return mScrollerY.mCurrentPosition;
168 }
169
170 /**
Gilles Debunnec35a8292010-12-10 14:36:17 -0800171 * Returns the absolute value of the current velocity.
Adam Powell637d3372010-08-25 14:37:03 -0700172 *
173 * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
174 */
175 public float getCurrVelocity() {
176 float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity;
177 squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity;
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800178 return FloatMath.sqrt(squaredNorm);
Adam Powell637d3372010-08-25 14:37:03 -0700179 }
180
181 /**
182 * Returns the start X offset in the scroll.
183 *
184 * @return The start X offset as an absolute distance from the origin.
185 */
186 public final int getStartX() {
187 return mScrollerX.mStart;
188 }
189
190 /**
191 * Returns the start Y offset in the scroll.
192 *
193 * @return The start Y offset as an absolute distance from the origin.
194 */
195 public final int getStartY() {
196 return mScrollerY.mStart;
197 }
198
199 /**
200 * Returns where the scroll will end. Valid only for "fling" scrolls.
201 *
202 * @return The final X offset as an absolute distance from the origin.
203 */
204 public final int getFinalX() {
205 return mScrollerX.mFinal;
206 }
207
208 /**
209 * Returns where the scroll will end. Valid only for "fling" scrolls.
210 *
211 * @return The final Y offset as an absolute distance from the origin.
212 */
213 public final int getFinalY() {
214 return mScrollerY.mFinal;
215 }
216
217 /**
218 * Returns how long the scroll event will take, in milliseconds.
219 *
220 * @return The duration of the scroll in milliseconds.
221 *
222 * @hide Pending removal once nothing depends on it
223 * @deprecated OverScrollers don't necessarily have a fixed duration.
224 * This function will lie to the best of its ability.
225 */
226 @Deprecated
227 public final int getDuration() {
228 return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
229 }
230
231 /**
232 * Extend the scroll animation. This allows a running animation to scroll
233 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
234 *
235 * @param extend Additional time to scroll in milliseconds.
236 * @see #setFinalX(int)
237 * @see #setFinalY(int)
238 *
239 * @hide Pending removal once nothing depends on it
240 * @deprecated OverScrollers don't necessarily have a fixed duration.
241 * Instead of setting a new final position and extending
242 * the duration of an existing scroll, use startScroll
243 * to begin a new animation.
244 */
245 @Deprecated
246 public void extendDuration(int extend) {
247 mScrollerX.extendDuration(extend);
248 mScrollerY.extendDuration(extend);
249 }
250
251 /**
252 * Sets the final position (X) for this scroller.
253 *
254 * @param newX The new X offset as an absolute distance from the origin.
255 * @see #extendDuration(int)
256 * @see #setFinalY(int)
257 *
258 * @hide Pending removal once nothing depends on it
259 * @deprecated OverScroller's final position may change during an animation.
260 * Instead of setting a new final position and extending
261 * the duration of an existing scroll, use startScroll
262 * to begin a new animation.
263 */
264 @Deprecated
265 public void setFinalX(int newX) {
266 mScrollerX.setFinalPosition(newX);
267 }
268
269 /**
270 * Sets the final position (Y) for this scroller.
271 *
272 * @param newY The new Y offset as an absolute distance from the origin.
273 * @see #extendDuration(int)
274 * @see #setFinalX(int)
275 *
276 * @hide Pending removal once nothing depends on it
277 * @deprecated OverScroller's final position may change during an animation.
278 * Instead of setting a new final position and extending
279 * the duration of an existing scroll, use startScroll
280 * to begin a new animation.
281 */
282 @Deprecated
283 public void setFinalY(int newY) {
284 mScrollerY.setFinalPosition(newY);
285 }
286
287 /**
288 * Call this when you want to know the new location. If it returns true, the
289 * animation is not yet finished.
290 */
291 public boolean computeScrollOffset() {
292 if (isFinished()) {
293 return false;
294 }
295
296 switch (mMode) {
297 case SCROLL_MODE:
298 long time = AnimationUtils.currentAnimationTimeMillis();
299 // Any scroller can be used for time, since they were started
300 // together in scroll mode. We use X here.
301 final long elapsedTime = time - mScrollerX.mStartTime;
302
303 final int duration = mScrollerX.mDuration;
304 if (elapsedTime < duration) {
305 float q = (float) (elapsedTime) / duration;
306
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800307 if (mInterpolator == null) {
308 q = Scroller.viscousFluid(q);
309 } else {
310 q = mInterpolator.getInterpolation(q);
311 }
Adam Powell637d3372010-08-25 14:37:03 -0700312
313 mScrollerX.updateScroll(q);
314 mScrollerY.updateScroll(q);
315 } else {
316 abortAnimation();
317 }
318 break;
319
320 case FLING_MODE:
321 if (!mScrollerX.mFinished) {
322 if (!mScrollerX.update()) {
323 if (!mScrollerX.continueWhenFinished()) {
324 mScrollerX.finish();
325 }
326 }
327 }
328
329 if (!mScrollerY.mFinished) {
330 if (!mScrollerY.update()) {
331 if (!mScrollerY.continueWhenFinished()) {
332 mScrollerY.finish();
333 }
334 }
335 }
336
337 break;
338 }
339
340 return true;
341 }
342
343 /**
344 * Start scrolling by providing a starting point and the distance to travel.
345 * The scroll will use the default value of 250 milliseconds for the
346 * duration.
347 *
348 * @param startX Starting horizontal scroll offset in pixels. Positive
349 * numbers will scroll the content to the left.
350 * @param startY Starting vertical scroll offset in pixels. Positive numbers
351 * will scroll the content up.
352 * @param dx Horizontal distance to travel. Positive numbers will scroll the
353 * content to the left.
354 * @param dy Vertical distance to travel. Positive numbers will scroll the
355 * content up.
356 */
357 public void startScroll(int startX, int startY, int dx, int dy) {
358 startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
359 }
360
361 /**
362 * Start scrolling by providing a starting point and the distance to travel.
363 *
364 * @param startX Starting horizontal scroll offset in pixels. Positive
365 * numbers will scroll the content to the left.
366 * @param startY Starting vertical scroll offset in pixels. Positive numbers
367 * will scroll the content up.
368 * @param dx Horizontal distance to travel. Positive numbers will scroll the
369 * content to the left.
370 * @param dy Vertical distance to travel. Positive numbers will scroll the
371 * content up.
372 * @param duration Duration of the scroll in milliseconds.
373 */
374 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
375 mMode = SCROLL_MODE;
376 mScrollerX.startScroll(startX, dx, duration);
377 mScrollerY.startScroll(startY, dy, duration);
378 }
379
380 /**
381 * Call this when you want to 'spring back' into a valid coordinate range.
382 *
383 * @param startX Starting X coordinate
384 * @param startY Starting Y coordinate
385 * @param minX Minimum valid X value
386 * @param maxX Maximum valid X value
387 * @param minY Minimum valid Y value
388 * @param maxY Minimum valid Y value
389 * @return true if a springback was initiated, false if startX and startY were
390 * already within the valid range.
391 */
392 public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
393 mMode = FLING_MODE;
394
395 // Make sure both methods are called.
396 final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
397 final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
398 return spingbackX || spingbackY;
399 }
400
401 public void fling(int startX, int startY, int velocityX, int velocityY,
402 int minX, int maxX, int minY, int maxY) {
403 fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
404 }
405
406 /**
407 * Start scrolling based on a fling gesture. The distance traveled will
408 * depend on the initial velocity of the fling.
409 *
410 * @param startX Starting point of the scroll (X)
411 * @param startY Starting point of the scroll (Y)
412 * @param velocityX Initial velocity of the fling (X) measured in pixels per
413 * second.
414 * @param velocityY Initial velocity of the fling (Y) measured in pixels per
415 * second
416 * @param minX Minimum X value. The scroller will not scroll past this point
417 * unless overX > 0. If overfling is allowed, it will use minX as
418 * a springback boundary.
419 * @param maxX Maximum X value. The scroller will not scroll past this point
420 * unless overX > 0. If overfling is allowed, it will use maxX as
421 * a springback boundary.
422 * @param minY Minimum Y value. The scroller will not scroll past this point
423 * unless overY > 0. If overfling is allowed, it will use minY as
424 * a springback boundary.
425 * @param maxY Maximum Y value. The scroller will not scroll past this point
426 * unless overY > 0. If overfling is allowed, it will use maxY as
427 * a springback boundary.
428 * @param overX Overfling range. If > 0, horizontal overfling in either
429 * direction will be possible.
430 * @param overY Overfling range. If > 0, vertical overfling in either
431 * direction will be possible.
432 */
433 public void fling(int startX, int startY, int velocityX, int velocityY,
434 int minX, int maxX, int minY, int maxY, int overX, int overY) {
435 // Continue a scroll or fling in progress
436 if (mFlywheel && !isFinished()) {
437 float oldVelocityX = mScrollerX.mCurrVelocity;
438 float oldVelocityY = mScrollerY.mCurrVelocity;
439 if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
440 Math.signum(velocityY) == Math.signum(oldVelocityY)) {
441 velocityX += oldVelocityX;
442 velocityY += oldVelocityY;
443 }
444 }
445
446 mMode = FLING_MODE;
447 mScrollerX.fling(startX, velocityX, minX, maxX, overX);
448 mScrollerY.fling(startY, velocityY, minY, maxY, overY);
449 }
450
451 /**
452 * Notify the scroller that we've reached a horizontal boundary.
453 * Normally the information to handle this will already be known
454 * when the animation is started, such as in a call to one of the
455 * fling functions. However there are cases where this cannot be known
456 * in advance. This function will transition the current motion and
457 * animate from startX to finalX as appropriate.
458 *
459 * @param startX Starting/current X position
460 * @param finalX Desired final X position
461 * @param overX Magnitude of overscroll allowed. This should be the maximum
462 * desired distance from finalX. Absolute value - must be positive.
463 */
464 public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
465 mScrollerX.notifyEdgeReached(startX, finalX, overX);
466 }
467
468 /**
469 * Notify the scroller that we've reached a vertical boundary.
470 * Normally the information to handle this will already be known
471 * when the animation is started, such as in a call to one of the
472 * fling functions. However there are cases where this cannot be known
473 * in advance. This function will animate a parabolic motion from
474 * startY to finalY.
475 *
476 * @param startY Starting/current Y position
477 * @param finalY Desired final Y position
478 * @param overY Magnitude of overscroll allowed. This should be the maximum
479 * desired distance from finalY. Absolute value - must be positive.
480 */
481 public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
482 mScrollerY.notifyEdgeReached(startY, finalY, overY);
483 }
484
485 /**
486 * Returns whether the current Scroller is currently returning to a valid position.
487 * Valid bounds were provided by the
488 * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
489 *
490 * One should check this value before calling
491 * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
492 * to restore a valid position will then be stopped. The caller has to take into account
493 * the fact that the started scroll will start from an overscrolled position.
494 *
495 * @return true when the current position is overscrolled and in the process of
496 * interpolating back to a valid value.
497 */
498 public boolean isOverScrolled() {
499 return ((!mScrollerX.mFinished &&
Gilles Debunnec35a8292010-12-10 14:36:17 -0800500 mScrollerX.mState != SplineOverScroller.SPLINE) ||
Adam Powell637d3372010-08-25 14:37:03 -0700501 (!mScrollerY.mFinished &&
Gilles Debunnec35a8292010-12-10 14:36:17 -0800502 mScrollerY.mState != SplineOverScroller.SPLINE));
Adam Powell637d3372010-08-25 14:37:03 -0700503 }
504
505 /**
506 * Stops the animation. Contrary to {@link #forceFinished(boolean)},
507 * aborting the animating causes the scroller to move to the final x and y
508 * positions.
509 *
510 * @see #forceFinished(boolean)
511 */
512 public void abortAnimation() {
513 mScrollerX.finish();
514 mScrollerY.finish();
515 }
516
517 /**
518 * Returns the time elapsed since the beginning of the scrolling.
519 *
520 * @return The elapsed time in milliseconds.
521 *
522 * @hide
523 */
524 public int timePassed() {
525 final long time = AnimationUtils.currentAnimationTimeMillis();
526 final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
527 return (int) (time - startTime);
528 }
529
530 /**
531 * @hide
532 */
533 public boolean isScrollingInDirection(float xvel, float yvel) {
534 final int dx = mScrollerX.mFinal - mScrollerX.mStart;
535 final int dy = mScrollerY.mFinal - mScrollerY.mStart;
536 return !isFinished() && Math.signum(xvel) == Math.signum(dx) &&
Jeff Brownb0c71eb2011-09-16 21:40:49 -0700537 Math.signum(yvel) == Math.signum(dy);
Adam Powell637d3372010-08-25 14:37:03 -0700538 }
539
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800540 static class SplineOverScroller {
Adam Powell637d3372010-08-25 14:37:03 -0700541 // Initial position
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800542 private int mStart;
Adam Powell637d3372010-08-25 14:37:03 -0700543
544 // Current position
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800545 private int mCurrentPosition;
Adam Powell637d3372010-08-25 14:37:03 -0700546
547 // Final position
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800548 private int mFinal;
Adam Powell637d3372010-08-25 14:37:03 -0700549
550 // Initial velocity
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800551 private int mVelocity;
Adam Powell637d3372010-08-25 14:37:03 -0700552
553 // Current velocity
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800554 private float mCurrVelocity;
Adam Powell637d3372010-08-25 14:37:03 -0700555
556 // Constant current deceleration
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800557 private float mDeceleration;
Adam Powell637d3372010-08-25 14:37:03 -0700558
559 // Animation starting time, in system milliseconds
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800560 private long mStartTime;
Adam Powell637d3372010-08-25 14:37:03 -0700561
562 // Animation duration, in milliseconds
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800563 private int mDuration;
Adam Powell637d3372010-08-25 14:37:03 -0700564
565 // Duration to complete spline component of animation
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800566 private int mSplineDuration;
Adam Powell637d3372010-08-25 14:37:03 -0700567
568 // Distance to travel along spline animation
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800569 private int mSplineDistance;
Adam Powell637d3372010-08-25 14:37:03 -0700570
571 // Whether the animation is currently in progress
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800572 private boolean mFinished;
573
574 // The allowed overshot distance before boundary is reached.
575 private int mOver;
576
577 // Fling friction
578 private float mFlingFriction = ViewConfiguration.getScrollFriction();
579
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800580 // Current state of the animation.
Gilles Debunnec35a8292010-12-10 14:36:17 -0800581 private int mState = SPLINE;
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800582
583 // Constant gravity value, used in the deceleration phase.
584 private static final float GRAVITY = 2000.0f;
585
Adam Powell990dfc6f2012-08-09 16:45:59 -0700586 // A context-specific coefficient adjusted to physical values.
587 private float mPhysicalCoeff;
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800588
Justin Hoa181bb42011-09-19 17:52:06 -0700589 private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
590 private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
591 private static final float START_TENSION = 0.5f;
592 private static final float END_TENSION = 1.0f;
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800593 private static final float P1 = START_TENSION * INFLEXION;
594 private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
595
596 private static final int NB_SAMPLES = 100;
597 private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
598 private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
Adam Powell637d3372010-08-25 14:37:03 -0700599
Gilles Debunnec35a8292010-12-10 14:36:17 -0800600 private static final int SPLINE = 0;
601 private static final int CUBIC = 1;
602 private static final int BALLISTIC = 2;
Adam Powell637d3372010-08-25 14:37:03 -0700603
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800604 static {
605 float x_min = 0.0f;
606 float y_min = 0.0f;
607 for (int i = 0; i < NB_SAMPLES; i++) {
608 final float alpha = (float) i / NB_SAMPLES;
Adam Powell637d3372010-08-25 14:37:03 -0700609
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800610 float x_max = 1.0f;
611 float x, tx, coef;
612 while (true) {
613 x = x_min + (x_max - x_min) / 2.0f;
614 coef = 3.0f * x * (1.0f - x);
615 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
616 if (Math.abs(tx - alpha) < 1E-5) break;
617 if (tx > alpha) x_max = x;
618 else x_min = x;
619 }
620 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
621
622 float y_max = 1.0f;
623 float y, dy;
624 while (true) {
625 y = y_min + (y_max - y_min) / 2.0f;
626 coef = 3.0f * y * (1.0f - y);
627 dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
628 if (Math.abs(dy - alpha) < 1E-5) break;
629 if (dy > alpha) y_max = y;
630 else y_min = y;
631 }
632 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
633 }
634 SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
635 }
636
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800637 void setFriction(float friction) {
638 mFlingFriction = friction;
639 }
640
Adam Powell990dfc6f2012-08-09 16:45:59 -0700641 SplineOverScroller(Context context) {
Adam Powell637d3372010-08-25 14:37:03 -0700642 mFinished = true;
Adam Powell990dfc6f2012-08-09 16:45:59 -0700643 final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
644 mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
645 * 39.37f // inch/meter
646 * ppi
647 * 0.84f; // look and feel tuning
Adam Powell637d3372010-08-25 14:37:03 -0700648 }
649
650 void updateScroll(float q) {
651 mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
652 }
653
654 /*
655 * Get a signed deceleration that will reduce the velocity.
656 */
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800657 static private float getDeceleration(int velocity) {
658 return velocity > 0 ? -GRAVITY : GRAVITY;
Adam Powell637d3372010-08-25 14:37:03 -0700659 }
660
661 /*
662 * Modifies mDuration to the duration it takes to get from start to newFinal using the
663 * spline interpolation. The previous duration was needed to get to oldFinal.
664 */
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800665 private void adjustDuration(int start, int oldFinal, int newFinal) {
Adam Powell637d3372010-08-25 14:37:03 -0700666 final int oldDistance = oldFinal - start;
667 final int newDistance = newFinal - start;
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800668 final float x = Math.abs((float) newDistance / oldDistance);
Adam Powell637d3372010-08-25 14:37:03 -0700669 final int index = (int) (NB_SAMPLES * x);
670 if (index < NB_SAMPLES) {
671 final float x_inf = (float) index / NB_SAMPLES;
672 final float x_sup = (float) (index + 1) / NB_SAMPLES;
673 final float t_inf = SPLINE_TIME[index];
674 final float t_sup = SPLINE_TIME[index + 1];
675 final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
Adam Powell637d3372010-08-25 14:37:03 -0700676 mDuration *= timeCoef;
677 }
678 }
679
680 void startScroll(int start, int distance, int duration) {
681 mFinished = false;
682
683 mStart = start;
684 mFinal = start + distance;
685
686 mStartTime = AnimationUtils.currentAnimationTimeMillis();
687 mDuration = duration;
688
689 // Unused
690 mDeceleration = 0.0f;
691 mVelocity = 0;
692 }
693
694 void finish() {
695 mCurrentPosition = mFinal;
696 // Not reset since WebView relies on this value for fast fling.
697 // TODO: restore when WebView uses the fast fling implemented in this class.
698 // mCurrVelocity = 0.0f;
699 mFinished = true;
700 }
701
702 void setFinalPosition(int position) {
703 mFinal = position;
704 mFinished = false;
705 }
706
707 void extendDuration(int extend) {
708 final long time = AnimationUtils.currentAnimationTimeMillis();
709 final int elapsedTime = (int) (time - mStartTime);
710 mDuration = elapsedTime + extend;
711 mFinished = false;
712 }
713
Adam Powell637d3372010-08-25 14:37:03 -0700714 boolean springback(int start, int min, int max) {
715 mFinished = true;
716
717 mStart = mFinal = start;
718 mVelocity = 0;
719
720 mStartTime = AnimationUtils.currentAnimationTimeMillis();
721 mDuration = 0;
722
723 if (start < min) {
724 startSpringback(start, min, 0);
725 } else if (start > max) {
726 startSpringback(start, max, 0);
727 }
728
729 return !mFinished;
730 }
731
732 private void startSpringback(int start, int end, int velocity) {
Gilles Debunnec35a8292010-12-10 14:36:17 -0800733 // mStartTime has been set
Adam Powell637d3372010-08-25 14:37:03 -0700734 mFinished = false;
Gilles Debunnec35a8292010-12-10 14:36:17 -0800735 mState = CUBIC;
736 mStart = start;
737 mFinal = end;
738 final int delta = start - end;
739 mDeceleration = getDeceleration(delta);
740 // TODO take velocity into account
741 mVelocity = -delta; // only sign is used
742 mOver = Math.abs(delta);
743 mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
Adam Powell637d3372010-08-25 14:37:03 -0700744 }
745
746 void fling(int start, int velocity, int min, int max, int over) {
747 mOver = over;
748 mFinished = false;
749 mCurrVelocity = mVelocity = velocity;
750 mDuration = mSplineDuration = 0;
751 mStartTime = AnimationUtils.currentAnimationTimeMillis();
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800752 mCurrentPosition = mStart = start;
Adam Powell637d3372010-08-25 14:37:03 -0700753
754 if (start > max || start < min) {
755 startAfterEdge(start, min, max, velocity);
756 return;
757 }
758
Gilles Debunnec35a8292010-12-10 14:36:17 -0800759 mState = SPLINE;
Adam Powell637d3372010-08-25 14:37:03 -0700760 double totalDistance = 0.0;
761
762 if (velocity != 0) {
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800763 mDuration = mSplineDuration = getSplineFlingDuration(velocity);
764 totalDistance = getSplineFlingDistance(velocity);
Adam Powell637d3372010-08-25 14:37:03 -0700765 }
766
767 mSplineDistance = (int) (totalDistance * Math.signum(velocity));
768 mFinal = start + mSplineDistance;
769
770 // Clamp to a valid final position
771 if (mFinal < min) {
772 adjustDuration(mStart, mFinal, min);
773 mFinal = min;
774 }
775
776 if (mFinal > max) {
777 adjustDuration(mStart, mFinal, max);
778 mFinal = max;
779 }
780 }
781
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800782 private double getSplineDeceleration(int velocity) {
Adam Powell990dfc6f2012-08-09 16:45:59 -0700783 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800784 }
785
786 private double getSplineFlingDistance(int velocity) {
787 final double l = getSplineDeceleration(velocity);
788 final double decelMinusOne = DECELERATION_RATE - 1.0;
Adam Powell990dfc6f2012-08-09 16:45:59 -0700789 return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800790 }
791
792 /* Returns the duration, expressed in milliseconds */
793 private int getSplineFlingDuration(int velocity) {
794 final double l = getSplineDeceleration(velocity);
795 final double decelMinusOne = DECELERATION_RATE - 1.0;
796 return (int) (1000.0 * Math.exp(l / decelMinusOne));
797 }
798
Adam Powell637d3372010-08-25 14:37:03 -0700799 private void fitOnBounceCurve(int start, int end, int velocity) {
800 // Simulate a bounce that started from edge
801 final float durationToApex = - velocity / mDeceleration;
802 final float distanceToApex = velocity * velocity / 2.0f / Math.abs(mDeceleration);
803 final float distanceToEdge = Math.abs(end - start);
804 final float totalDuration = (float) Math.sqrt(
805 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
806 mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
807 mStart = end;
808 mVelocity = (int) (- mDeceleration * totalDuration);
809 }
810
811 private void startBounceAfterEdge(int start, int end, int velocity) {
812 mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
813 fitOnBounceCurve(start, end, velocity);
814 onEdgeReached();
815 }
816
817 private void startAfterEdge(int start, int min, int max, int velocity) {
818 if (start > min && start < max) {
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800819 Log.e("OverScroller", "startAfterEdge called from a valid position");
Adam Powell637d3372010-08-25 14:37:03 -0700820 mFinished = true;
821 return;
822 }
823 final boolean positive = start > max;
824 final int edge = positive ? max : min;
825 final int overDistance = start - edge;
826 boolean keepIncreasing = overDistance * velocity >= 0;
827 if (keepIncreasing) {
828 // Will result in a bounce or a to_boundary depending on velocity.
829 startBounceAfterEdge(start, edge, velocity);
830 } else {
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800831 final double totalDistance = getSplineFlingDistance(velocity);
Adam Powell637d3372010-08-25 14:37:03 -0700832 if (totalDistance > Math.abs(overDistance)) {
833 fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
834 } else {
835 startSpringback(start, edge, velocity);
836 }
837 }
838 }
839
840 void notifyEdgeReached(int start, int end, int over) {
Gilles Debunnec35a8292010-12-10 14:36:17 -0800841 // mState is used to detect successive notifications
842 if (mState == SPLINE) {
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800843 mOver = over;
844 mStartTime = AnimationUtils.currentAnimationTimeMillis();
845 // We were in fling/scroll mode before: current velocity is such that distance to
Gilles Debunnec35a8292010-12-10 14:36:17 -0800846 // edge is increasing. This ensures that startAfterEdge will not start a new fling.
Gilles Debunnefff4ab02010-11-23 15:34:37 -0800847 startAfterEdge(start, end, end, (int) mCurrVelocity);
848 }
Adam Powell637d3372010-08-25 14:37:03 -0700849 }
850
851 private void onEdgeReached() {
852 // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
Gilles Debunnec35a8292010-12-10 14:36:17 -0800853 float distance = mVelocity * mVelocity / (2.0f * Math.abs(mDeceleration));
854 final float sign = Math.signum(mVelocity);
Adam Powell637d3372010-08-25 14:37:03 -0700855
Gilles Debunnec35a8292010-12-10 14:36:17 -0800856 if (distance > mOver) {
857 // Default deceleration is not sufficient to slow us down before boundary
858 mDeceleration = - sign * mVelocity * mVelocity / (2.0f * mOver);
859 distance = mOver;
Adam Powell637d3372010-08-25 14:37:03 -0700860 }
Gilles Debunnec35a8292010-12-10 14:36:17 -0800861
862 mOver = (int) distance;
863 mState = BALLISTIC;
864 mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
865 mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
Adam Powell637d3372010-08-25 14:37:03 -0700866 }
867
868 boolean continueWhenFinished() {
869 switch (mState) {
Gilles Debunnec35a8292010-12-10 14:36:17 -0800870 case SPLINE:
Adam Powell637d3372010-08-25 14:37:03 -0700871 // Duration from start to null velocity
872 if (mDuration < mSplineDuration) {
873 // If the animation was clamped, we reached the edge
874 mStart = mFinal;
Gilles Debunnec35a8292010-12-10 14:36:17 -0800875 // TODO Better compute speed when edge was reached
Adam Powell637d3372010-08-25 14:37:03 -0700876 mVelocity = (int) mCurrVelocity;
877 mDeceleration = getDeceleration(mVelocity);
878 mStartTime += mDuration;
879 onEdgeReached();
880 } else {
881 // Normal stop, no need to continue
882 return false;
883 }
884 break;
Gilles Debunnec35a8292010-12-10 14:36:17 -0800885 case BALLISTIC:
Adam Powell637d3372010-08-25 14:37:03 -0700886 mStartTime += mDuration;
Gilles Debunnec35a8292010-12-10 14:36:17 -0800887 startSpringback(mFinal, mStart, 0);
Adam Powell637d3372010-08-25 14:37:03 -0700888 break;
Gilles Debunnec35a8292010-12-10 14:36:17 -0800889 case CUBIC:
890 return false;
Adam Powell637d3372010-08-25 14:37:03 -0700891 }
892
893 update();
894 return true;
895 }
896
897 /*
898 * Update the current position and velocity for current time. Returns
899 * true if update has been done and false if animation duration has been
900 * reached.
901 */
902 boolean update() {
903 final long time = AnimationUtils.currentAnimationTimeMillis();
904 final long currentTime = time - mStartTime;
905
906 if (currentTime > mDuration) {
907 return false;
908 }
909
910 double distance = 0.0;
911 switch (mState) {
Gilles Debunnec35a8292010-12-10 14:36:17 -0800912 case SPLINE: {
Adam Powell637d3372010-08-25 14:37:03 -0700913 final float t = (float) currentTime / mSplineDuration;
914 final int index = (int) (NB_SAMPLES * t);
915 float distanceCoef = 1.f;
916 float velocityCoef = 0.f;
917 if (index < NB_SAMPLES) {
918 final float t_inf = (float) index / NB_SAMPLES;
919 final float t_sup = (float) (index + 1) / NB_SAMPLES;
920 final float d_inf = SPLINE_POSITION[index];
921 final float d_sup = SPLINE_POSITION[index + 1];
922 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
923 distanceCoef = d_inf + (t - t_inf) * velocityCoef;
924 }
925
926 distance = distanceCoef * mSplineDistance;
Gilles Debunnec35a8292010-12-10 14:36:17 -0800927 mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
Adam Powell637d3372010-08-25 14:37:03 -0700928 break;
929 }
930
Gilles Debunnec35a8292010-12-10 14:36:17 -0800931 case BALLISTIC: {
Adam Powell637d3372010-08-25 14:37:03 -0700932 final float t = currentTime / 1000.0f;
933 mCurrVelocity = mVelocity + mDeceleration * t;
934 distance = mVelocity * t + mDeceleration * t * t / 2.0f;
935 break;
936 }
937
Gilles Debunnec35a8292010-12-10 14:36:17 -0800938 case CUBIC: {
939 final float t = (float) (currentTime) / mDuration;
940 final float t2 = t * t;
941 final float sign = Math.signum(mVelocity);
942 distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
943 mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
Adam Powell637d3372010-08-25 14:37:03 -0700944 break;
945 }
946 }
947
948 mCurrentPosition = mStart + (int) Math.round(distance);
Gilles Debunne34961cc2010-12-14 15:25:36 -0800949
Adam Powell637d3372010-08-25 14:37:03 -0700950 return true;
951 }
952 }
953}