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.view.ViewConfiguration;
23import android.view.animation.AnimationUtils;
24import 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 */
62public 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     */
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     */
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     */
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     */
190    public final void setFriction(float friction) {
191        mDeceleration = computeDeceleration(friction);
192        mFlingFriction = friction;
193    }
194
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
474    private double getSplineDeceleration(float velocity) {
475        return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
476    }
477
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
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     */
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     */
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     */
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     */
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     */
547    public void setFinalY(int newY) {
548        mFinalY = newY;
549        mDeltaY = mFinalY - mStartY;
550        mFinished = false;
551    }
552
553    /**
554     * @hide
555     */
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
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
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