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