Scroller.java revision 0ee0a2ea57197cb2f03905454098d9a7a309f77b
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
19
20import android.content.Context;
21import android.hardware.SensorManager;
22import android.util.FloatMath;
23import android.view.ViewConfiguration;
24import android.view.animation.AnimationUtils;
25import android.view.animation.Interpolator;
26
27
28/**
29 * This class encapsulates scrolling.  The duration of the scroll
30 * is either specified along with the distance or depends on the initial fling velocity.
31 * Past this time, the scrolling is automatically moved to its final stage and
32 * computeScrollOffset() will always return false to indicate that scrolling is over.
33 */
34public class Scroller  {
35    int mMode;
36
37    MagneticScroller mScrollerX;
38    MagneticScroller mScrollerY;
39
40    private final Interpolator mInterpolator;
41
42    static final int DEFAULT_DURATION = 250;
43    static final int SCROLL_MODE = 0;
44    static final int FLING_MODE = 1;
45
46    // This controls the viscous fluid effect (how much of it)
47    private final static float VISCOUS_FLUID_SCALE = 8.0f;
48    private static float VISCOUS_FLUID_NORMALIZE;
49
50    static {
51        // Set a neutral value that will be used in the next call to viscousFluid().
52        VISCOUS_FLUID_NORMALIZE = 1.0f;
53        VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
54    }
55
56    /**
57     * Create a Scroller with a viscous fluid scroll interpolator.
58     */
59    public Scroller(Context context) {
60        this(context, null);
61    }
62
63    /**
64     * Create a Scroller with the specified interpolator. If the interpolator is
65     * null, the default (viscous) interpolator will be used.
66     */
67    public Scroller(Context context, Interpolator interpolator) {
68        instantiateScrollers();
69        MagneticScroller.initializeFromContext(context);
70
71        mInterpolator = interpolator;
72    }
73
74    void instantiateScrollers() {
75        mScrollerX = new MagneticScroller();
76        mScrollerY = new MagneticScroller();
77    }
78
79    /**
80     *
81     * Returns whether the scroller has finished scrolling.
82     *
83     * @return True if the scroller has finished scrolling, false otherwise.
84     */
85    public final boolean isFinished() {
86        return mScrollerX.mFinished && mScrollerY.mFinished;
87    }
88
89    /**
90     * Force the finished field to a particular value.
91     *
92     * @param finished The new finished value.
93     */
94    public final void forceFinished(boolean finished) {
95        mScrollerX.mFinished = mScrollerY.mFinished = finished;
96    }
97
98    /**
99     * Returns how long the scroll event will take, in milliseconds.
100     *
101     * @return The duration of the scroll in milliseconds.
102     */
103    public final int getDuration() {
104        return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
105    }
106
107    /**
108     * Returns the current X offset in the scroll.
109     *
110     * @return The new X offset as an absolute distance from the origin.
111     */
112    public final int getCurrX() {
113        return mScrollerX.mCurrentPosition;
114    }
115
116    /**
117     * Returns the current Y offset in the scroll.
118     *
119     * @return The new Y offset as an absolute distance from the origin.
120     */
121    public final int getCurrY() {
122        return mScrollerY.mCurrentPosition;
123    }
124
125    /**
126     * @hide
127     * Returns the current velocity.
128     *
129     * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
130     */
131    public float getCurrVelocity() {
132        float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity;
133        squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity;
134        return FloatMath.sqrt(squaredNorm);
135    }
136
137    /**
138     * Returns the start X offset in the scroll.
139     *
140     * @return The start X offset as an absolute distance from the origin.
141     */
142    public final int getStartX() {
143        return mScrollerX.mStart;
144    }
145
146    /**
147     * Returns the start Y offset in the scroll.
148     *
149     * @return The start Y offset as an absolute distance from the origin.
150     */
151    public final int getStartY() {
152        return mScrollerY.mStart;
153    }
154
155    /**
156     * Returns where the scroll will end. Valid only for "fling" scrolls.
157     *
158     * @return The final X offset as an absolute distance from the origin.
159     */
160    public final int getFinalX() {
161        return mScrollerX.mFinal;
162    }
163
164    /**
165     * Returns where the scroll will end. Valid only for "fling" scrolls.
166     *
167     * @return The final Y offset as an absolute distance from the origin.
168     */
169    public final int getFinalY() {
170        return mScrollerY.mFinal;
171    }
172
173    /**
174     * Call this when you want to know the new location. If it returns true, the
175     * animation is not yet finished.
176     */
177    public boolean computeScrollOffset() {
178        if (isFinished()) {
179            return false;
180        }
181
182        switch (mMode) {
183            case SCROLL_MODE:
184                long time = AnimationUtils.currentAnimationTimeMillis();
185                // Any scroller can be used for time, since they were started
186                // together in scroll mode. We use X here.
187                final long elapsedTime = time - mScrollerX.mStartTime;
188
189                final int duration = mScrollerX.mDuration;
190                if (elapsedTime < duration) {
191                    float q = (float) (elapsedTime) / duration;
192
193                    if (mInterpolator == null)
194                        q = viscousFluid(q);
195                    else
196                        q = mInterpolator.getInterpolation(q);
197
198                    mScrollerX.updateScroll(q);
199                    mScrollerY.updateScroll(q);
200                } else {
201                    abortAnimation();
202                }
203                break;
204
205            case FLING_MODE:
206                if (!mScrollerX.mFinished) {
207                    if (!mScrollerX.update()) {
208                        if (!mScrollerX.continueWhenFinished()) {
209                            mScrollerX.finish();
210                        }
211                    }
212                }
213
214                if (!mScrollerY.mFinished) {
215                    if (!mScrollerY.update()) {
216                        if (!mScrollerY.continueWhenFinished()) {
217                            mScrollerY.finish();
218                        }
219                    }
220                }
221
222                break;
223        }
224
225        return true;
226    }
227
228    /**
229     * Start scrolling by providing a starting point and the distance to travel.
230     * The scroll will use the default value of 250 milliseconds for the
231     * duration.
232     *
233     * @param startX Starting horizontal scroll offset in pixels. Positive
234     *        numbers will scroll the content to the left.
235     * @param startY Starting vertical scroll offset in pixels. Positive numbers
236     *        will scroll the content up.
237     * @param dx Horizontal distance to travel. Positive numbers will scroll the
238     *        content to the left.
239     * @param dy Vertical distance to travel. Positive numbers will scroll the
240     *        content up.
241     */
242    public void startScroll(int startX, int startY, int dx, int dy) {
243        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
244    }
245
246    /**
247     * Start scrolling by providing a starting point and the distance to travel.
248     *
249     * @param startX Starting horizontal scroll offset in pixels. Positive
250     *        numbers will scroll the content to the left.
251     * @param startY Starting vertical scroll offset in pixels. Positive numbers
252     *        will scroll the content up.
253     * @param dx Horizontal distance to travel. Positive numbers will scroll the
254     *        content to the left.
255     * @param dy Vertical distance to travel. Positive numbers will scroll the
256     *        content up.
257     * @param duration Duration of the scroll in milliseconds.
258     */
259    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
260        mMode = SCROLL_MODE;
261        mScrollerX.startScroll(startX, dx, duration);
262        mScrollerY.startScroll(startY, dy, duration);
263    }
264
265    /**
266     * Start scrolling based on a fling gesture. The distance traveled will
267     * depend on the initial velocity of the fling. Velocity is slowed down by a
268     * constant deceleration until it reaches 0 or the limits are reached.
269     *
270     * @param startX Starting point of the scroll (X)
271     * @param startY Starting point of the scroll (Y)
272     * @param velocityX Initial velocity of the fling (X) measured in pixels per
273     *            second.
274     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
275     *            second.
276     * @param minX Minimum X value. The scroller will not scroll past this
277     *            point.
278     * @param maxX Maximum X value. The scroller will not scroll past this
279     *            point.
280     * @param minY Minimum Y value. The scroller will not scroll past this
281     *            point.
282     * @param maxY Maximum Y value. The scroller will not scroll past this
283     *            point.
284     */
285    public void fling(int startX, int startY, int velocityX, int velocityY,
286            int minX, int maxX, int minY, int maxY) {
287        mMode = FLING_MODE;
288        mScrollerX.fling(startX, velocityX, minX, maxX);
289        mScrollerY.fling(startY, velocityY, minY, maxY);
290    }
291
292    private static float viscousFluid(float x) {
293        x *= VISCOUS_FLUID_SCALE;
294        if (x < 1.0f) {
295            x -= (1.0f - (float)Math.exp(-x));
296        } else {
297            float start = 0.36787944117f;   // 1/e == exp(-1)
298            x = 1.0f - (float)Math.exp(1.0f - x);
299            x = start + x * (1.0f - start);
300        }
301        x *= VISCOUS_FLUID_NORMALIZE;
302        return x;
303    }
304
305    /**
306     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
307     * aborting the animating cause the scroller to move to the final x and y
308     * position
309     *
310     * @see #forceFinished(boolean)
311     */
312    public void abortAnimation() {
313        mScrollerX.finish();
314        mScrollerY.finish();
315    }
316
317    /**
318     * Extend the scroll animation. This allows a running animation to scroll
319     * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
320     *
321     * @param extend Additional time to scroll in milliseconds.
322     * @see #setFinalX(int)
323     * @see #setFinalY(int)
324     */
325    public void extendDuration(int extend) {
326        mScrollerX.extendDuration(extend);
327        mScrollerY.extendDuration(extend);
328    }
329
330    /**
331     * Returns the time elapsed since the beginning of the scrolling.
332     *
333     * @return The elapsed time in milliseconds.
334     */
335    public int timePassed() {
336        final long time = AnimationUtils.currentAnimationTimeMillis();
337        final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
338        return (int) (time - startTime);
339    }
340
341    /**
342     * Sets the final position (X) for this scroller.
343     *
344     * @param newX The new X offset as an absolute distance from the origin.
345     * @see #extendDuration(int)
346     * @see #setFinalY(int)
347     */
348    public void setFinalX(int newX) {
349        mScrollerX.setFinalPosition(newX);
350    }
351
352    /**
353     * Sets the final position (Y) for this scroller.
354     *
355     * @param newY The new Y offset as an absolute distance from the origin.
356     * @see #extendDuration(int)
357     * @see #setFinalX(int)
358     */
359    public void setFinalY(int newY) {
360        mScrollerY.setFinalPosition(newY);
361    }
362
363    static class MagneticScroller {
364        // Initial position
365        int mStart;
366
367        // Current position
368        int mCurrentPosition;
369
370        // Final position
371        int mFinal;
372
373        // Initial velocity
374        int mVelocity;
375
376        // Current velocity
377        float mCurrVelocity;
378
379        // Constant current deceleration
380        float mDeceleration;
381
382        // Animation starting time, in system milliseconds
383        long mStartTime;
384
385        // Animation duration, in milliseconds
386        int mDuration;
387
388        // Whether the animation is currently in progress
389        boolean mFinished;
390
391        // Constant gravity value, used to scale deceleration
392        static float GRAVITY;
393
394        static void initializeFromContext(Context context) {
395            final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
396            GRAVITY = SensorManager.GRAVITY_EARTH // g (m/s^2)
397                    * 39.37f // inch/meter
398                    * ppi // pixels per inch
399                    * ViewConfiguration.getScrollFriction();
400        }
401
402        MagneticScroller() {
403            mFinished = true;
404        }
405
406        void updateScroll(float q) {
407            mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
408        }
409
410        /*
411         * Update the current position and velocity for current time. Returns
412         * true if update has been done and false if animation duration has been
413         * reached.
414         */
415        boolean update() {
416            final long time = AnimationUtils.currentAnimationTimeMillis();
417            final long duration = time - mStartTime;
418
419            if (duration > mDuration) {
420                return false;
421            }
422
423            final float t = duration / 1000.0f;
424            mCurrVelocity = mVelocity + mDeceleration * t;
425            final float distance = mVelocity * t + mDeceleration * t * t / 2.0f;
426            mCurrentPosition = mStart + (int) distance;
427
428            return true;
429        }
430
431        /*
432         * Get a signed deceleration that will reduce the velocity.
433         */
434        static float getDeceleration(int velocity) {
435            return velocity > 0 ? -GRAVITY : GRAVITY;
436        }
437
438        /*
439         * Returns the time (in milliseconds) it will take to go from start to end.
440         */
441        static int computeDuration(int start, int end, float initialVelocity, float deceleration) {
442            final int distance = start - end;
443            final float discriminant = initialVelocity * initialVelocity - 2.0f * deceleration
444                    * distance;
445            if (discriminant >= 0.0f) {
446                float delta = (float) Math.sqrt(discriminant);
447                if (deceleration < 0.0f) {
448                    delta = -delta;
449                }
450                return (int) (1000.0f * (-initialVelocity - delta) / deceleration);
451            }
452
453            // End position can not be reached
454            return 0;
455        }
456
457        void startScroll(int start, int distance, int duration) {
458            mFinished = false;
459
460            mStart = start;
461            mFinal = start + distance;
462
463            mStartTime = AnimationUtils.currentAnimationTimeMillis();
464            mDuration = duration;
465
466            // Unused
467            mDeceleration = 0.0f;
468            mVelocity = 0;
469        }
470
471        void fling(int start, int velocity, int min, int max) {
472            mFinished = false;
473
474            mStart = start;
475            mStartTime = AnimationUtils.currentAnimationTimeMillis();
476
477            mVelocity = velocity;
478
479            mDeceleration = getDeceleration(velocity);
480
481            // A start from an invalid position immediately brings back to a valid position
482            if (mStart < min) {
483                mDuration = 0;
484                mFinal = min;
485                return;
486            }
487
488            if (mStart > max) {
489                mDuration = 0;
490                mFinal = max;
491                return;
492            }
493
494            // Duration are expressed in milliseconds
495            mDuration = (int) (-1000.0f * velocity / mDeceleration);
496
497            mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration));
498
499            // Clamp to a valid final position
500            if (mFinal < min) {
501                mFinal = min;
502                mDuration = computeDuration(mStart, min, mVelocity, mDeceleration);
503            }
504
505            if (mFinal > max) {
506                mFinal = max;
507                mDuration = computeDuration(mStart, max, mVelocity, mDeceleration);
508            }
509        }
510
511        void finish() {
512            mCurrentPosition = mFinal;
513            // Not reset since WebView relies on this value for fast fling.
514            // mCurrVelocity = 0.0f;
515            mFinished = true;
516        }
517
518        boolean continueWhenFinished() {
519            return false;
520        }
521
522        void setFinalPosition(int position) {
523            mFinal = position;
524            mFinished = false;
525        }
526
527        void extendDuration(int extend) {
528            final long time = AnimationUtils.currentAnimationTimeMillis();
529            final int elapsedTime = (int) (time - mStartTime);
530            mDuration = elapsedTime + extend;
531            mFinished = false;
532        }
533    }
534}
535