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