• 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.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)} instead.
93      */
94     @Deprecated
OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY)95     public OverScroller(Context context, Interpolator interpolator,
96             float bounceCoefficientX, float bounceCoefficientY) {
97         this(context, interpolator, true);
98     }
99 
100     /**
101      * Creates an OverScroller.
102      * @param context The context of this application.
103      * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
104      * be used.
105      * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
106      * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
107      * means no bounce. This behavior is no longer supported and this coefficient has no effect.
108      * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
109      * behavior is no longer supported and this coefficient has no effect.
110      * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
111      * @deprecated Use {@link #OverScroller(Context, Interpolator)} instead.
112      */
113     @Deprecated
OverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY, boolean flywheel)114     public OverScroller(Context context, Interpolator interpolator,
115             float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
116         this(context, interpolator, flywheel);
117     }
118 
setInterpolator(Interpolator interpolator)119     void setInterpolator(Interpolator interpolator) {
120         if (interpolator == null) {
121             mInterpolator = new Scroller.ViscousFluidInterpolator();
122         } else {
123             mInterpolator = interpolator;
124         }
125     }
126 
127     /**
128      * The amount of friction applied to flings. The default value
129      * is {@link ViewConfiguration#getScrollFriction}.
130      *
131      * @param friction A scalar dimension-less value representing the coefficient of
132      *         friction.
133      */
setFriction(float friction)134     public final void setFriction(float friction) {
135         mScrollerX.setFriction(friction);
136         mScrollerY.setFriction(friction);
137     }
138 
139     /**
140      *
141      * Returns whether the scroller has finished scrolling.
142      *
143      * @return True if the scroller has finished scrolling, false otherwise.
144      */
isFinished()145     public final boolean isFinished() {
146         return mScrollerX.mFinished && mScrollerY.mFinished;
147     }
148 
149     /**
150      * Force the finished field to a particular value. Contrary to
151      * {@link #abortAnimation()}, forcing the animation to finished
152      * does NOT cause the scroller to move to the final x and y
153      * position.
154      *
155      * @param finished The new finished value.
156      */
forceFinished(boolean finished)157     public final void forceFinished(boolean finished) {
158         mScrollerX.mFinished = mScrollerY.mFinished = finished;
159     }
160 
161     /**
162      * Returns the current X offset in the scroll.
163      *
164      * @return The new X offset as an absolute distance from the origin.
165      */
getCurrX()166     public final int getCurrX() {
167         return mScrollerX.mCurrentPosition;
168     }
169 
170     /**
171      * Returns the current Y offset in the scroll.
172      *
173      * @return The new Y offset as an absolute distance from the origin.
174      */
getCurrY()175     public final int getCurrY() {
176         return mScrollerY.mCurrentPosition;
177     }
178 
179     /**
180      * Returns the absolute value of the current velocity.
181      *
182      * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
183      */
getCurrVelocity()184     public float getCurrVelocity() {
185         return (float) Math.hypot(mScrollerX.mCurrVelocity, mScrollerY.mCurrVelocity);
186     }
187 
188     /**
189      * Returns the start X offset in the scroll.
190      *
191      * @return The start X offset as an absolute distance from the origin.
192      */
getStartX()193     public final int getStartX() {
194         return mScrollerX.mStart;
195     }
196 
197     /**
198      * Returns the start Y offset in the scroll.
199      *
200      * @return The start Y offset as an absolute distance from the origin.
201      */
getStartY()202     public final int getStartY() {
203         return mScrollerY.mStart;
204     }
205 
206     /**
207      * Returns where the scroll will end. Valid only for "fling" scrolls.
208      *
209      * @return The final X offset as an absolute distance from the origin.
210      */
getFinalX()211     public final int getFinalX() {
212         return mScrollerX.mFinal;
213     }
214 
215     /**
216      * Returns where the scroll will end. Valid only for "fling" scrolls.
217      *
218      * @return The final Y offset as an absolute distance from the origin.
219      */
getFinalY()220     public final int getFinalY() {
221         return mScrollerY.mFinal;
222     }
223 
224     /**
225      * Returns how long the scroll event will take, in milliseconds.
226      *
227      * @return The duration of the scroll in milliseconds.
228      *
229      * @hide Pending removal once nothing depends on it
230      * @deprecated OverScrollers don't necessarily have a fixed duration.
231      *             This function will lie to the best of its ability.
232      */
233     @Deprecated
getDuration()234     public final int getDuration() {
235         return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
236     }
237 
238     /**
239      * Extend the scroll animation. This allows a running animation to scroll
240      * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
241      *
242      * @param extend Additional time to scroll in milliseconds.
243      * @see #setFinalX(int)
244      * @see #setFinalY(int)
245      *
246      * @hide Pending removal once nothing depends on it
247      * @deprecated OverScrollers don't necessarily have a fixed duration.
248      *             Instead of setting a new final position and extending
249      *             the duration of an existing scroll, use startScroll
250      *             to begin a new animation.
251      */
252     @Deprecated
extendDuration(int extend)253     public void extendDuration(int extend) {
254         mScrollerX.extendDuration(extend);
255         mScrollerY.extendDuration(extend);
256     }
257 
258     /**
259      * Sets the final position (X) for this scroller.
260      *
261      * @param newX The new X offset as an absolute distance from the origin.
262      * @see #extendDuration(int)
263      * @see #setFinalY(int)
264      *
265      * @hide Pending removal once nothing depends on it
266      * @deprecated OverScroller's final position may change during an animation.
267      *             Instead of setting a new final position and extending
268      *             the duration of an existing scroll, use startScroll
269      *             to begin a new animation.
270      */
271     @Deprecated
setFinalX(int newX)272     public void setFinalX(int newX) {
273         mScrollerX.setFinalPosition(newX);
274     }
275 
276     /**
277      * Sets the final position (Y) for this scroller.
278      *
279      * @param newY The new Y offset as an absolute distance from the origin.
280      * @see #extendDuration(int)
281      * @see #setFinalX(int)
282      *
283      * @hide Pending removal once nothing depends on it
284      * @deprecated OverScroller's final position may change during an animation.
285      *             Instead of setting a new final position and extending
286      *             the duration of an existing scroll, use startScroll
287      *             to begin a new animation.
288      */
289     @Deprecated
setFinalY(int newY)290     public void setFinalY(int newY) {
291         mScrollerY.setFinalPosition(newY);
292     }
293 
294     /**
295      * Call this when you want to know the new location. If it returns true, the
296      * animation is not yet finished.
297      */
computeScrollOffset()298     public boolean computeScrollOffset() {
299         if (isFinished()) {
300             return false;
301         }
302 
303         switch (mMode) {
304             case SCROLL_MODE:
305                 long time = AnimationUtils.currentAnimationTimeMillis();
306                 // Any scroller can be used for time, since they were started
307                 // together in scroll mode. We use X here.
308                 final long elapsedTime = time - mScrollerX.mStartTime;
309 
310                 final int duration = mScrollerX.mDuration;
311                 if (elapsedTime < duration) {
312                     final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
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             mCurrentPosition = 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             mCurrentPosition = 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             mCurrentPosition = 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             // The float cast below is necessary to avoid integer overflow.
803             final float velocitySquared = (float) velocity * velocity;
804             final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration);
805             final float distanceToEdge = Math.abs(end - start);
806             final float totalDuration = (float) Math.sqrt(
807                     2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
808             mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
809             mCurrentPosition = mStart = end;
810             mVelocity = (int) (- mDeceleration * totalDuration);
811         }
812 
startBounceAfterEdge(int start, int end, int velocity)813         private void startBounceAfterEdge(int start, int end, int velocity) {
814             mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
815             fitOnBounceCurve(start, end, velocity);
816             onEdgeReached();
817         }
818 
startAfterEdge(int start, int min, int max, int velocity)819         private void startAfterEdge(int start, int min, int max, int velocity) {
820             if (start > min && start < max) {
821                 Log.e("OverScroller", "startAfterEdge called from a valid position");
822                 mFinished = true;
823                 return;
824             }
825             final boolean positive = start > max;
826             final int edge = positive ? max : min;
827             final int overDistance = start - edge;
828             boolean keepIncreasing = overDistance * velocity >= 0;
829             if (keepIncreasing) {
830                 // Will result in a bounce or a to_boundary depending on velocity.
831                 startBounceAfterEdge(start, edge, velocity);
832             } else {
833                 final double totalDistance = getSplineFlingDistance(velocity);
834                 if (totalDistance > Math.abs(overDistance)) {
835                     fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
836                 } else {
837                     startSpringback(start, edge, velocity);
838                 }
839             }
840         }
841 
notifyEdgeReached(int start, int end, int over)842         void notifyEdgeReached(int start, int end, int over) {
843             // mState is used to detect successive notifications
844             if (mState == SPLINE) {
845                 mOver = over;
846                 mStartTime = AnimationUtils.currentAnimationTimeMillis();
847                 // We were in fling/scroll mode before: current velocity is such that distance to
848                 // edge is increasing. This ensures that startAfterEdge will not start a new fling.
849                 startAfterEdge(start, end, end, (int) mCurrVelocity);
850             }
851         }
852 
onEdgeReached()853         private void onEdgeReached() {
854             // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
855             // The float cast below is necessary to avoid integer overflow.
856             final float velocitySquared = (float) mVelocity * mVelocity;
857             float distance = velocitySquared / (2.0f * Math.abs(mDeceleration));
858             final float sign = Math.signum(mVelocity);
859 
860             if (distance > mOver) {
861                 // Default deceleration is not sufficient to slow us down before boundary
862                  mDeceleration = - sign * velocitySquared / (2.0f * mOver);
863                  distance = mOver;
864             }
865 
866             mOver = (int) distance;
867             mState = BALLISTIC;
868             mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
869             mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
870         }
871 
continueWhenFinished()872         boolean continueWhenFinished() {
873             switch (mState) {
874                 case SPLINE:
875                     // Duration from start to null velocity
876                     if (mDuration < mSplineDuration) {
877                         // If the animation was clamped, we reached the edge
878                         mCurrentPosition = mStart = mFinal;
879                         // TODO Better compute speed when edge was reached
880                         mVelocity = (int) mCurrVelocity;
881                         mDeceleration = getDeceleration(mVelocity);
882                         mStartTime += mDuration;
883                         onEdgeReached();
884                     } else {
885                         // Normal stop, no need to continue
886                         return false;
887                     }
888                     break;
889                 case BALLISTIC:
890                     mStartTime += mDuration;
891                     startSpringback(mFinal, mStart, 0);
892                     break;
893                 case CUBIC:
894                     return false;
895             }
896 
897             update();
898             return true;
899         }
900 
901         /*
902          * Update the current position and velocity for current time. Returns
903          * true if update has been done and false if animation duration has been
904          * reached.
905          */
update()906         boolean update() {
907             final long time = AnimationUtils.currentAnimationTimeMillis();
908             final long currentTime = time - mStartTime;
909 
910             if (currentTime == 0) {
911                 // Skip work but report that we're still going if we have a nonzero duration.
912                 return mDuration > 0;
913             }
914             if (currentTime > mDuration) {
915                 return false;
916             }
917 
918             double distance = 0.0;
919             switch (mState) {
920                 case SPLINE: {
921                     final float t = (float) currentTime / mSplineDuration;
922                     final int index = (int) (NB_SAMPLES * t);
923                     float distanceCoef = 1.f;
924                     float velocityCoef = 0.f;
925                     if (index < NB_SAMPLES) {
926                         final float t_inf = (float) index / NB_SAMPLES;
927                         final float t_sup = (float) (index + 1) / NB_SAMPLES;
928                         final float d_inf = SPLINE_POSITION[index];
929                         final float d_sup = SPLINE_POSITION[index + 1];
930                         velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
931                         distanceCoef = d_inf + (t - t_inf) * velocityCoef;
932                     }
933 
934                     distance = distanceCoef * mSplineDistance;
935                     mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
936                     break;
937                 }
938 
939                 case BALLISTIC: {
940                     final float t = currentTime / 1000.0f;
941                     mCurrVelocity = mVelocity + mDeceleration * t;
942                     distance = mVelocity * t + mDeceleration * t * t / 2.0f;
943                     break;
944                 }
945 
946                 case CUBIC: {
947                     final float t = (float) (currentTime) / mDuration;
948                     final float t2 = t * t;
949                     final float sign = Math.signum(mVelocity);
950                     distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
951                     mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
952                     break;
953                 }
954             }
955 
956             mCurrentPosition = mStart + (int) Math.round(distance);
957 
958             return true;
959         }
960     }
961 }
962