• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package android.widget;
18 
19 import android.content.Context;
20 import android.hardware.SensorManager;
21 import android.util.FloatMath;
22 import android.util.Log;
23 import android.view.ViewConfiguration;
24 import android.view.animation.AnimationUtils;
25 import android.view.animation.Interpolator;
26 
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  */
32 public class OverScroller {
33     private int mMode;
34 
35     private final SplineOverScroller mScrollerX;
36     private final SplineOverScroller mScrollerY;
37 
38     private Interpolator mInterpolator;
39 
40     private final boolean mFlywheel;
41 
42     private static final int DEFAULT_DURATION = 250;
43     private static final int SCROLL_MODE = 0;
44     private static final int FLING_MODE = 1;
45 
46     /**
47      * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel.
48      * @param context
49      */
OverScroller(Context context)50     public OverScroller(Context context) {
51         this(context, null);
52     }
53 
54     /**
55      * Creates an OverScroller with flywheel enabled.
56      * @param context The context of this application.
57      * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
58      * be used.
59      */
OverScroller(Context context, Interpolator interpolator)60     public OverScroller(Context context, Interpolator interpolator) {
61         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.
70      * @hide
71      */
OverScroller(Context context, Interpolator interpolator, boolean flywheel)72     public OverScroller(Context context, Interpolator interpolator, boolean flywheel) {
73         mInterpolator = interpolator;
74         mFlywheel = flywheel;
75         mScrollerX = new SplineOverScroller(context);
76         mScrollerY = new SplineOverScroller(context);
77     }
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
86      * 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.
89      * !deprecated Use {!link #OverScroller(Context, Interpolator, boolean)} instead.
90      */
OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY)91     public OverScroller(Context context, Interpolator interpolator,
92             float bounceCoefficientX, float bounceCoefficientY) {
93         this(context, interpolator, true);
94     }
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
103      * 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.
106      * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
107      * !deprecated Use {!link OverScroller(Context, Interpolator, boolean)} instead.
108      */
OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY, boolean flywheel)109     public OverScroller(Context context, Interpolator interpolator,
110             float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
111         this(context, interpolator, flywheel);
112     }
113 
setInterpolator(Interpolator interpolator)114     void setInterpolator(Interpolator interpolator) {
115         mInterpolator = interpolator;
116     }
117 
118     /**
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      */
setFriction(float friction)125     public final void setFriction(float friction) {
126         mScrollerX.setFriction(friction);
127         mScrollerY.setFriction(friction);
128     }
129 
130     /**
131      *
132      * Returns whether the scroller has finished scrolling.
133      *
134      * @return True if the scroller has finished scrolling, false otherwise.
135      */
isFinished()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      */
forceFinished(boolean finished)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      */
getCurrX()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      */
getCurrY()166     public final int getCurrY() {
167         return mScrollerY.mCurrentPosition;
168     }
169 
170     /**
171      * Returns the absolute value of the current velocity.
172      *
173      * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
174      */
getCurrVelocity()175     public float getCurrVelocity() {
176         float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity;
177         squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity;
178         return FloatMath.sqrt(squaredNorm);
179     }
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      */
getStartX()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      */
getStartY()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      */
getFinalX()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      */
getFinalY()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
getDuration()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
extendDuration(int extend)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
setFinalX(int newX)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
setFinalY(int newY)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      */
computeScrollOffset()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 
307                     if (mInterpolator == null) {
308                         q = Scroller.viscousFluid(q);
309                     } else {
310                         q = mInterpolator.getInterpolation(q);
311                     }
312 
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      */
startScroll(int startX, int startY, int dx, int dy)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      */
startScroll(int startX, int startY, int dx, int dy, int duration)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      */
springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)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 
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)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      */
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)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      */
notifyHorizontalEdgeReached(int startX, int finalX, int overX)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      */
notifyVerticalEdgeReached(int startY, int finalY, int overY)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      */
isOverScrolled()498     public boolean isOverScrolled() {
499         return ((!mScrollerX.mFinished &&
500                 mScrollerX.mState != SplineOverScroller.SPLINE) ||
501                 (!mScrollerY.mFinished &&
502                         mScrollerY.mState != SplineOverScroller.SPLINE));
503     }
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      */
abortAnimation()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      */
timePassed()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      */
isScrollingInDirection(float xvel, float yvel)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) &&
537                 Math.signum(yvel) == Math.signum(dy);
538     }
539 
540     static class SplineOverScroller {
541         // Initial position
542         private int mStart;
543 
544         // Current position
545         private int mCurrentPosition;
546 
547         // Final position
548         private int mFinal;
549 
550         // Initial velocity
551         private int mVelocity;
552 
553         // Current velocity
554         private float mCurrVelocity;
555 
556         // Constant current deceleration
557         private float mDeceleration;
558 
559         // Animation starting time, in system milliseconds
560         private long mStartTime;
561 
562         // Animation duration, in milliseconds
563         private int mDuration;
564 
565         // Duration to complete spline component of animation
566         private int mSplineDuration;
567 
568         // Distance to travel along spline animation
569         private int mSplineDistance;
570 
571         // Whether the animation is currently in progress
572         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 
580         // Current state of the animation.
581         private int mState = SPLINE;
582 
583         // Constant gravity value, used in the deceleration phase.
584         private static final float GRAVITY = 2000.0f;
585 
586         // A context-specific coefficient adjusted to physical values.
587         private float mPhysicalCoeff;
588 
589         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;
593         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];
599 
600         private static final int SPLINE = 0;
601         private static final int CUBIC = 1;
602         private static final int BALLISTIC = 2;
603 
604         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;
609 
610                 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 
setFriction(float friction)637         void setFriction(float friction) {
638             mFlingFriction = friction;
639         }
640 
SplineOverScroller(Context context)641         SplineOverScroller(Context context) {
642             mFinished = true;
643             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
648         }
649 
updateScroll(float q)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          */
getDeceleration(int velocity)657         static private float getDeceleration(int velocity) {
658             return velocity > 0 ? -GRAVITY : GRAVITY;
659         }
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          */
adjustDuration(int start, int oldFinal, int newFinal)665         private void adjustDuration(int start, int oldFinal, int newFinal) {
666             final int oldDistance = oldFinal - start;
667             final int newDistance = newFinal - start;
668             final float x = Math.abs((float) newDistance / oldDistance);
669             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);
676                 mDuration *= timeCoef;
677             }
678         }
679 
startScroll(int start, int distance, int duration)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 
finish()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 
setFinalPosition(int position)702         void setFinalPosition(int position) {
703             mFinal = position;
704             mFinished = false;
705         }
706 
extendDuration(int extend)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 
springback(int start, int min, int max)714         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 
startSpringback(int start, int end, int velocity)732         private void startSpringback(int start, int end, int velocity) {
733             // mStartTime has been set
734             mFinished = false;
735             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));
744         }
745 
fling(int start, int velocity, int min, int max, int over)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();
752             mCurrentPosition = mStart = start;
753 
754             if (start > max || start < min) {
755                 startAfterEdge(start, min, max, velocity);
756                 return;
757             }
758 
759             mState = SPLINE;
760             double totalDistance = 0.0;
761 
762             if (velocity != 0) {
763                 mDuration = mSplineDuration = getSplineFlingDuration(velocity);
764                 totalDistance = getSplineFlingDistance(velocity);
765             }
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 
getSplineDeceleration(int velocity)782         private double getSplineDeceleration(int velocity) {
783             return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
784         }
785 
getSplineFlingDistance(int velocity)786         private double getSplineFlingDistance(int velocity) {
787             final double l = getSplineDeceleration(velocity);
788             final double decelMinusOne = DECELERATION_RATE - 1.0;
789             return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
790         }
791 
792         /* Returns the duration, expressed in milliseconds */
getSplineFlingDuration(int velocity)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 
fitOnBounceCurve(int start, int end, int velocity)799         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 
startBounceAfterEdge(int start, int end, int velocity)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 
startAfterEdge(int start, int min, int max, int velocity)817         private void startAfterEdge(int start, int min, int max, int velocity) {
818             if (start > min && start < max) {
819                 Log.e("OverScroller", "startAfterEdge called from a valid position");
820                 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 {
831                 final double totalDistance = getSplineFlingDistance(velocity);
832                 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 
notifyEdgeReached(int start, int end, int over)840         void notifyEdgeReached(int start, int end, int over) {
841             // mState is used to detect successive notifications
842             if (mState == SPLINE) {
843                 mOver = over;
844                 mStartTime = AnimationUtils.currentAnimationTimeMillis();
845                 // We were in fling/scroll mode before: current velocity is such that distance to
846                 // edge is increasing. This ensures that startAfterEdge will not start a new fling.
847                 startAfterEdge(start, end, end, (int) mCurrVelocity);
848             }
849         }
850 
onEdgeReached()851         private void onEdgeReached() {
852             // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
853             float distance = mVelocity * mVelocity / (2.0f * Math.abs(mDeceleration));
854             final float sign = Math.signum(mVelocity);
855 
856             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;
860             }
861 
862             mOver = (int) distance;
863             mState = BALLISTIC;
864             mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
865             mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
866         }
867 
continueWhenFinished()868         boolean continueWhenFinished() {
869             switch (mState) {
870                 case SPLINE:
871                     // Duration from start to null velocity
872                     if (mDuration < mSplineDuration) {
873                         // If the animation was clamped, we reached the edge
874                         mStart = mFinal;
875                         // TODO Better compute speed when edge was reached
876                         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;
885                 case BALLISTIC:
886                     mStartTime += mDuration;
887                     startSpringback(mFinal, mStart, 0);
888                     break;
889                 case CUBIC:
890                     return false;
891             }
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          */
update()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) {
912                 case SPLINE: {
913                     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;
927                     mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
928                     break;
929                 }
930 
931                 case BALLISTIC: {
932                     final float t = currentTime / 1000.0f;
933                     mCurrVelocity = mVelocity + mDeceleration * t;
934                     distance = mVelocity * t + mDeceleration * t * t / 2.0f;
935                     break;
936                 }
937 
938                 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);
944                     break;
945                 }
946             }
947 
948             mCurrentPosition = mStart + (int) Math.round(distance);
949 
950             return true;
951         }
952     }
953 }
954