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