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