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.compat.annotation.UnsupportedAppUsage;
20 import android.content.Context;
21 import android.hardware.SensorManager;
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     @UnsupportedAppUsage
37     private final SplineOverScroller mScrollerY;
38 
39     @UnsupportedAppUsage
40     private Interpolator mInterpolator;
41 
42     private final boolean mFlywheel;
43 
44     private static final int DEFAULT_DURATION = 250;
45     private static final int SCROLL_MODE = 0;
46     private static final int FLING_MODE = 1;
47 
48     /**
49      * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel.
50      * @param context
51      */
OverScroller(Context context)52     public OverScroller(Context context) {
53         this(context, null);
54     }
55 
56     /**
57      * Creates an OverScroller with flywheel enabled.
58      * @param context The context of this application.
59      * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
60      * be used.
61      */
OverScroller(Context context, Interpolator interpolator)62     public OverScroller(Context context, Interpolator interpolator) {
63         this(context, interpolator, true);
64     }
65 
66     /**
67      * Creates an OverScroller.
68      * @param context The context of this application.
69      * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
70      * be used.
71      * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
72      * @hide
73      */
74     @UnsupportedAppUsage
OverScroller(Context context, Interpolator interpolator, boolean flywheel)75     public OverScroller(Context context, Interpolator interpolator, boolean flywheel) {
76         if (interpolator == null) {
77             mInterpolator = new Scroller.ViscousFluidInterpolator();
78         } else {
79             mInterpolator = interpolator;
80         }
81         mFlywheel = flywheel;
82         mScrollerX = new SplineOverScroller(context);
83         mScrollerY = new SplineOverScroller(context);
84     }
85 
86     /**
87      * Creates an OverScroller with flywheel enabled.
88      * @param context The context of this application.
89      * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
90      * be used.
91      * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
92      * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
93      * means no bounce. This behavior is no longer supported and this coefficient has no effect.
94      * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
95      * behavior is no longer supported and this coefficient has no effect.
96      * @deprecated Use {@link #OverScroller(Context, Interpolator)} instead.
97      */
98     @Deprecated
OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY)99     public OverScroller(Context context, Interpolator interpolator,
100             float bounceCoefficientX, float bounceCoefficientY) {
101         this(context, interpolator, true);
102     }
103 
104     /**
105      * Creates an OverScroller.
106      * @param context The context of this application.
107      * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
108      * be used.
109      * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
110      * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
111      * means no bounce. This behavior is no longer supported and this coefficient has no effect.
112      * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
113      * behavior is no longer supported and this coefficient has no effect.
114      * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
115      * @deprecated Use {@link #OverScroller(Context, Interpolator)} instead.
116      */
117     @Deprecated
OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY, boolean flywheel)118     public OverScroller(Context context, Interpolator interpolator,
119             float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
120         this(context, interpolator, flywheel);
121     }
122 
123     @UnsupportedAppUsage
setInterpolator(Interpolator interpolator)124     void setInterpolator(Interpolator interpolator) {
125         if (interpolator == null) {
126             mInterpolator = new Scroller.ViscousFluidInterpolator();
127         } else {
128             mInterpolator = interpolator;
129         }
130     }
131 
132     /**
133      * The amount of friction applied to flings. The default value
134      * is {@link ViewConfiguration#getScrollFriction}.
135      *
136      * @param friction A scalar dimension-less value representing the coefficient of
137      *         friction.
138      */
setFriction(float friction)139     public final void setFriction(float friction) {
140         mScrollerX.setFriction(friction);
141         mScrollerY.setFriction(friction);
142     }
143 
144     /**
145      *
146      * Returns whether the scroller has finished scrolling.
147      *
148      * @return True if the scroller has finished scrolling, false otherwise.
149      */
isFinished()150     public final boolean isFinished() {
151         return mScrollerX.mFinished && mScrollerY.mFinished;
152     }
153 
154     /**
155      * Force the finished field to a particular value. Contrary to
156      * {@link #abortAnimation()}, forcing the animation to finished
157      * does NOT cause the scroller to move to the final x and y
158      * position.
159      *
160      * @param finished The new finished value.
161      */
forceFinished(boolean finished)162     public final void forceFinished(boolean finished) {
163         mScrollerX.mFinished = mScrollerY.mFinished = finished;
164     }
165 
166     /**
167      * Returns the current X offset in the scroll.
168      *
169      * @return The new X offset as an absolute distance from the origin.
170      */
getCurrX()171     public final int getCurrX() {
172         return mScrollerX.mCurrentPosition;
173     }
174 
175     /**
176      * Returns the current Y offset in the scroll.
177      *
178      * @return The new Y offset as an absolute distance from the origin.
179      */
getCurrY()180     public final int getCurrY() {
181         return mScrollerY.mCurrentPosition;
182     }
183 
184     /**
185      * Returns the absolute value of the current velocity.
186      *
187      * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
188      */
getCurrVelocity()189     public float getCurrVelocity() {
190         return (float) Math.hypot(mScrollerX.mCurrVelocity, mScrollerY.mCurrVelocity);
191     }
192 
193     /**
194      * Returns the start X offset in the scroll.
195      *
196      * @return The start X offset as an absolute distance from the origin.
197      */
getStartX()198     public final int getStartX() {
199         return mScrollerX.mStart;
200     }
201 
202     /**
203      * Returns the start Y offset in the scroll.
204      *
205      * @return The start Y offset as an absolute distance from the origin.
206      */
getStartY()207     public final int getStartY() {
208         return mScrollerY.mStart;
209     }
210 
211     /**
212      * Returns where the scroll will end. Valid only for "fling" scrolls.
213      *
214      * @return The final X offset as an absolute distance from the origin.
215      */
getFinalX()216     public final int getFinalX() {
217         return mScrollerX.mFinal;
218     }
219 
220     /**
221      * Returns where the scroll will end. Valid only for "fling" scrolls.
222      *
223      * @return The final Y offset as an absolute distance from the origin.
224      */
getFinalY()225     public final int getFinalY() {
226         return mScrollerY.mFinal;
227     }
228 
229     /**
230      * Returns how long the scroll event will take, in milliseconds.
231      *
232      * @return The duration of the scroll in milliseconds.
233      *
234      * @hide Pending removal once nothing depends on it
235      * @deprecated OverScrollers don't necessarily have a fixed duration.
236      *             This function will lie to the best of its ability.
237      */
238     @Deprecated
getDuration()239     public final int getDuration() {
240         return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
241     }
242 
243     /**
244      * Extend the scroll animation. This allows a running animation to scroll
245      * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
246      *
247      * @param extend Additional time to scroll in milliseconds.
248      * @see #setFinalX(int)
249      * @see #setFinalY(int)
250      *
251      * @hide Pending removal once nothing depends on it
252      * @deprecated OverScrollers don't necessarily have a fixed duration.
253      *             Instead of setting a new final position and extending
254      *             the duration of an existing scroll, use startScroll
255      *             to begin a new animation.
256      */
257     @Deprecated
258     @UnsupportedAppUsage
extendDuration(int extend)259     public void extendDuration(int extend) {
260         mScrollerX.extendDuration(extend);
261         mScrollerY.extendDuration(extend);
262     }
263 
264     /**
265      * Sets the final position (X) for this scroller.
266      *
267      * @param newX The new X offset as an absolute distance from the origin.
268      * @see #extendDuration(int)
269      * @see #setFinalY(int)
270      *
271      * @hide Pending removal once nothing depends on it
272      * @deprecated OverScroller's final position may change during an animation.
273      *             Instead of setting a new final position and extending
274      *             the duration of an existing scroll, use startScroll
275      *             to begin a new animation.
276      */
277     @Deprecated
setFinalX(int newX)278     public void setFinalX(int newX) {
279         mScrollerX.setFinalPosition(newX);
280     }
281 
282     /**
283      * Sets the final position (Y) for this scroller.
284      *
285      * @param newY The new Y offset as an absolute distance from the origin.
286      * @see #extendDuration(int)
287      * @see #setFinalX(int)
288      *
289      * @hide Pending removal once nothing depends on it
290      * @deprecated OverScroller's final position may change during an animation.
291      *             Instead of setting a new final position and extending
292      *             the duration of an existing scroll, use startScroll
293      *             to begin a new animation.
294      */
295     @Deprecated
setFinalY(int newY)296     public void setFinalY(int newY) {
297         mScrollerY.setFinalPosition(newY);
298     }
299 
300     /**
301      * Call this when you want to know the new location. If it returns true, the
302      * animation is not yet finished.
303      */
computeScrollOffset()304     public boolean computeScrollOffset() {
305         if (isFinished()) {
306             return false;
307         }
308 
309         switch (mMode) {
310             case SCROLL_MODE:
311                 long time = AnimationUtils.currentAnimationTimeMillis();
312                 // Any scroller can be used for time, since they were started
313                 // together in scroll mode. We use X here.
314                 final long elapsedTime = time - mScrollerX.mStartTime;
315 
316                 final int duration = mScrollerX.mDuration;
317                 if (elapsedTime < duration) {
318                     final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
319                     mScrollerX.updateScroll(q);
320                     mScrollerY.updateScroll(q);
321                 } else {
322                     abortAnimation();
323                 }
324                 break;
325 
326             case FLING_MODE:
327                 if (!mScrollerX.mFinished) {
328                     if (!mScrollerX.update()) {
329                         if (!mScrollerX.continueWhenFinished()) {
330                             mScrollerX.finish();
331                         }
332                     }
333                 }
334 
335                 if (!mScrollerY.mFinished) {
336                     if (!mScrollerY.update()) {
337                         if (!mScrollerY.continueWhenFinished()) {
338                             mScrollerY.finish();
339                         }
340                     }
341                 }
342 
343                 break;
344         }
345 
346         return true;
347     }
348 
349     /**
350      * Start scrolling by providing a starting point and the distance to travel.
351      * The scroll will use the default value of 250 milliseconds for the
352      * duration.
353      *
354      * @param startX Starting horizontal scroll offset in pixels. Positive
355      *        numbers will scroll the content to the left.
356      * @param startY Starting vertical scroll offset in pixels. Positive numbers
357      *        will scroll the content up.
358      * @param dx Horizontal distance to travel. Positive numbers will scroll the
359      *        content to the left.
360      * @param dy Vertical distance to travel. Positive numbers will scroll the
361      *        content up.
362      */
startScroll(int startX, int startY, int dx, int dy)363     public void startScroll(int startX, int startY, int dx, int dy) {
364         startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
365     }
366 
367     /**
368      * Start scrolling by providing a starting point and the distance to travel.
369      *
370      * @param startX Starting horizontal scroll offset in pixels. Positive
371      *        numbers will scroll the content to the left.
372      * @param startY Starting vertical scroll offset in pixels. Positive numbers
373      *        will scroll the content up.
374      * @param dx Horizontal distance to travel. Positive numbers will scroll the
375      *        content to the left.
376      * @param dy Vertical distance to travel. Positive numbers will scroll the
377      *        content up.
378      * @param duration Duration of the scroll in milliseconds.
379      */
startScroll(int startX, int startY, int dx, int dy, int duration)380     public void startScroll(int startX, int startY, int dx, int dy, int duration) {
381         mMode = SCROLL_MODE;
382         mScrollerX.startScroll(startX, dx, duration);
383         mScrollerY.startScroll(startY, dy, duration);
384     }
385 
386     /**
387      * Call this when you want to 'spring back' into a valid coordinate range.
388      *
389      * @param startX Starting X coordinate
390      * @param startY Starting Y coordinate
391      * @param minX Minimum valid X value
392      * @param maxX Maximum valid X value
393      * @param minY Minimum valid Y value
394      * @param maxY Minimum valid Y value
395      * @return true if a springback was initiated, false if startX and startY were
396      *          already within the valid range.
397      */
springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)398     public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
399         mMode = FLING_MODE;
400 
401         // Make sure both methods are called.
402         final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
403         final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
404         return spingbackX || spingbackY;
405     }
406 
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)407     public void fling(int startX, int startY, int velocityX, int velocityY,
408             int minX, int maxX, int minY, int maxY) {
409         fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
410     }
411 
412     /**
413      * Start scrolling based on a fling gesture. The distance traveled will
414      * depend on the initial velocity of the fling.
415      *
416      * @param startX Starting point of the scroll (X)
417      * @param startY Starting point of the scroll (Y)
418      * @param velocityX Initial velocity of the fling (X) measured in pixels per
419      *            second.
420      * @param velocityY Initial velocity of the fling (Y) measured in pixels per
421      *            second
422      * @param minX Minimum X value. The scroller will not scroll past this point
423      *            unless overX > 0. If overfling is allowed, it will use minX as
424      *            a springback boundary.
425      * @param maxX Maximum X value. The scroller will not scroll past this point
426      *            unless overX > 0. If overfling is allowed, it will use maxX as
427      *            a springback boundary.
428      * @param minY Minimum Y value. The scroller will not scroll past this point
429      *            unless overY > 0. If overfling is allowed, it will use minY as
430      *            a springback boundary.
431      * @param maxY Maximum Y value. The scroller will not scroll past this point
432      *            unless overY > 0. If overfling is allowed, it will use maxY as
433      *            a springback boundary.
434      * @param overX Overfling range. If > 0, horizontal overfling in either
435      *            direction will be possible.
436      * @param overY Overfling range. If > 0, vertical overfling in either
437      *            direction will be possible.
438      */
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)439     public void fling(int startX, int startY, int velocityX, int velocityY,
440             int minX, int maxX, int minY, int maxY, int overX, int overY) {
441         // Continue a scroll or fling in progress
442         if (mFlywheel && !isFinished()) {
443             float oldVelocityX = mScrollerX.mCurrVelocity;
444             float oldVelocityY = mScrollerY.mCurrVelocity;
445             if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
446                     Math.signum(velocityY) == Math.signum(oldVelocityY)) {
447                 velocityX += oldVelocityX;
448                 velocityY += oldVelocityY;
449             }
450         }
451 
452         mMode = FLING_MODE;
453         mScrollerX.fling(startX, velocityX, minX, maxX, overX);
454         mScrollerY.fling(startY, velocityY, minY, maxY, overY);
455     }
456 
457     /**
458      * Notify the scroller that we've reached a horizontal boundary.
459      * Normally the information to handle this will already be known
460      * when the animation is started, such as in a call to one of the
461      * fling functions. However there are cases where this cannot be known
462      * in advance. This function will transition the current motion and
463      * animate from startX to finalX as appropriate.
464      *
465      * @param startX Starting/current X position
466      * @param finalX Desired final X position
467      * @param overX Magnitude of overscroll allowed. This should be the maximum
468      *              desired distance from finalX. Absolute value - must be positive.
469      */
notifyHorizontalEdgeReached(int startX, int finalX, int overX)470     public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
471         mScrollerX.notifyEdgeReached(startX, finalX, overX);
472     }
473 
474     /**
475      * Notify the scroller that we've reached a vertical boundary.
476      * Normally the information to handle this will already be known
477      * when the animation is started, such as in a call to one of the
478      * fling functions. However there are cases where this cannot be known
479      * in advance. This function will animate a parabolic motion from
480      * startY to finalY.
481      *
482      * @param startY Starting/current Y position
483      * @param finalY Desired final Y position
484      * @param overY Magnitude of overscroll allowed. This should be the maximum
485      *              desired distance from finalY. Absolute value - must be positive.
486      */
notifyVerticalEdgeReached(int startY, int finalY, int overY)487     public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
488         mScrollerY.notifyEdgeReached(startY, finalY, overY);
489     }
490 
491     /**
492      * Returns whether the current Scroller is currently returning to a valid position.
493      * Valid bounds were provided by the
494      * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
495      *
496      * One should check this value before calling
497      * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
498      * to restore a valid position will then be stopped. The caller has to take into account
499      * the fact that the started scroll will start from an overscrolled position.
500      *
501      * @return true when the current position is overscrolled and in the process of
502      *         interpolating back to a valid value.
503      */
isOverScrolled()504     public boolean isOverScrolled() {
505         return ((!mScrollerX.mFinished &&
506                 mScrollerX.mState != SplineOverScroller.SPLINE) ||
507                 (!mScrollerY.mFinished &&
508                         mScrollerY.mState != SplineOverScroller.SPLINE));
509     }
510 
511     /**
512      * Stops the animation. Contrary to {@link #forceFinished(boolean)},
513      * aborting the animating causes the scroller to move to the final x and y
514      * positions.
515      *
516      * @see #forceFinished(boolean)
517      */
abortAnimation()518     public void abortAnimation() {
519         mScrollerX.finish();
520         mScrollerY.finish();
521     }
522 
523     /**
524      * Returns the time elapsed since the beginning of the scrolling.
525      *
526      * @return The elapsed time in milliseconds.
527      *
528      * @hide
529      */
timePassed()530     public int timePassed() {
531         final long time = AnimationUtils.currentAnimationTimeMillis();
532         final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
533         return (int) (time - startTime);
534     }
535 
536     /**
537      * @hide
538      */
539     @UnsupportedAppUsage
isScrollingInDirection(float xvel, float yvel)540     public boolean isScrollingInDirection(float xvel, float yvel) {
541         final int dx = mScrollerX.mFinal - mScrollerX.mStart;
542         final int dy = mScrollerY.mFinal - mScrollerY.mStart;
543         return !isFinished() && Math.signum(xvel) == Math.signum(dx) &&
544                 Math.signum(yvel) == Math.signum(dy);
545     }
546 
547     static class SplineOverScroller {
548         // Initial position
549         private int mStart;
550 
551         // Current position
552         private int mCurrentPosition;
553 
554         // Final position
555         private int mFinal;
556 
557         // Initial velocity
558         private int mVelocity;
559 
560         // Current velocity
561         @UnsupportedAppUsage
562         private float mCurrVelocity;
563 
564         // Constant current deceleration
565         private float mDeceleration;
566 
567         // Animation starting time, in system milliseconds
568         private long mStartTime;
569 
570         // Animation duration, in milliseconds
571         private int mDuration;
572 
573         // Duration to complete spline component of animation
574         private int mSplineDuration;
575 
576         // Distance to travel along spline animation
577         private int mSplineDistance;
578 
579         // Whether the animation is currently in progress
580         private boolean mFinished;
581 
582         // The allowed overshot distance before boundary is reached.
583         private int mOver;
584 
585         // Fling friction
586         private float mFlingFriction = ViewConfiguration.getScrollFriction();
587 
588         // Current state of the animation.
589         private int mState = SPLINE;
590 
591         // Constant gravity value, used in the deceleration phase.
592         private static final float GRAVITY = 2000.0f;
593 
594         // A context-specific coefficient adjusted to physical values.
595         private float mPhysicalCoeff;
596 
597         private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
598         private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
599         private static final float START_TENSION = 0.5f;
600         private static final float END_TENSION = 1.0f;
601         private static final float P1 = START_TENSION * INFLEXION;
602         private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
603 
604         private static final int NB_SAMPLES = 100;
605         private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
606         private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
607 
608         private static final int SPLINE = 0;
609         private static final int CUBIC = 1;
610         private static final int BALLISTIC = 2;
611 
612         static {
613             float x_min = 0.0f;
614             float y_min = 0.0f;
615             for (int i = 0; i < NB_SAMPLES; i++) {
616                 final float alpha = (float) i / NB_SAMPLES;
617 
618                 float x_max = 1.0f;
619                 float x, tx, coef;
620                 while (true) {
621                     x = x_min + (x_max - x_min) / 2.0f;
622                     coef = 3.0f * x * (1.0f - x);
623                     tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
624                     if (Math.abs(tx - alpha) < 1E-5) break;
625                     if (tx > alpha) x_max = x;
626                     else x_min = x;
627                 }
628                 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
629 
630                 float y_max = 1.0f;
631                 float y, dy;
632                 while (true) {
633                     y = y_min + (y_max - y_min) / 2.0f;
634                     coef = 3.0f * y * (1.0f - y);
635                     dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
636                     if (Math.abs(dy - alpha) < 1E-5) break;
637                     if (dy > alpha) y_max = y;
638                     else y_min = y;
639                 }
640                 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
641             }
642             SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
643         }
644 
setFriction(float friction)645         void setFriction(float friction) {
646             mFlingFriction = friction;
647         }
648 
SplineOverScroller(Context context)649         SplineOverScroller(Context context) {
650             mFinished = true;
651             final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
652             mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
653                     * 39.37f // inch/meter
654                     * ppi
655                     * 0.84f; // look and feel tuning
656         }
657 
updateScroll(float q)658         void updateScroll(float q) {
659             mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
660         }
661 
662         /*
663          * Get a signed deceleration that will reduce the velocity.
664          */
getDeceleration(int velocity)665         static private float getDeceleration(int velocity) {
666             return velocity > 0 ? -GRAVITY : GRAVITY;
667         }
668 
669         /*
670          * Modifies mDuration to the duration it takes to get from start to newFinal using the
671          * spline interpolation. The previous duration was needed to get to oldFinal.
672          */
adjustDuration(int start, int oldFinal, int newFinal)673         private void adjustDuration(int start, int oldFinal, int newFinal) {
674             final int oldDistance = oldFinal - start;
675             final int newDistance = newFinal - start;
676             final float x = Math.abs((float) newDistance / oldDistance);
677             final int index = (int) (NB_SAMPLES * x);
678             if (index < NB_SAMPLES) {
679                 final float x_inf = (float) index / NB_SAMPLES;
680                 final float x_sup = (float) (index + 1) / NB_SAMPLES;
681                 final float t_inf = SPLINE_TIME[index];
682                 final float t_sup = SPLINE_TIME[index + 1];
683                 final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
684                 mDuration *= timeCoef;
685             }
686         }
687 
startScroll(int start, int distance, int duration)688         void startScroll(int start, int distance, int duration) {
689             mFinished = false;
690 
691             mCurrentPosition = mStart = start;
692             mFinal = start + distance;
693 
694             mStartTime = AnimationUtils.currentAnimationTimeMillis();
695             mDuration = duration;
696 
697             // Unused
698             mDeceleration = 0.0f;
699             mVelocity = 0;
700         }
701 
finish()702         void finish() {
703             mCurrentPosition = mFinal;
704             // Not reset since WebView relies on this value for fast fling.
705             // TODO: restore when WebView uses the fast fling implemented in this class.
706             // mCurrVelocity = 0.0f;
707             mFinished = true;
708         }
709 
setFinalPosition(int position)710         void setFinalPosition(int position) {
711             mFinal = position;
712             mFinished = false;
713         }
714 
extendDuration(int extend)715         void extendDuration(int extend) {
716             final long time = AnimationUtils.currentAnimationTimeMillis();
717             final int elapsedTime = (int) (time - mStartTime);
718             mDuration = elapsedTime + extend;
719             mFinished = false;
720         }
721 
springback(int start, int min, int max)722         boolean springback(int start, int min, int max) {
723             mFinished = true;
724 
725             mCurrentPosition = mStart = mFinal = start;
726             mVelocity = 0;
727 
728             mStartTime = AnimationUtils.currentAnimationTimeMillis();
729             mDuration = 0;
730 
731             if (start < min) {
732                 startSpringback(start, min, 0);
733             } else if (start > max) {
734                 startSpringback(start, max, 0);
735             }
736 
737             return !mFinished;
738         }
739 
startSpringback(int start, int end, int velocity)740         private void startSpringback(int start, int end, int velocity) {
741             // mStartTime has been set
742             mFinished = false;
743             mState = CUBIC;
744             mCurrentPosition = mStart = start;
745             mFinal = end;
746             final int delta = start - end;
747             mDeceleration = getDeceleration(delta);
748             // TODO take velocity into account
749             mVelocity = -delta; // only sign is used
750             mOver = Math.abs(delta);
751             mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
752         }
753 
fling(int start, int velocity, int min, int max, int over)754         void fling(int start, int velocity, int min, int max, int over) {
755             mOver = over;
756             mFinished = false;
757             mCurrVelocity = mVelocity = velocity;
758             mDuration = mSplineDuration = 0;
759             mStartTime = AnimationUtils.currentAnimationTimeMillis();
760             mCurrentPosition = mStart = start;
761 
762             if (start > max || start < min) {
763                 startAfterEdge(start, min, max, velocity);
764                 return;
765             }
766 
767             mState = SPLINE;
768             double totalDistance = 0.0;
769 
770             if (velocity != 0) {
771                 mDuration = mSplineDuration = getSplineFlingDuration(velocity);
772                 totalDistance = getSplineFlingDistance(velocity);
773             }
774 
775             mSplineDistance = (int) (totalDistance * Math.signum(velocity));
776             mFinal = start + mSplineDistance;
777 
778             // Clamp to a valid final position
779             if (mFinal < min) {
780                 adjustDuration(mStart, mFinal, min);
781                 mFinal = min;
782             }
783 
784             if (mFinal > max) {
785                 adjustDuration(mStart, mFinal, max);
786                 mFinal = max;
787             }
788         }
789 
getSplineDeceleration(int velocity)790         private double getSplineDeceleration(int velocity) {
791             return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
792         }
793 
getSplineFlingDistance(int velocity)794         private double getSplineFlingDistance(int velocity) {
795             final double l = getSplineDeceleration(velocity);
796             final double decelMinusOne = DECELERATION_RATE - 1.0;
797             return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
798         }
799 
800         /* Returns the duration, expressed in milliseconds */
getSplineFlingDuration(int velocity)801         private int getSplineFlingDuration(int velocity) {
802             final double l = getSplineDeceleration(velocity);
803             final double decelMinusOne = DECELERATION_RATE - 1.0;
804             return (int) (1000.0 * Math.exp(l / decelMinusOne));
805         }
806 
fitOnBounceCurve(int start, int end, int velocity)807         private void fitOnBounceCurve(int start, int end, int velocity) {
808             // Simulate a bounce that started from edge
809             final float durationToApex = - velocity / mDeceleration;
810             // The float cast below is necessary to avoid integer overflow.
811             final float velocitySquared = (float) velocity * velocity;
812             final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration);
813             final float distanceToEdge = Math.abs(end - start);
814             final float totalDuration = (float) Math.sqrt(
815                     2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
816             mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
817             mCurrentPosition = mStart = end;
818             mVelocity = (int) (- mDeceleration * totalDuration);
819         }
820 
startBounceAfterEdge(int start, int end, int velocity)821         private void startBounceAfterEdge(int start, int end, int velocity) {
822             mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
823             fitOnBounceCurve(start, end, velocity);
824             onEdgeReached();
825         }
826 
startAfterEdge(int start, int min, int max, int velocity)827         private void startAfterEdge(int start, int min, int max, int velocity) {
828             if (start > min && start < max) {
829                 Log.e("OverScroller", "startAfterEdge called from a valid position");
830                 mFinished = true;
831                 return;
832             }
833             final boolean positive = start > max;
834             final int edge = positive ? max : min;
835             final int overDistance = start - edge;
836             boolean keepIncreasing = overDistance * velocity >= 0;
837             if (keepIncreasing) {
838                 // Will result in a bounce or a to_boundary depending on velocity.
839                 startBounceAfterEdge(start, edge, velocity);
840             } else {
841                 final double totalDistance = getSplineFlingDistance(velocity);
842                 if (totalDistance > Math.abs(overDistance)) {
843                     fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
844                 } else {
845                     startSpringback(start, edge, velocity);
846                 }
847             }
848         }
849 
notifyEdgeReached(int start, int end, int over)850         void notifyEdgeReached(int start, int end, int over) {
851             // mState is used to detect successive notifications
852             if (mState == SPLINE) {
853                 mOver = over;
854                 mStartTime = AnimationUtils.currentAnimationTimeMillis();
855                 // We were in fling/scroll mode before: current velocity is such that distance to
856                 // edge is increasing. This ensures that startAfterEdge will not start a new fling.
857                 startAfterEdge(start, end, end, (int) mCurrVelocity);
858             }
859         }
860 
onEdgeReached()861         private void onEdgeReached() {
862             // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
863             // The float cast below is necessary to avoid integer overflow.
864             final float velocitySquared = (float) mVelocity * mVelocity;
865             float distance = velocitySquared / (2.0f * Math.abs(mDeceleration));
866             final float sign = Math.signum(mVelocity);
867 
868             if (distance > mOver) {
869                 // Default deceleration is not sufficient to slow us down before boundary
870                  mDeceleration = - sign * velocitySquared / (2.0f * mOver);
871                  distance = mOver;
872             }
873 
874             mOver = (int) distance;
875             mState = BALLISTIC;
876             mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
877             mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
878         }
879 
continueWhenFinished()880         boolean continueWhenFinished() {
881             switch (mState) {
882                 case SPLINE:
883                     // Duration from start to null velocity
884                     if (mDuration < mSplineDuration) {
885                         // If the animation was clamped, we reached the edge
886                         mCurrentPosition = mStart = mFinal;
887                         // TODO Better compute speed when edge was reached
888                         mVelocity = (int) mCurrVelocity;
889                         mDeceleration = getDeceleration(mVelocity);
890                         mStartTime += mDuration;
891                         onEdgeReached();
892                     } else {
893                         // Normal stop, no need to continue
894                         return false;
895                     }
896                     break;
897                 case BALLISTIC:
898                     mStartTime += mDuration;
899                     startSpringback(mFinal, mStart, 0);
900                     break;
901                 case CUBIC:
902                     return false;
903             }
904 
905             update();
906             return true;
907         }
908 
909         /*
910          * Update the current position and velocity for current time. Returns
911          * true if update has been done and false if animation duration has been
912          * reached.
913          */
update()914         boolean update() {
915             final long time = AnimationUtils.currentAnimationTimeMillis();
916             final long currentTime = time - mStartTime;
917 
918             if (currentTime == 0) {
919                 // Skip work but report that we're still going if we have a nonzero duration.
920                 return mDuration > 0;
921             }
922             if (currentTime > mDuration) {
923                 return false;
924             }
925 
926             double distance = 0.0;
927             switch (mState) {
928                 case SPLINE: {
929                     final float t = (float) currentTime / mSplineDuration;
930                     final int index = (int) (NB_SAMPLES * t);
931                     float distanceCoef = 1.f;
932                     float velocityCoef = 0.f;
933                     if (index < NB_SAMPLES) {
934                         final float t_inf = (float) index / NB_SAMPLES;
935                         final float t_sup = (float) (index + 1) / NB_SAMPLES;
936                         final float d_inf = SPLINE_POSITION[index];
937                         final float d_sup = SPLINE_POSITION[index + 1];
938                         velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
939                         distanceCoef = d_inf + (t - t_inf) * velocityCoef;
940                     }
941 
942                     distance = distanceCoef * mSplineDistance;
943                     mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
944                     break;
945                 }
946 
947                 case BALLISTIC: {
948                     final float t = currentTime / 1000.0f;
949                     mCurrVelocity = mVelocity + mDeceleration * t;
950                     distance = mVelocity * t + mDeceleration * t * t / 2.0f;
951                     break;
952                 }
953 
954                 case CUBIC: {
955                     final float t = (float) (currentTime) / mDuration;
956                     final float t2 = t * t;
957                     final float sign = Math.signum(mVelocity);
958                     distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
959                     mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
960                     break;
961                 }
962             }
963 
964             mCurrentPosition = mStart + (int) Math.round(distance);
965 
966             return true;
967         }
968     }
969 }
970