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