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