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