• 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 
getSplineFlingDistance(int velocity)530     double getSplineFlingDistance(int velocity) {
531         return mScrollerY.getSplineFlingDistance(velocity);
532     }
533 
534     static class SplineOverScroller {
535         // Initial position
536         private int mStart;
537 
538         // Current position
539         private int mCurrentPosition;
540 
541         // Final position
542         private int mFinal;
543 
544         // Initial velocity
545         private int mVelocity;
546 
547         // Current velocity
548         @UnsupportedAppUsage
549         private float mCurrVelocity;
550 
551         // Constant current deceleration
552         private float mDeceleration;
553 
554         // Animation starting time, in system milliseconds
555         private long mStartTime;
556 
557         // Animation duration, in milliseconds
558         private int mDuration;
559 
560         // Duration to complete spline component of animation
561         private int mSplineDuration;
562 
563         // Distance to travel along spline animation
564         private int mSplineDistance;
565 
566         // Whether the animation is currently in progress
567         private boolean mFinished;
568 
569         // The allowed overshot distance before boundary is reached.
570         private int mOver;
571 
572         // Fling friction
573         private float mFlingFriction = ViewConfiguration.getScrollFriction();
574 
575         // Current state of the animation.
576         private int mState = SPLINE;
577 
578         // Constant gravity value, used in the deceleration phase.
579         private static final float GRAVITY = 2000.0f;
580 
581         // A context-specific coefficient adjusted to physical values.
582         private float mPhysicalCoeff;
583 
584         private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
585         private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
586         private static final float START_TENSION = 0.5f;
587         private static final float END_TENSION = 1.0f;
588         private static final float P1 = START_TENSION * INFLEXION;
589         private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
590 
591         private static final int NB_SAMPLES = 100;
592         private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
593         private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
594 
595         private static final int SPLINE = 0;
596         private static final int CUBIC = 1;
597         private static final int BALLISTIC = 2;
598 
599         static {
600             float x_min = 0.0f;
601             float y_min = 0.0f;
602             for (int i = 0; i < NB_SAMPLES; i++) {
603                 final float alpha = (float) i / NB_SAMPLES;
604 
605                 float x_max = 1.0f;
606                 float x, tx, coef;
607                 while (true) {
608                     x = x_min + (x_max - x_min) / 2.0f;
609                     coef = 3.0f * x * (1.0f - x);
610                     tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
611                     if (Math.abs(tx - alpha) < 1E-5) break;
612                     if (tx > alpha) x_max = x;
613                     else x_min = x;
614                 }
615                 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
616 
617                 float y_max = 1.0f;
618                 float y, dy;
619                 while (true) {
620                     y = y_min + (y_max - y_min) / 2.0f;
621                     coef = 3.0f * y * (1.0f - y);
622                     dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
623                     if (Math.abs(dy - alpha) < 1E-5) break;
624                     if (dy > alpha) y_max = y;
625                     else y_min = y;
626                 }
627                 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
628             }
629             SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
630         }
631 
setFriction(float friction)632         void setFriction(float friction) {
633             mFlingFriction = friction;
634         }
635 
SplineOverScroller(Context context)636         SplineOverScroller(Context context) {
637             mFinished = true;
638             final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
639             mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
640                     * 39.37f // inch/meter
641                     * ppi
642                     * 0.84f; // look and feel tuning
643         }
644 
updateScroll(float q)645         void updateScroll(float q) {
646             mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
647         }
648 
649         /*
650          * Get a signed deceleration that will reduce the velocity.
651          */
getDeceleration(int velocity)652         static private float getDeceleration(int velocity) {
653             return velocity > 0 ? -GRAVITY : GRAVITY;
654         }
655 
656         /*
657          * Modifies mDuration to the duration it takes to get from start to newFinal using the
658          * spline interpolation. The previous duration was needed to get to oldFinal.
659          */
adjustDuration(int start, int oldFinal, int newFinal)660         private void adjustDuration(int start, int oldFinal, int newFinal) {
661             final int oldDistance = oldFinal - start;
662             final int newDistance = newFinal - start;
663             final float x = Math.abs((float) newDistance / oldDistance);
664             final int index = (int) (NB_SAMPLES * x);
665             if (index < NB_SAMPLES) {
666                 final float x_inf = (float) index / NB_SAMPLES;
667                 final float x_sup = (float) (index + 1) / NB_SAMPLES;
668                 final float t_inf = SPLINE_TIME[index];
669                 final float t_sup = SPLINE_TIME[index + 1];
670                 final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
671                 mDuration *= timeCoef;
672             }
673         }
674 
startScroll(int start, int distance, int duration)675         void startScroll(int start, int distance, int duration) {
676             mFinished = false;
677 
678             mCurrentPosition = mStart = start;
679             mFinal = start + distance;
680 
681             mStartTime = AnimationUtils.currentAnimationTimeMillis();
682             mDuration = duration;
683 
684             // Unused
685             mDeceleration = 0.0f;
686             mVelocity = 0;
687         }
688 
finish()689         void finish() {
690             mCurrentPosition = mFinal;
691             // Not reset since WebView relies on this value for fast fling.
692             // TODO: restore when WebView uses the fast fling implemented in this class.
693             // mCurrVelocity = 0.0f;
694             mFinished = true;
695         }
696 
setFinalPosition(int position)697         void setFinalPosition(int position) {
698             mFinal = position;
699             mSplineDistance = mFinal - mStart;
700             mFinished = false;
701         }
702 
extendDuration(int extend)703         void extendDuration(int extend) {
704             final long time = AnimationUtils.currentAnimationTimeMillis();
705             final int elapsedTime = (int) (time - mStartTime);
706             mDuration = mSplineDuration = elapsedTime + extend;
707             mFinished = false;
708         }
709 
springback(int start, int min, int max)710         boolean springback(int start, int min, int max) {
711             mFinished = true;
712 
713             mCurrentPosition = mStart = mFinal = start;
714             mVelocity = 0;
715 
716             mStartTime = AnimationUtils.currentAnimationTimeMillis();
717             mDuration = 0;
718 
719             if (start < min) {
720                 startSpringback(start, min, 0);
721             } else if (start > max) {
722                 startSpringback(start, max, 0);
723             }
724 
725             return !mFinished;
726         }
727 
startSpringback(int start, int end, int velocity)728         private void startSpringback(int start, int end, int velocity) {
729             // mStartTime has been set
730             mFinished = false;
731             mState = CUBIC;
732             mCurrentPosition = mStart = start;
733             mFinal = end;
734             final int delta = start - end;
735             mDeceleration = getDeceleration(delta);
736             // TODO take velocity into account
737             mVelocity = -delta; // only sign is used
738             mOver = Math.abs(delta);
739             mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
740         }
741 
fling(int start, int velocity, int min, int max, int over)742         void fling(int start, int velocity, int min, int max, int over) {
743             mOver = over;
744             mFinished = false;
745             mCurrVelocity = mVelocity = velocity;
746             mDuration = mSplineDuration = 0;
747             mStartTime = AnimationUtils.currentAnimationTimeMillis();
748             mCurrentPosition = mStart = start;
749 
750             if (start > max || start < min) {
751                 startAfterEdge(start, min, max, velocity);
752                 return;
753             }
754 
755             mState = SPLINE;
756             double totalDistance = 0.0;
757 
758             if (velocity != 0) {
759                 mDuration = mSplineDuration = getSplineFlingDuration(velocity);
760                 totalDistance = getSplineFlingDistance(velocity);
761             }
762 
763             mSplineDistance = (int) (totalDistance * Math.signum(velocity));
764             mFinal = start + mSplineDistance;
765 
766             // Clamp to a valid final position
767             if (mFinal < min) {
768                 adjustDuration(mStart, mFinal, min);
769                 mFinal = min;
770             }
771 
772             if (mFinal > max) {
773                 adjustDuration(mStart, mFinal, max);
774                 mFinal = max;
775             }
776         }
777 
getSplineDeceleration(int velocity)778         private double getSplineDeceleration(int velocity) {
779             return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
780         }
781 
getSplineFlingDistance(int velocity)782         private double getSplineFlingDistance(int velocity) {
783             final double l = getSplineDeceleration(velocity);
784             final double decelMinusOne = DECELERATION_RATE - 1.0;
785             return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
786         }
787 
788         /* Returns the duration, expressed in milliseconds */
getSplineFlingDuration(int velocity)789         private int getSplineFlingDuration(int velocity) {
790             final double l = getSplineDeceleration(velocity);
791             final double decelMinusOne = DECELERATION_RATE - 1.0;
792             return (int) (1000.0 * Math.exp(l / decelMinusOne));
793         }
794 
fitOnBounceCurve(int start, int end, int velocity)795         private void fitOnBounceCurve(int start, int end, int velocity) {
796             // Simulate a bounce that started from edge
797             final float durationToApex = - velocity / mDeceleration;
798             // The float cast below is necessary to avoid integer overflow.
799             final float velocitySquared = (float) velocity * velocity;
800             final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration);
801             final float distanceToEdge = Math.abs(end - start);
802             final float totalDuration = (float) Math.sqrt(
803                     2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
804             mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
805             mCurrentPosition = mStart = end;
806             mVelocity = (int) (- mDeceleration * totalDuration);
807         }
808 
startBounceAfterEdge(int start, int end, int velocity)809         private void startBounceAfterEdge(int start, int end, int velocity) {
810             mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
811             fitOnBounceCurve(start, end, velocity);
812             onEdgeReached();
813         }
814 
startAfterEdge(int start, int min, int max, int velocity)815         private void startAfterEdge(int start, int min, int max, int velocity) {
816             if (start > min && start < max) {
817                 Log.e("OverScroller", "startAfterEdge called from a valid position");
818                 mFinished = true;
819                 return;
820             }
821             final boolean positive = start > max;
822             final int edge = positive ? max : min;
823             final int overDistance = start - edge;
824             boolean keepIncreasing = overDistance * velocity >= 0;
825             if (keepIncreasing) {
826                 // Will result in a bounce or a to_boundary depending on velocity.
827                 startBounceAfterEdge(start, edge, velocity);
828             } else {
829                 final double totalDistance = getSplineFlingDistance(velocity);
830                 if (totalDistance > Math.abs(overDistance)) {
831                     fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
832                 } else {
833                     startSpringback(start, edge, velocity);
834                 }
835             }
836         }
837 
notifyEdgeReached(int start, int end, int over)838         void notifyEdgeReached(int start, int end, int over) {
839             // mState is used to detect successive notifications
840             if (mState == SPLINE) {
841                 mOver = over;
842                 mStartTime = AnimationUtils.currentAnimationTimeMillis();
843                 // We were in fling/scroll mode before: current velocity is such that distance to
844                 // edge is increasing. This ensures that startAfterEdge will not start a new fling.
845                 startAfterEdge(start, end, end, (int) mCurrVelocity);
846             }
847         }
848 
onEdgeReached()849         private void onEdgeReached() {
850             // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
851             // The float cast below is necessary to avoid integer overflow.
852             final float velocitySquared = (float) mVelocity * mVelocity;
853             float distance = velocitySquared / (2.0f * Math.abs(mDeceleration));
854             final float sign = Math.signum(mVelocity);
855 
856             if (distance > mOver) {
857                 // Default deceleration is not sufficient to slow us down before boundary
858                  mDeceleration = - sign * velocitySquared / (2.0f * mOver);
859                  distance = mOver;
860             }
861 
862             mOver = (int) distance;
863             mState = BALLISTIC;
864             mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
865             mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
866         }
867 
continueWhenFinished()868         boolean continueWhenFinished() {
869             switch (mState) {
870                 case SPLINE:
871                     // Duration from start to null velocity
872                     if (mDuration < mSplineDuration) {
873                         // If the animation was clamped, we reached the edge
874                         mCurrentPosition = mStart = mFinal;
875                         // TODO Better compute speed when edge was reached
876                         mVelocity = (int) mCurrVelocity;
877                         mDeceleration = getDeceleration(mVelocity);
878                         mStartTime += mDuration;
879                         onEdgeReached();
880                     } else {
881                         // Normal stop, no need to continue
882                         return false;
883                     }
884                     break;
885                 case BALLISTIC:
886                     mStartTime += mDuration;
887                     startSpringback(mFinal, mStart, 0);
888                     break;
889                 case CUBIC:
890                     return false;
891             }
892 
893             update();
894             return true;
895         }
896 
897         /*
898          * Update the current position and velocity for current time. Returns
899          * true if update has been done and false if animation duration has been
900          * reached.
901          */
update()902         boolean update() {
903             final long time = AnimationUtils.currentAnimationTimeMillis();
904             final long currentTime = time - mStartTime;
905 
906             if (currentTime == 0) {
907                 // Skip work but report that we're still going if we have a nonzero duration.
908                 return mDuration > 0;
909             }
910             if (currentTime > mDuration) {
911                 return false;
912             }
913 
914             double distance = 0.0;
915             switch (mState) {
916                 case SPLINE: {
917                     final float t = (float) currentTime / mSplineDuration;
918                     final int index = (int) (NB_SAMPLES * t);
919                     float distanceCoef = 1.f;
920                     float velocityCoef = 0.f;
921                     if (index < NB_SAMPLES) {
922                         final float t_inf = (float) index / NB_SAMPLES;
923                         final float t_sup = (float) (index + 1) / NB_SAMPLES;
924                         final float d_inf = SPLINE_POSITION[index];
925                         final float d_sup = SPLINE_POSITION[index + 1];
926                         velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
927                         distanceCoef = d_inf + (t - t_inf) * velocityCoef;
928                     }
929 
930                     distance = distanceCoef * mSplineDistance;
931                     mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
932                     break;
933                 }
934 
935                 case BALLISTIC: {
936                     final float t = currentTime / 1000.0f;
937                     mCurrVelocity = mVelocity + mDeceleration * t;
938                     distance = mVelocity * t + mDeceleration * t * t / 2.0f;
939                     break;
940                 }
941 
942                 case CUBIC: {
943                     final float t = (float) (currentTime) / mDuration;
944                     final float t2 = t * t;
945                     final float sign = Math.signum(mVelocity);
946                     distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
947                     mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
948                     break;
949                 }
950             }
951 
952             mCurrentPosition = mStart + (int) Math.round(distance);
953 
954             return true;
955         }
956     }
957 }
958