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