• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.util.FloatMath;
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     private int mMode;
65 
66     private int mStartX;
67     private int mStartY;
68     private int mFinalX;
69     private int mFinalY;
70 
71     private int mMinX;
72     private int mMaxX;
73     private int mMinY;
74     private int mMaxY;
75 
76     private int mCurrX;
77     private int mCurrY;
78     private long mStartTime;
79     private int mDuration;
80     private float mDurationReciprocal;
81     private float mDeltaX;
82     private float mDeltaY;
83     private boolean mFinished;
84     private Interpolator mInterpolator;
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         // This controls the viscous fluid effect (how much of it)
147         sViscousFluidScale = 8.0f;
148         // must be set to 1.0 (used in viscousFluid())
149         sViscousFluidNormalize = 1.0f;
150         sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
151 
152     }
153 
154     private static float sViscousFluidScale;
155     private static float sViscousFluidNormalize;
156 
157     /**
158      * Create a Scroller with the default duration and interpolator.
159      */
Scroller(Context context)160     public Scroller(Context context) {
161         this(context, null);
162     }
163 
164     /**
165      * Create a Scroller with the specified interpolator. If the interpolator is
166      * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
167      * be in effect for apps targeting Honeycomb or newer.
168      */
Scroller(Context context, Interpolator interpolator)169     public Scroller(Context context, Interpolator interpolator) {
170         this(context, interpolator,
171                 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
172     }
173 
174     /**
175      * Create a Scroller with the specified interpolator. If the interpolator is
176      * null, the default (viscous) interpolator will be used. Specify whether or
177      * not to support progressive "flywheel" behavior in flinging.
178      */
Scroller(Context context, Interpolator interpolator, boolean flywheel)179     public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
180         mFinished = true;
181         mInterpolator = interpolator;
182         mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
183         mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
184         mFlywheel = flywheel;
185 
186         mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
187     }
188 
189     /**
190      * The amount of friction applied to flings. The default value
191      * is {@link ViewConfiguration#getScrollFriction}.
192      *
193      * @param friction A scalar dimension-less value representing the coefficient of
194      *         friction.
195      */
setFriction(float friction)196     public final void setFriction(float friction) {
197         mDeceleration = computeDeceleration(friction);
198         mFlingFriction = friction;
199     }
200 
computeDeceleration(float friction)201     private float computeDeceleration(float friction) {
202         return SensorManager.GRAVITY_EARTH   // g (m/s^2)
203                       * 39.37f               // inch/meter
204                       * mPpi                 // pixels per inch
205                       * friction;
206     }
207 
208     /**
209      *
210      * Returns whether the scroller has finished scrolling.
211      *
212      * @return True if the scroller has finished scrolling, false otherwise.
213      */
isFinished()214     public final boolean isFinished() {
215         return mFinished;
216     }
217 
218     /**
219      * Force the finished field to a particular value.
220      *
221      * @param finished The new finished value.
222      */
forceFinished(boolean finished)223     public final void forceFinished(boolean finished) {
224         mFinished = finished;
225     }
226 
227     /**
228      * Returns how long the scroll event will take, in milliseconds.
229      *
230      * @return The duration of the scroll in milliseconds.
231      */
getDuration()232     public final int getDuration() {
233         return mDuration;
234     }
235 
236     /**
237      * Returns the current X offset in the scroll.
238      *
239      * @return The new X offset as an absolute distance from the origin.
240      */
getCurrX()241     public final int getCurrX() {
242         return mCurrX;
243     }
244 
245     /**
246      * Returns the current Y offset in the scroll.
247      *
248      * @return The new Y offset as an absolute distance from the origin.
249      */
getCurrY()250     public final int getCurrY() {
251         return mCurrY;
252     }
253 
254     /**
255      * Returns the current velocity.
256      *
257      * @return The original velocity less the deceleration. Result may be
258      * negative.
259      */
getCurrVelocity()260     public float getCurrVelocity() {
261         return mMode == FLING_MODE ?
262                 mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f;
263     }
264 
265     /**
266      * Returns the start X offset in the scroll.
267      *
268      * @return The start X offset as an absolute distance from the origin.
269      */
getStartX()270     public final int getStartX() {
271         return mStartX;
272     }
273 
274     /**
275      * Returns the start Y offset in the scroll.
276      *
277      * @return The start Y offset as an absolute distance from the origin.
278      */
getStartY()279     public final int getStartY() {
280         return mStartY;
281     }
282 
283     /**
284      * Returns where the scroll will end. Valid only for "fling" scrolls.
285      *
286      * @return The final X offset as an absolute distance from the origin.
287      */
getFinalX()288     public final int getFinalX() {
289         return mFinalX;
290     }
291 
292     /**
293      * Returns where the scroll will end. Valid only for "fling" scrolls.
294      *
295      * @return The final Y offset as an absolute distance from the origin.
296      */
getFinalY()297     public final int getFinalY() {
298         return mFinalY;
299     }
300 
301     /**
302      * Call this when you want to know the new location.  If it returns true,
303      * the animation is not yet finished.
304      */
computeScrollOffset()305     public boolean computeScrollOffset() {
306         if (mFinished) {
307             return false;
308         }
309 
310         int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
311 
312         if (timePassed < mDuration) {
313             switch (mMode) {
314             case SCROLL_MODE:
315                 float x = timePassed * mDurationReciprocal;
316 
317                 if (mInterpolator == null)
318                     x = viscousFluid(x);
319                 else
320                     x = mInterpolator.getInterpolation(x);
321 
322                 mCurrX = mStartX + Math.round(x * mDeltaX);
323                 mCurrY = mStartY + Math.round(x * mDeltaY);
324                 break;
325             case FLING_MODE:
326                 final float t = (float) timePassed / mDuration;
327                 final int index = (int) (NB_SAMPLES * t);
328                 float distanceCoef = 1.f;
329                 float velocityCoef = 0.f;
330                 if (index < NB_SAMPLES) {
331                     final float t_inf = (float) index / NB_SAMPLES;
332                     final float t_sup = (float) (index + 1) / NB_SAMPLES;
333                     final float d_inf = SPLINE_POSITION[index];
334                     final float d_sup = SPLINE_POSITION[index + 1];
335                     velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
336                     distanceCoef = d_inf + (t - t_inf) * velocityCoef;
337                 }
338 
339                 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
340 
341                 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
342                 // Pin to mMinX <= mCurrX <= mMaxX
343                 mCurrX = Math.min(mCurrX, mMaxX);
344                 mCurrX = Math.max(mCurrX, mMinX);
345 
346                 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
347                 // Pin to mMinY <= mCurrY <= mMaxY
348                 mCurrY = Math.min(mCurrY, mMaxY);
349                 mCurrY = Math.max(mCurrY, mMinY);
350 
351                 if (mCurrX == mFinalX && mCurrY == mFinalY) {
352                     mFinished = true;
353                 }
354 
355                 break;
356             }
357         }
358         else {
359             mCurrX = mFinalX;
360             mCurrY = mFinalY;
361             mFinished = true;
362         }
363         return true;
364     }
365 
366     /**
367      * Start scrolling by providing a starting point and the distance to travel.
368      * The scroll will use the default value of 250 milliseconds for the
369      * duration.
370      *
371      * @param startX Starting horizontal scroll offset in pixels. Positive
372      *        numbers will scroll the content to the left.
373      * @param startY Starting vertical scroll offset in pixels. Positive numbers
374      *        will scroll the content up.
375      * @param dx Horizontal distance to travel. Positive numbers will scroll the
376      *        content to the left.
377      * @param dy Vertical distance to travel. Positive numbers will scroll the
378      *        content up.
379      */
startScroll(int startX, int startY, int dx, int dy)380     public void startScroll(int startX, int startY, int dx, int dy) {
381         startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
382     }
383 
384     /**
385      * Start scrolling by providing a starting point, the distance to travel,
386      * and the duration of the scroll.
387      *
388      * @param startX Starting horizontal scroll offset in pixels. Positive
389      *        numbers will scroll the content to the left.
390      * @param startY Starting vertical scroll offset in pixels. Positive numbers
391      *        will scroll the content up.
392      * @param dx Horizontal distance to travel. Positive numbers will scroll the
393      *        content to the left.
394      * @param dy Vertical distance to travel. Positive numbers will scroll the
395      *        content up.
396      * @param duration Duration of the scroll in milliseconds.
397      */
startScroll(int startX, int startY, int dx, int dy, int duration)398     public void startScroll(int startX, int startY, int dx, int dy, int duration) {
399         mMode = SCROLL_MODE;
400         mFinished = false;
401         mDuration = duration;
402         mStartTime = AnimationUtils.currentAnimationTimeMillis();
403         mStartX = startX;
404         mStartY = startY;
405         mFinalX = startX + dx;
406         mFinalY = startY + dy;
407         mDeltaX = dx;
408         mDeltaY = dy;
409         mDurationReciprocal = 1.0f / (float) mDuration;
410     }
411 
412     /**
413      * Start scrolling based on a fling gesture. The distance travelled will
414      * depend on the initial velocity of the fling.
415      *
416      * @param startX Starting point of the scroll (X)
417      * @param startY Starting point of the scroll (Y)
418      * @param velocityX Initial velocity of the fling (X) measured in pixels per
419      *        second.
420      * @param velocityY Initial velocity of the fling (Y) measured in pixels per
421      *        second
422      * @param minX Minimum X value. The scroller will not scroll past this
423      *        point.
424      * @param maxX Maximum X value. The scroller will not scroll past this
425      *        point.
426      * @param minY Minimum Y value. The scroller will not scroll past this
427      *        point.
428      * @param maxY Maximum Y value. The scroller will not scroll past this
429      *        point.
430      */
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)431     public void fling(int startX, int startY, int velocityX, int velocityY,
432             int minX, int maxX, int minY, int maxY) {
433         // Continue a scroll or fling in progress
434         if (mFlywheel && !mFinished) {
435             float oldVel = getCurrVelocity();
436 
437             float dx = (float) (mFinalX - mStartX);
438             float dy = (float) (mFinalY - mStartY);
439             float hyp = FloatMath.sqrt(dx * dx + dy * dy);
440 
441             float ndx = dx / hyp;
442             float ndy = dy / hyp;
443 
444             float oldVelocityX = ndx * oldVel;
445             float oldVelocityY = ndy * oldVel;
446             if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
447                     Math.signum(velocityY) == Math.signum(oldVelocityY)) {
448                 velocityX += oldVelocityX;
449                 velocityY += oldVelocityY;
450             }
451         }
452 
453         mMode = FLING_MODE;
454         mFinished = false;
455 
456         float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
457 
458         mVelocity = velocity;
459         mDuration = getSplineFlingDuration(velocity);
460         mStartTime = AnimationUtils.currentAnimationTimeMillis();
461         mStartX = startX;
462         mStartY = startY;
463 
464         float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
465         float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
466 
467         double totalDistance = getSplineFlingDistance(velocity);
468         mDistance = (int) (totalDistance * Math.signum(velocity));
469 
470         mMinX = minX;
471         mMaxX = maxX;
472         mMinY = minY;
473         mMaxY = maxY;
474 
475         mFinalX = startX + (int) Math.round(totalDistance * coeffX);
476         // Pin to mMinX <= mFinalX <= mMaxX
477         mFinalX = Math.min(mFinalX, mMaxX);
478         mFinalX = Math.max(mFinalX, mMinX);
479 
480         mFinalY = startY + (int) Math.round(totalDistance * coeffY);
481         // Pin to mMinY <= mFinalY <= mMaxY
482         mFinalY = Math.min(mFinalY, mMaxY);
483         mFinalY = Math.max(mFinalY, mMinY);
484     }
485 
getSplineDeceleration(float velocity)486     private double getSplineDeceleration(float velocity) {
487         return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
488     }
489 
getSplineFlingDuration(float velocity)490     private int getSplineFlingDuration(float velocity) {
491         final double l = getSplineDeceleration(velocity);
492         final double decelMinusOne = DECELERATION_RATE - 1.0;
493         return (int) (1000.0 * Math.exp(l / decelMinusOne));
494     }
495 
getSplineFlingDistance(float velocity)496     private double getSplineFlingDistance(float velocity) {
497         final double l = getSplineDeceleration(velocity);
498         final double decelMinusOne = DECELERATION_RATE - 1.0;
499         return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
500     }
501 
viscousFluid(float x)502     static float viscousFluid(float x)
503     {
504         x *= sViscousFluidScale;
505         if (x < 1.0f) {
506             x -= (1.0f - (float)Math.exp(-x));
507         } else {
508             float start = 0.36787944117f;   // 1/e == exp(-1)
509             x = 1.0f - (float)Math.exp(1.0f - x);
510             x = start + x * (1.0f - start);
511         }
512         x *= sViscousFluidNormalize;
513         return x;
514     }
515 
516     /**
517      * Stops the animation. Contrary to {@link #forceFinished(boolean)},
518      * aborting the animating cause the scroller to move to the final x and y
519      * position
520      *
521      * @see #forceFinished(boolean)
522      */
abortAnimation()523     public void abortAnimation() {
524         mCurrX = mFinalX;
525         mCurrY = mFinalY;
526         mFinished = true;
527     }
528 
529     /**
530      * Extend the scroll animation. This allows a running animation to scroll
531      * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
532      *
533      * @param extend Additional time to scroll in milliseconds.
534      * @see #setFinalX(int)
535      * @see #setFinalY(int)
536      */
extendDuration(int extend)537     public void extendDuration(int extend) {
538         int passed = timePassed();
539         mDuration = passed + extend;
540         mDurationReciprocal = 1.0f / mDuration;
541         mFinished = false;
542     }
543 
544     /**
545      * Returns the time elapsed since the beginning of the scrolling.
546      *
547      * @return The elapsed time in milliseconds.
548      */
timePassed()549     public int timePassed() {
550         return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
551     }
552 
553     /**
554      * Sets the final position (X) for this scroller.
555      *
556      * @param newX The new X offset as an absolute distance from the origin.
557      * @see #extendDuration(int)
558      * @see #setFinalY(int)
559      */
setFinalX(int newX)560     public void setFinalX(int newX) {
561         mFinalX = newX;
562         mDeltaX = mFinalX - mStartX;
563         mFinished = false;
564     }
565 
566     /**
567      * Sets the final position (Y) for this scroller.
568      *
569      * @param newY The new Y offset as an absolute distance from the origin.
570      * @see #extendDuration(int)
571      * @see #setFinalX(int)
572      */
setFinalY(int newY)573     public void setFinalY(int newY) {
574         mFinalY = newY;
575         mDeltaY = mFinalY - mStartY;
576         mFinished = false;
577     }
578 
579     /**
580      * @hide
581      */
isScrollingInDirection(float xvel, float yvel)582     public boolean isScrollingInDirection(float xvel, float yvel) {
583         return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
584                 Math.signum(yvel) == Math.signum(mFinalY - mStartY);
585     }
586 }
587