• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.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                     mScrollerX.updateScroll(q);
303                     mScrollerY.updateScroll(q);
304                 } else {
305                     abortAnimation();
306                 }
307                 break;
308 
309             case FLING_MODE:
310                 if (!mScrollerX.mFinished) {
311                     if (!mScrollerX.update()) {
312                         if (!mScrollerX.continueWhenFinished()) {
313                             mScrollerX.finish();
314                         }
315                     }
316                 }
317 
318                 if (!mScrollerY.mFinished) {
319                     if (!mScrollerY.update()) {
320                         if (!mScrollerY.continueWhenFinished()) {
321                             mScrollerY.finish();
322                         }
323                     }
324                 }
325 
326                 break;
327         }
328 
329         return true;
330     }
331 
332     /**
333      * Start scrolling by providing a starting point and the distance to travel.
334      * The scroll will use the default value of 250 milliseconds for the
335      * duration.
336      *
337      * @param startX Starting horizontal scroll offset in pixels. Positive
338      *        numbers will scroll the content to the left.
339      * @param startY Starting vertical scroll offset in pixels. Positive numbers
340      *        will scroll the content up.
341      * @param dx Horizontal distance to travel. Positive numbers will scroll the
342      *        content to the left.
343      * @param dy Vertical distance to travel. Positive numbers will scroll the
344      *        content up.
345      */
startScroll(int startX, int startY, int dx, int dy)346     public void startScroll(int startX, int startY, int dx, int dy) {
347         startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
348     }
349 
350     /**
351      * Start scrolling by providing a starting point and the distance to travel.
352      *
353      * @param startX Starting horizontal scroll offset in pixels. Positive
354      *        numbers will scroll the content to the left.
355      * @param startY Starting vertical scroll offset in pixels. Positive numbers
356      *        will scroll the content up.
357      * @param dx Horizontal distance to travel. Positive numbers will scroll the
358      *        content to the left.
359      * @param dy Vertical distance to travel. Positive numbers will scroll the
360      *        content up.
361      * @param duration Duration of the scroll in milliseconds.
362      */
startScroll(int startX, int startY, int dx, int dy, int duration)363     public void startScroll(int startX, int startY, int dx, int dy, int duration) {
364         mMode = SCROLL_MODE;
365         mScrollerX.startScroll(startX, dx, duration);
366         mScrollerY.startScroll(startY, dy, duration);
367     }
368 
369     /**
370      * Call this when you want to 'spring back' into a valid coordinate range.
371      *
372      * @param startX Starting X coordinate
373      * @param startY Starting Y coordinate
374      * @param minX Minimum valid X value
375      * @param maxX Maximum valid X value
376      * @param minY Minimum valid Y value
377      * @param maxY Minimum valid Y value
378      * @return true if a springback was initiated, false if startX and startY were
379      *          already within the valid range.
380      */
springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)381     public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
382         mMode = FLING_MODE;
383 
384         // Make sure both methods are called.
385         final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
386         final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
387         return spingbackX || spingbackY;
388     }
389 
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)390     public void fling(int startX, int startY, int velocityX, int velocityY,
391             int minX, int maxX, int minY, int maxY) {
392         fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
393     }
394 
395     /**
396      * Start scrolling based on a fling gesture. The distance traveled will
397      * depend on the initial velocity of the fling.
398      *
399      * @param startX Starting point of the scroll (X)
400      * @param startY Starting point of the scroll (Y)
401      * @param velocityX Initial velocity of the fling (X) measured in pixels per
402      *            second.
403      * @param velocityY Initial velocity of the fling (Y) measured in pixels per
404      *            second
405      * @param minX Minimum X value. The scroller will not scroll past this point
406      *            unless overX > 0. If overfling is allowed, it will use minX as
407      *            a springback boundary.
408      * @param maxX Maximum X value. The scroller will not scroll past this point
409      *            unless overX > 0. If overfling is allowed, it will use maxX as
410      *            a springback boundary.
411      * @param minY Minimum Y value. The scroller will not scroll past this point
412      *            unless overY > 0. If overfling is allowed, it will use minY as
413      *            a springback boundary.
414      * @param maxY Maximum Y value. The scroller will not scroll past this point
415      *            unless overY > 0. If overfling is allowed, it will use maxY as
416      *            a springback boundary.
417      * @param overX Overfling range. If > 0, horizontal overfling in either
418      *            direction will be possible.
419      * @param overY Overfling range. If > 0, vertical overfling in either
420      *            direction will be possible.
421      */
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)422     public void fling(int startX, int startY, int velocityX, int velocityY,
423             int minX, int maxX, int minY, int maxY, int overX, int overY) {
424         // Continue a scroll or fling in progress
425         if (mFlywheel && !isFinished()) {
426             float oldVelocityX = mScrollerX.mCurrVelocity;
427             float oldVelocityY = mScrollerY.mCurrVelocity;
428             if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
429                     Math.signum(velocityY) == Math.signum(oldVelocityY)) {
430                 velocityX += oldVelocityX;
431                 velocityY += oldVelocityY;
432             }
433         }
434 
435         mMode = FLING_MODE;
436         mScrollerX.fling(startX, velocityX, minX, maxX, overX);
437         mScrollerY.fling(startY, velocityY, minY, maxY, overY);
438     }
439 
440     /**
441      * Notify the scroller that we've reached a horizontal boundary.
442      * Normally the information to handle this will already be known
443      * when the animation is started, such as in a call to one of the
444      * fling functions. However there are cases where this cannot be known
445      * in advance. This function will transition the current motion and
446      * animate from startX to finalX as appropriate.
447      *
448      * @param startX Starting/current X position
449      * @param finalX Desired final X position
450      * @param overX Magnitude of overscroll allowed. This should be the maximum
451      *              desired distance from finalX. Absolute value - must be positive.
452      */
notifyHorizontalEdgeReached(int startX, int finalX, int overX)453     public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
454         mScrollerX.notifyEdgeReached(startX, finalX, overX);
455     }
456 
457     /**
458      * Notify the scroller that we've reached a vertical boundary.
459      * Normally the information to handle this will already be known
460      * when the animation is started, such as in a call to one of the
461      * fling functions. However there are cases where this cannot be known
462      * in advance. This function will animate a parabolic motion from
463      * startY to finalY.
464      *
465      * @param startY Starting/current Y position
466      * @param finalY Desired final Y position
467      * @param overY Magnitude of overscroll allowed. This should be the maximum
468      *              desired distance from finalY. Absolute value - must be positive.
469      */
notifyVerticalEdgeReached(int startY, int finalY, int overY)470     public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
471         mScrollerY.notifyEdgeReached(startY, finalY, overY);
472     }
473 
474     /**
475      * Returns whether the current Scroller is currently returning to a valid position.
476      * Valid bounds were provided by the
477      * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
478      *
479      * One should check this value before calling
480      * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
481      * to restore a valid position will then be stopped. The caller has to take into account
482      * the fact that the started scroll will start from an overscrolled position.
483      *
484      * @return true when the current position is overscrolled and in the process of
485      *         interpolating back to a valid value.
486      */
isOverScrolled()487     public boolean isOverScrolled() {
488         return ((!mScrollerX.mFinished &&
489                 mScrollerX.mState != SplineOverScroller.SPLINE) ||
490                 (!mScrollerY.mFinished &&
491                         mScrollerY.mState != SplineOverScroller.SPLINE));
492     }
493 
494     /**
495      * Stops the animation. Contrary to {@link #forceFinished(boolean)},
496      * aborting the animating causes the scroller to move to the final x and y
497      * positions.
498      *
499      * @see #forceFinished(boolean)
500      */
abortAnimation()501     public void abortAnimation() {
502         mScrollerX.finish();
503         mScrollerY.finish();
504     }
505 
506     /**
507      * Returns the time elapsed since the beginning of the scrolling.
508      *
509      * @return The elapsed time in milliseconds.
510      *
511      * @hide
512      */
timePassed()513     public int timePassed() {
514         final long time = AnimationUtils.currentAnimationTimeMillis();
515         final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
516         return (int) (time - startTime);
517     }
518 
519     /**
520      * @hide
521      */
522     @UnsupportedAppUsage
isScrollingInDirection(float xvel, float yvel)523     public boolean isScrollingInDirection(float xvel, float yvel) {
524         final int dx = mScrollerX.mFinal - mScrollerX.mStart;
525         final int dy = mScrollerY.mFinal - mScrollerY.mStart;
526         return !isFinished() && Math.signum(xvel) == Math.signum(dx) &&
527                 Math.signum(yvel) == Math.signum(dy);
528     }
529 
530     static class SplineOverScroller {
531         // Initial position
532         private int mStart;
533 
534         // Current position
535         private int mCurrentPosition;
536 
537         // Final position
538         private int mFinal;
539 
540         // Initial velocity
541         private int mVelocity;
542 
543         // Current velocity
544         @UnsupportedAppUsage
545         private float mCurrVelocity;
546 
547         // Constant current deceleration
548         private float mDeceleration;
549 
550         // Animation starting time, in system milliseconds
551         private long mStartTime;
552 
553         // Animation duration, in milliseconds
554         private int mDuration;
555 
556         // Duration to complete spline component of animation
557         private int mSplineDuration;
558 
559         // Distance to travel along spline animation
560         private int mSplineDistance;
561 
562         // Whether the animation is currently in progress
563         private boolean mFinished;
564 
565         // The allowed overshot distance before boundary is reached.
566         private int mOver;
567 
568         // Fling friction
569         private float mFlingFriction = ViewConfiguration.getScrollFriction();
570 
571         // Current state of the animation.
572         private int mState = SPLINE;
573 
574         // Constant gravity value, used in the deceleration phase.
575         private static final float GRAVITY = 2000.0f;
576 
577         // A context-specific coefficient adjusted to physical values.
578         private float mPhysicalCoeff;
579 
580         private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
581         private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
582         private static final float START_TENSION = 0.5f;
583         private static final float END_TENSION = 1.0f;
584         private static final float P1 = START_TENSION * INFLEXION;
585         private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
586 
587         private static final int NB_SAMPLES = 100;
588         private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
589         private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
590 
591         private static final int SPLINE = 0;
592         private static final int CUBIC = 1;
593         private static final int BALLISTIC = 2;
594 
595         static {
596             float x_min = 0.0f;
597             float y_min = 0.0f;
598             for (int i = 0; i < NB_SAMPLES; i++) {
599                 final float alpha = (float) i / NB_SAMPLES;
600 
601                 float x_max = 1.0f;
602                 float x, tx, coef;
603                 while (true) {
604                     x = x_min + (x_max - x_min) / 2.0f;
605                     coef = 3.0f * x * (1.0f - x);
606                     tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
607                     if (Math.abs(tx - alpha) < 1E-5) break;
608                     if (tx > alpha) x_max = x;
609                     else x_min = x;
610                 }
611                 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
612 
613                 float y_max = 1.0f;
614                 float y, dy;
615                 while (true) {
616                     y = y_min + (y_max - y_min) / 2.0f;
617                     coef = 3.0f * y * (1.0f - y);
618                     dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
619                     if (Math.abs(dy - alpha) < 1E-5) break;
620                     if (dy > alpha) y_max = y;
621                     else y_min = y;
622                 }
623                 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
624             }
625             SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
626         }
627 
setFriction(float friction)628         void setFriction(float friction) {
629             mFlingFriction = friction;
630         }
631 
SplineOverScroller(Context context)632         SplineOverScroller(Context context) {
633             mFinished = true;
634             final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
635             mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
636                     * 39.37f // inch/meter
637                     * ppi
638                     * 0.84f; // look and feel tuning
639         }
640 
updateScroll(float q)641         void updateScroll(float q) {
642             mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
643         }
644 
645         /*
646          * Get a signed deceleration that will reduce the velocity.
647          */
getDeceleration(int velocity)648         static private float getDeceleration(int velocity) {
649             return velocity > 0 ? -GRAVITY : GRAVITY;
650         }
651 
652         /*
653          * Modifies mDuration to the duration it takes to get from start to newFinal using the
654          * spline interpolation. The previous duration was needed to get to oldFinal.
655          */
adjustDuration(int start, int oldFinal, int newFinal)656         private void adjustDuration(int start, int oldFinal, int newFinal) {
657             final int oldDistance = oldFinal - start;
658             final int newDistance = newFinal - start;
659             final float x = Math.abs((float) newDistance / oldDistance);
660             final int index = (int) (NB_SAMPLES * x);
661             if (index < NB_SAMPLES) {
662                 final float x_inf = (float) index / NB_SAMPLES;
663                 final float x_sup = (float) (index + 1) / NB_SAMPLES;
664                 final float t_inf = SPLINE_TIME[index];
665                 final float t_sup = SPLINE_TIME[index + 1];
666                 final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
667                 mDuration *= timeCoef;
668             }
669         }
670 
startScroll(int start, int distance, int duration)671         void startScroll(int start, int distance, int duration) {
672             mFinished = false;
673 
674             mCurrentPosition = mStart = start;
675             mFinal = start + distance;
676 
677             mStartTime = AnimationUtils.currentAnimationTimeMillis();
678             mDuration = duration;
679 
680             // Unused
681             mDeceleration = 0.0f;
682             mVelocity = 0;
683         }
684 
finish()685         void finish() {
686             mCurrentPosition = mFinal;
687             // Not reset since WebView relies on this value for fast fling.
688             // TODO: restore when WebView uses the fast fling implemented in this class.
689             // mCurrVelocity = 0.0f;
690             mFinished = true;
691         }
692 
setFinalPosition(int position)693         void setFinalPosition(int position) {
694             mFinal = position;
695             mSplineDistance = mFinal - mStart;
696             mFinished = false;
697         }
698 
extendDuration(int extend)699         void extendDuration(int extend) {
700             final long time = AnimationUtils.currentAnimationTimeMillis();
701             final int elapsedTime = (int) (time - mStartTime);
702             mDuration = mSplineDuration = elapsedTime + extend;
703             mFinished = false;
704         }
705 
springback(int start, int min, int max)706         boolean springback(int start, int min, int max) {
707             mFinished = true;
708 
709             mCurrentPosition = mStart = mFinal = start;
710             mVelocity = 0;
711 
712             mStartTime = AnimationUtils.currentAnimationTimeMillis();
713             mDuration = 0;
714 
715             if (start < min) {
716                 startSpringback(start, min, 0);
717             } else if (start > max) {
718                 startSpringback(start, max, 0);
719             }
720 
721             return !mFinished;
722         }
723 
startSpringback(int start, int end, int velocity)724         private void startSpringback(int start, int end, int velocity) {
725             // mStartTime has been set
726             mFinished = false;
727             mState = CUBIC;
728             mCurrentPosition = mStart = start;
729             mFinal = end;
730             final int delta = start - end;
731             mDeceleration = getDeceleration(delta);
732             // TODO take velocity into account
733             mVelocity = -delta; // only sign is used
734             mOver = Math.abs(delta);
735             mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
736         }
737 
fling(int start, int velocity, int min, int max, int over)738         void fling(int start, int velocity, int min, int max, int over) {
739             mOver = over;
740             mFinished = false;
741             mCurrVelocity = mVelocity = velocity;
742             mDuration = mSplineDuration = 0;
743             mStartTime = AnimationUtils.currentAnimationTimeMillis();
744             mCurrentPosition = mStart = start;
745 
746             if (start > max || start < min) {
747                 startAfterEdge(start, min, max, velocity);
748                 return;
749             }
750 
751             mState = SPLINE;
752             double totalDistance = 0.0;
753 
754             if (velocity != 0) {
755                 mDuration = mSplineDuration = getSplineFlingDuration(velocity);
756                 totalDistance = getSplineFlingDistance(velocity);
757             }
758 
759             mSplineDistance = (int) (totalDistance * Math.signum(velocity));
760             mFinal = start + mSplineDistance;
761 
762             // Clamp to a valid final position
763             if (mFinal < min) {
764                 adjustDuration(mStart, mFinal, min);
765                 mFinal = min;
766             }
767 
768             if (mFinal > max) {
769                 adjustDuration(mStart, mFinal, max);
770                 mFinal = max;
771             }
772         }
773 
getSplineDeceleration(int velocity)774         private double getSplineDeceleration(int velocity) {
775             return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
776         }
777 
getSplineFlingDistance(int velocity)778         private double getSplineFlingDistance(int velocity) {
779             final double l = getSplineDeceleration(velocity);
780             final double decelMinusOne = DECELERATION_RATE - 1.0;
781             return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
782         }
783 
784         /* Returns the duration, expressed in milliseconds */
getSplineFlingDuration(int velocity)785         private int getSplineFlingDuration(int velocity) {
786             final double l = getSplineDeceleration(velocity);
787             final double decelMinusOne = DECELERATION_RATE - 1.0;
788             return (int) (1000.0 * Math.exp(l / decelMinusOne));
789         }
790 
fitOnBounceCurve(int start, int end, int velocity)791         private void fitOnBounceCurve(int start, int end, int velocity) {
792             // Simulate a bounce that started from edge
793             final float durationToApex = - velocity / mDeceleration;
794             // The float cast below is necessary to avoid integer overflow.
795             final float velocitySquared = (float) velocity * velocity;
796             final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration);
797             final float distanceToEdge = Math.abs(end - start);
798             final float totalDuration = (float) Math.sqrt(
799                     2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
800             mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
801             mCurrentPosition = mStart = end;
802             mVelocity = (int) (- mDeceleration * totalDuration);
803         }
804 
startBounceAfterEdge(int start, int end, int velocity)805         private void startBounceAfterEdge(int start, int end, int velocity) {
806             mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
807             fitOnBounceCurve(start, end, velocity);
808             onEdgeReached();
809         }
810 
startAfterEdge(int start, int min, int max, int velocity)811         private void startAfterEdge(int start, int min, int max, int velocity) {
812             if (start > min && start < max) {
813                 Log.e("OverScroller", "startAfterEdge called from a valid position");
814                 mFinished = true;
815                 return;
816             }
817             final boolean positive = start > max;
818             final int edge = positive ? max : min;
819             final int overDistance = start - edge;
820             boolean keepIncreasing = overDistance * velocity >= 0;
821             if (keepIncreasing) {
822                 // Will result in a bounce or a to_boundary depending on velocity.
823                 startBounceAfterEdge(start, edge, velocity);
824             } else {
825                 final double totalDistance = getSplineFlingDistance(velocity);
826                 if (totalDistance > Math.abs(overDistance)) {
827                     fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
828                 } else {
829                     startSpringback(start, edge, velocity);
830                 }
831             }
832         }
833 
notifyEdgeReached(int start, int end, int over)834         void notifyEdgeReached(int start, int end, int over) {
835             // mState is used to detect successive notifications
836             if (mState == SPLINE) {
837                 mOver = over;
838                 mStartTime = AnimationUtils.currentAnimationTimeMillis();
839                 // We were in fling/scroll mode before: current velocity is such that distance to
840                 // edge is increasing. This ensures that startAfterEdge will not start a new fling.
841                 startAfterEdge(start, end, end, (int) mCurrVelocity);
842             }
843         }
844 
onEdgeReached()845         private void onEdgeReached() {
846             // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
847             // The float cast below is necessary to avoid integer overflow.
848             final float velocitySquared = (float) mVelocity * mVelocity;
849             float distance = velocitySquared / (2.0f * Math.abs(mDeceleration));
850             final float sign = Math.signum(mVelocity);
851 
852             if (distance > mOver) {
853                 // Default deceleration is not sufficient to slow us down before boundary
854                  mDeceleration = - sign * velocitySquared / (2.0f * mOver);
855                  distance = mOver;
856             }
857 
858             mOver = (int) distance;
859             mState = BALLISTIC;
860             mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
861             mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
862         }
863 
continueWhenFinished()864         boolean continueWhenFinished() {
865             switch (mState) {
866                 case SPLINE:
867                     // Duration from start to null velocity
868                     if (mDuration < mSplineDuration) {
869                         // If the animation was clamped, we reached the edge
870                         mCurrentPosition = mStart = mFinal;
871                         // TODO Better compute speed when edge was reached
872                         mVelocity = (int) mCurrVelocity;
873                         mDeceleration = getDeceleration(mVelocity);
874                         mStartTime += mDuration;
875                         onEdgeReached();
876                     } else {
877                         // Normal stop, no need to continue
878                         return false;
879                     }
880                     break;
881                 case BALLISTIC:
882                     mStartTime += mDuration;
883                     startSpringback(mFinal, mStart, 0);
884                     break;
885                 case CUBIC:
886                     return false;
887             }
888 
889             update();
890             return true;
891         }
892 
893         /*
894          * Update the current position and velocity for current time. Returns
895          * true if update has been done and false if animation duration has been
896          * reached.
897          */
update()898         boolean update() {
899             final long time = AnimationUtils.currentAnimationTimeMillis();
900             final long currentTime = time - mStartTime;
901 
902             if (currentTime == 0) {
903                 // Skip work but report that we're still going if we have a nonzero duration.
904                 return mDuration > 0;
905             }
906             if (currentTime > mDuration) {
907                 return false;
908             }
909 
910             double distance = 0.0;
911             switch (mState) {
912                 case SPLINE: {
913                     final float t = (float) currentTime / mSplineDuration;
914                     final int index = (int) (NB_SAMPLES * t);
915                     float distanceCoef = 1.f;
916                     float velocityCoef = 0.f;
917                     if (index < NB_SAMPLES) {
918                         final float t_inf = (float) index / NB_SAMPLES;
919                         final float t_sup = (float) (index + 1) / NB_SAMPLES;
920                         final float d_inf = SPLINE_POSITION[index];
921                         final float d_sup = SPLINE_POSITION[index + 1];
922                         velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
923                         distanceCoef = d_inf + (t - t_inf) * velocityCoef;
924                     }
925 
926                     distance = distanceCoef * mSplineDistance;
927                     mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
928                     break;
929                 }
930 
931                 case BALLISTIC: {
932                     final float t = currentTime / 1000.0f;
933                     mCurrVelocity = mVelocity + mDeceleration * t;
934                     distance = mVelocity * t + mDeceleration * t * t / 2.0f;
935                     break;
936                 }
937 
938                 case CUBIC: {
939                     final float t = (float) (currentTime) / mDuration;
940                     final float t2 = t * t;
941                     final float sign = Math.signum(mVelocity);
942                     distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
943                     mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
944                     break;
945                 }
946             }
947 
948             mCurrentPosition = mStart + (int) Math.round(distance);
949 
950             return true;
951         }
952     }
953 }
954