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