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
17package android.widget;
18
19import android.content.Context;
20import android.hardware.SensorManager;
21import android.os.Build;
22import android.util.FloatMath;
23import android.view.ViewConfiguration;
24import android.view.animation.AnimationUtils;
25import 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 */
63public 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     */
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     */
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     */
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     */
196    public final void setFriction(float friction) {
197        mDeceleration = computeDeceleration(friction);
198        mFlingFriction = friction;
199    }
200
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
486    private double getSplineDeceleration(float velocity) {
487        return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
488    }
489
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
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
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     */
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     */
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     */
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     */
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     */
573    public void setFinalY(int newY) {
574        mFinalY = newY;
575        mDeltaY = mFinalY - mStartY;
576        mFinished = false;
577    }
578
579    /**
580     * @hide
581     */
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