1 /*
2  * Copyright (C) 2006 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.view.ViewConfiguration;
24 import android.view.animation.AnimationUtils;
25 import android.view.animation.Interpolator;
26 
27 
28 /**
29  * <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller}
30  * or {@link OverScroller}) to collect the data you need to produce a scrolling
31  * animation&mdash;for example, in response to a fling gesture. Scrollers track
32  * scroll offsets for you over time, but they don't automatically apply those
33  * positions to your view. It's your responsibility to get and apply new
34  * coordinates at a rate that will make the scrolling animation look smooth.</p>
35  *
36  * <p>Here is a simple example:</p>
37  *
38  * <pre> private Scroller mScroller = new Scroller(context);
39  * ...
40  * public void zoomIn() {
41  *     // Revert any animation currently in progress
42  *     mScroller.forceFinished(true);
43  *     // Start scrolling by providing a starting point and
44  *     // the distance to travel
45  *     mScroller.startScroll(0, 0, 100, 0);
46  *     // Invalidate to request a redraw
47  *     invalidate();
48  * }</pre>
49  *
50  * <p>To track the changing positions of the x/y coordinates, use
51  * {@link #computeScrollOffset}. The method returns a boolean to indicate
52  * whether the scroller is finished. If it isn't, it means that a fling or
53  * programmatic pan operation is still in progress. You can use this method to
54  * find the current offsets of the x and y coordinates, for example:</p>
55  *
56  * <pre>if (mScroller.computeScrollOffset()) {
57  *     // Get current x and y positions
58  *     int currX = mScroller.getCurrX();
59  *     int currY = mScroller.getCurrY();
60  *    ...
61  * }</pre>
62  */
63 public class Scroller  {
64     @UnsupportedAppUsage
65     private final Interpolator mInterpolator;
66 
67     private int mMode;
68 
69     private int mStartX;
70     private int mStartY;
71     private int mFinalX;
72     private int mFinalY;
73 
74     private int mMinX;
75     private int mMaxX;
76     private int mMinY;
77     private int mMaxY;
78 
79     private int mCurrX;
80     private int mCurrY;
81     private long mStartTime;
82     @UnsupportedAppUsage
83     private int mDuration;
84     private float mDurationReciprocal;
85     private float mDeltaX;
86     private float mDeltaY;
87     private boolean mFinished;
88     private boolean mFlywheel;
89 
90     private float mVelocity;
91     private float mCurrVelocity;
92     private int mDistance;
93 
94     private float mFlingFriction = ViewConfiguration.getScrollFriction();
95 
96     private static final int DEFAULT_DURATION = 250;
97     private static final int SCROLL_MODE = 0;
98     private static final int FLING_MODE = 1;
99 
100     @UnsupportedAppUsage
101     private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
102     @UnsupportedAppUsage
103     private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
104     private static final float START_TENSION = 0.5f;
105     private static final float END_TENSION = 1.0f;
106     private static final float P1 = START_TENSION * INFLEXION;
107     private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
108 
109     private static final int NB_SAMPLES = 100;
110     private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
111     private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
112 
113     @UnsupportedAppUsage
114     private float mDeceleration;
115     private final float mPpi;
116 
117     // A context-specific coefficient adjusted to physical values.
118     @UnsupportedAppUsage
119     private float mPhysicalCoeff;
120 
121     static {
122         float x_min = 0.0f;
123         float y_min = 0.0f;
124         for (int i = 0; i < NB_SAMPLES; i++) {
125             final float alpha = (float) i / NB_SAMPLES;
126 
127             float x_max = 1.0f;
128             float x, tx, coef;
129             while (true) {
130                 x = x_min + (x_max - x_min) / 2.0f;
131                 coef = 3.0f * x * (1.0f - x);
132                 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
133                 if (Math.abs(tx - alpha) < 1E-5) break;
134                 if (tx > alpha) x_max = x;
135                 else x_min = x;
136             }
137             SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
138 
139             float y_max = 1.0f;
140             float y, dy;
141             while (true) {
142                 y = y_min + (y_max - y_min) / 2.0f;
143                 coef = 3.0f * y * (1.0f - y);
144                 dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
145                 if (Math.abs(dy - alpha) < 1E-5) break;
146                 if (dy > alpha) y_max = y;
147                 else y_min = y;
148             }
149             SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
150         }
151         SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
152     }
153 
154     /**
155      * Create a Scroller with the default duration and interpolator.
156      */
Scroller(Context context)157     public Scroller(Context context) {
158         this(context, null);
159     }
160 
161     /**
162      * Create a Scroller with the specified interpolator. If the interpolator is
163      * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
164      * be in effect for apps targeting Honeycomb or newer.
165      */
Scroller(Context context, Interpolator interpolator)166     public Scroller(Context context, Interpolator interpolator) {
167         this(context, interpolator,
168                 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
169     }
170 
171     /**
172      * Create a Scroller with the specified interpolator. If the interpolator is
173      * null, the default (viscous) interpolator will be used. Specify whether or
174      * not to support progressive "flywheel" behavior in flinging.
175      */
Scroller(Context context, Interpolator interpolator, boolean flywheel)176     public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
177         mFinished = true;
178         if (interpolator == null) {
179             mInterpolator = new ViscousFluidInterpolator();
180         } else {
181             mInterpolator = interpolator;
182         }
183         mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
184         mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
185         mFlywheel = flywheel;
186 
187         mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
188     }
189 
190     /**
191      * The amount of friction applied to flings. The default value
192      * is {@link ViewConfiguration#getScrollFriction}.
193      *
194      * @param friction A scalar dimension-less value representing the coefficient of
195      *         friction.
196      */
setFriction(float friction)197     public final void setFriction(float friction) {
198         mDeceleration = computeDeceleration(friction);
199         mFlingFriction = friction;
200     }
201 
computeDeceleration(float friction)202     private float computeDeceleration(float friction) {
203         return SensorManager.GRAVITY_EARTH   // g (m/s^2)
204                       * 39.37f               // inch/meter
205                       * mPpi                 // pixels per inch
206                       * friction;
207     }
208 
209     /**
210      *
211      * Returns whether the scroller has finished scrolling.
212      *
213      * @return True if the scroller has finished scrolling, false otherwise.
214      */
isFinished()215     public final boolean isFinished() {
216         return mFinished;
217     }
218 
219     /**
220      * Force the finished field to a particular value.
221      *
222      * @param finished The new finished value.
223      */
forceFinished(boolean finished)224     public final void forceFinished(boolean finished) {
225         mFinished = finished;
226     }
227 
228     /**
229      * Returns how long the scroll event will take, in milliseconds.
230      *
231      * @return The duration of the scroll in milliseconds.
232      */
getDuration()233     public final int getDuration() {
234         return mDuration;
235     }
236 
237     /**
238      * Returns the current X offset in the scroll.
239      *
240      * @return The new X offset as an absolute distance from the origin.
241      */
getCurrX()242     public final int getCurrX() {
243         return mCurrX;
244     }
245 
246     /**
247      * Returns the current Y offset in the scroll.
248      *
249      * @return The new Y offset as an absolute distance from the origin.
250      */
getCurrY()251     public final int getCurrY() {
252         return mCurrY;
253     }
254 
255     /**
256      * Returns the current velocity.
257      *
258      * @return The original velocity less the deceleration. Result may be
259      * negative.
260      */
getCurrVelocity()261     public float getCurrVelocity() {
262         return mMode == FLING_MODE ?
263                 mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f;
264     }
265 
266     /**
267      * Returns the start X offset in the scroll.
268      *
269      * @return The start X offset as an absolute distance from the origin.
270      */
getStartX()271     public final int getStartX() {
272         return mStartX;
273     }
274 
275     /**
276      * Returns the start Y offset in the scroll.
277      *
278      * @return The start Y offset as an absolute distance from the origin.
279      */
getStartY()280     public final int getStartY() {
281         return mStartY;
282     }
283 
284     /**
285      * Returns where the scroll will end. Valid only for "fling" scrolls.
286      *
287      * @return The final X offset as an absolute distance from the origin.
288      */
getFinalX()289     public final int getFinalX() {
290         return mFinalX;
291     }
292 
293     /**
294      * Returns where the scroll will end. Valid only for "fling" scrolls.
295      *
296      * @return The final Y offset as an absolute distance from the origin.
297      */
getFinalY()298     public final int getFinalY() {
299         return mFinalY;
300     }
301 
302     /**
303      * Call this when you want to know the new location.  If it returns true,
304      * the animation is not yet finished.
305      */
computeScrollOffset()306     public boolean computeScrollOffset() {
307         if (mFinished) {
308             return false;
309         }
310 
311         int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
312 
313         if (timePassed < mDuration) {
314             switch (mMode) {
315             case SCROLL_MODE:
316                 final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
317                 mCurrX = mStartX + Math.round(x * mDeltaX);
318                 mCurrY = mStartY + Math.round(x * mDeltaY);
319                 break;
320             case FLING_MODE:
321                 final float t = (float) timePassed / mDuration;
322                 final int index = (int) (NB_SAMPLES * t);
323                 float distanceCoef = 1.f;
324                 float velocityCoef = 0.f;
325                 if (index < NB_SAMPLES) {
326                     final float t_inf = (float) index / NB_SAMPLES;
327                     final float t_sup = (float) (index + 1) / NB_SAMPLES;
328                     final float d_inf = SPLINE_POSITION[index];
329                     final float d_sup = SPLINE_POSITION[index + 1];
330                     velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
331                     distanceCoef = d_inf + (t - t_inf) * velocityCoef;
332                 }
333 
334                 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
335 
336                 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
337                 // Pin to mMinX <= mCurrX <= mMaxX
338                 mCurrX = Math.min(mCurrX, mMaxX);
339                 mCurrX = Math.max(mCurrX, mMinX);
340 
341                 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
342                 // Pin to mMinY <= mCurrY <= mMaxY
343                 mCurrY = Math.min(mCurrY, mMaxY);
344                 mCurrY = Math.max(mCurrY, mMinY);
345 
346                 if (mCurrX == mFinalX && mCurrY == mFinalY) {
347                     mFinished = true;
348                 }
349 
350                 break;
351             }
352         }
353         else {
354             mCurrX = mFinalX;
355             mCurrY = mFinalY;
356             mFinished = true;
357         }
358         return true;
359     }
360 
361     /**
362      * Start scrolling by providing a starting point and the distance to travel.
363      * The scroll will use the default value of 250 milliseconds for the
364      * duration.
365      *
366      * @param startX Starting horizontal scroll offset in pixels. Positive
367      *        numbers will scroll the content to the left.
368      * @param startY Starting vertical scroll offset in pixels. Positive numbers
369      *        will scroll the content up.
370      * @param dx Horizontal distance to travel. Positive numbers will scroll the
371      *        content to the left.
372      * @param dy Vertical distance to travel. Positive numbers will scroll the
373      *        content up.
374      */
startScroll(int startX, int startY, int dx, int dy)375     public void startScroll(int startX, int startY, int dx, int dy) {
376         startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
377     }
378 
379     /**
380      * Start scrolling by providing a starting point, the distance to travel,
381      * and the duration of the scroll.
382      *
383      * @param startX Starting horizontal scroll offset in pixels. Positive
384      *        numbers will scroll the content to the left.
385      * @param startY Starting vertical scroll offset in pixels. Positive numbers
386      *        will scroll the content up.
387      * @param dx Horizontal distance to travel. Positive numbers will scroll the
388      *        content to the left.
389      * @param dy Vertical distance to travel. Positive numbers will scroll the
390      *        content up.
391      * @param duration Duration of the scroll in milliseconds.
392      */
startScroll(int startX, int startY, int dx, int dy, int duration)393     public void startScroll(int startX, int startY, int dx, int dy, int duration) {
394         mMode = SCROLL_MODE;
395         mFinished = false;
396         mDuration = duration;
397         mStartTime = AnimationUtils.currentAnimationTimeMillis();
398         mStartX = startX;
399         mStartY = startY;
400         mFinalX = startX + dx;
401         mFinalY = startY + dy;
402         mDeltaX = dx;
403         mDeltaY = dy;
404         mDurationReciprocal = 1.0f / (float) mDuration;
405     }
406 
407     /**
408      * Start scrolling based on a fling gesture. The distance travelled will
409      * depend on the initial velocity of the fling.
410      *
411      * @param startX Starting point of the scroll (X)
412      * @param startY Starting point of the scroll (Y)
413      * @param velocityX Initial velocity of the fling (X) measured in pixels per
414      *        second.
415      * @param velocityY Initial velocity of the fling (Y) measured in pixels per
416      *        second
417      * @param minX Minimum X value. The scroller will not scroll past this
418      *        point.
419      * @param maxX Maximum X value. The scroller will not scroll past this
420      *        point.
421      * @param minY Minimum Y value. The scroller will not scroll past this
422      *        point.
423      * @param maxY Maximum Y value. The scroller will not scroll past this
424      *        point.
425      */
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)426     public void fling(int startX, int startY, int velocityX, int velocityY,
427             int minX, int maxX, int minY, int maxY) {
428         // Continue a scroll or fling in progress
429         if (mFlywheel && !mFinished) {
430             float oldVel = getCurrVelocity();
431 
432             float dx = (float) (mFinalX - mStartX);
433             float dy = (float) (mFinalY - mStartY);
434             float hyp = (float) Math.hypot(dx, dy);
435 
436             float ndx = dx / hyp;
437             float ndy = dy / hyp;
438 
439             float oldVelocityX = ndx * oldVel;
440             float oldVelocityY = ndy * oldVel;
441             if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
442                     Math.signum(velocityY) == Math.signum(oldVelocityY)) {
443                 velocityX += oldVelocityX;
444                 velocityY += oldVelocityY;
445             }
446         }
447 
448         mMode = FLING_MODE;
449         mFinished = false;
450 
451         float velocity = (float) Math.hypot(velocityX, velocityY);
452 
453         mVelocity = velocity;
454         mDuration = getSplineFlingDuration(velocity);
455         mStartTime = AnimationUtils.currentAnimationTimeMillis();
456         mStartX = startX;
457         mStartY = startY;
458 
459         float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
460         float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
461 
462         double totalDistance = getSplineFlingDistance(velocity);
463         mDistance = (int) (totalDistance * Math.signum(velocity));
464 
465         mMinX = minX;
466         mMaxX = maxX;
467         mMinY = minY;
468         mMaxY = maxY;
469 
470         mFinalX = startX + (int) Math.round(totalDistance * coeffX);
471         // Pin to mMinX <= mFinalX <= mMaxX
472         mFinalX = Math.min(mFinalX, mMaxX);
473         mFinalX = Math.max(mFinalX, mMinX);
474 
475         mFinalY = startY + (int) Math.round(totalDistance * coeffY);
476         // Pin to mMinY <= mFinalY <= mMaxY
477         mFinalY = Math.min(mFinalY, mMaxY);
478         mFinalY = Math.max(mFinalY, mMinY);
479     }
480 
getSplineDeceleration(float velocity)481     private double getSplineDeceleration(float velocity) {
482         return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
483     }
484 
getSplineFlingDuration(float velocity)485     private int getSplineFlingDuration(float velocity) {
486         final double l = getSplineDeceleration(velocity);
487         final double decelMinusOne = DECELERATION_RATE - 1.0;
488         return (int) (1000.0 * Math.exp(l / decelMinusOne));
489     }
490 
getSplineFlingDistance(float velocity)491     private double getSplineFlingDistance(float velocity) {
492         final double l = getSplineDeceleration(velocity);
493         final double decelMinusOne = DECELERATION_RATE - 1.0;
494         return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
495     }
496 
497     /**
498      * Stops the animation. Contrary to {@link #forceFinished(boolean)},
499      * aborting the animating cause the scroller to move to the final x and y
500      * position
501      *
502      * @see #forceFinished(boolean)
503      */
abortAnimation()504     public void abortAnimation() {
505         mCurrX = mFinalX;
506         mCurrY = mFinalY;
507         mFinished = true;
508     }
509 
510     /**
511      * Extend the scroll animation. This allows a running animation to scroll
512      * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
513      *
514      * @param extend Additional time to scroll in milliseconds.
515      * @see #setFinalX(int)
516      * @see #setFinalY(int)
517      */
extendDuration(int extend)518     public void extendDuration(int extend) {
519         int passed = timePassed();
520         mDuration = passed + extend;
521         mDurationReciprocal = 1.0f / mDuration;
522         mFinished = false;
523     }
524 
525     /**
526      * Returns the time elapsed since the beginning of the scrolling.
527      *
528      * @return The elapsed time in milliseconds.
529      */
timePassed()530     public int timePassed() {
531         return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
532     }
533 
534     /**
535      * Sets the final position (X) for this scroller.
536      *
537      * @param newX The new X offset as an absolute distance from the origin.
538      * @see #extendDuration(int)
539      * @see #setFinalY(int)
540      */
setFinalX(int newX)541     public void setFinalX(int newX) {
542         mFinalX = newX;
543         mDeltaX = mFinalX - mStartX;
544         mFinished = false;
545     }
546 
547     /**
548      * Sets the final position (Y) for this scroller.
549      *
550      * @param newY The new Y offset as an absolute distance from the origin.
551      * @see #extendDuration(int)
552      * @see #setFinalX(int)
553      */
setFinalY(int newY)554     public void setFinalY(int newY) {
555         mFinalY = newY;
556         mDeltaY = mFinalY - mStartY;
557         mFinished = false;
558     }
559 
560     /**
561      * @hide
562      */
isScrollingInDirection(float xvel, float yvel)563     public boolean isScrollingInDirection(float xvel, float yvel) {
564         return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
565                 Math.signum(yvel) == Math.signum(mFinalY - mStartY);
566     }
567 
568     static class ViscousFluidInterpolator implements Interpolator {
569         /** Controls the viscous fluid effect (how much of it). */
570         private static final float VISCOUS_FLUID_SCALE = 8.0f;
571 
572         private static final float VISCOUS_FLUID_NORMALIZE;
573         private static final float VISCOUS_FLUID_OFFSET;
574 
575         static {
576 
577             // must be set to 1.0 (used in viscousFluid())
578             VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
579             // account for very small floating-point error
580             VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
581         }
582 
viscousFluid(float x)583         private static float viscousFluid(float x) {
584             x *= VISCOUS_FLUID_SCALE;
585             if (x < 1.0f) {
586                 x -= (1.0f - (float)Math.exp(-x));
587             } else {
588                 float start = 0.36787944117f;   // 1/e == exp(-1)
589                 x = 1.0f - (float)Math.exp(1.0f - x);
590                 x = start + x * (1.0f - start);
591             }
592             return x;
593         }
594 
595         @Override
getInterpolation(float input)596         public float getInterpolation(float input) {
597             final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
598             if (interpolated > 0) {
599                 return interpolated + VISCOUS_FLUID_OFFSET;
600             }
601             return interpolated;
602         }
603     }
604 }
605