ScrollerCompat.java revision 846786b33c7325e99603c2a7947f976b633c3496
1/*
2 * Copyright (C) 2012 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.support.v4.widget;
18
19import android.content.Context;
20import android.os.Build;
21import android.view.animation.AnimationUtils;
22import android.view.animation.Interpolator;
23import android.widget.Scroller;
24
25/**
26 * Provides access to new {@link android.widget.Scroller Scroller} APIs when available.
27 *
28 * <p>This class provides a platform version-independent mechanism for obeying the
29 * current device's preferred scroll physics and fling behavior. It offers a subset of
30 * the APIs from Scroller or OverScroller.</p>
31 */
32public class ScrollerCompat {
33    private static final String TAG = "ScrollerCompat";
34
35    Object mScroller;
36    ScrollerCompatImpl mImpl;
37
38    interface ScrollerCompatImpl {
39        Object createScroller(Context context, Interpolator interpolator);
40        boolean isFinished(Object scroller);
41        int getCurrX(Object scroller);
42        int getCurrY(Object scroller);
43        float getCurrVelocity(Object scroller);
44        boolean computeScrollOffset(Object scroller);
45        void startScroll(Object scroller, int startX, int startY, int dx, int dy);
46        void startScroll(Object scroller, int startX, int startY, int dx, int dy, int duration);
47        void fling(Object scroller, int startX, int startY, int velX, int velY,
48                int minX, int maxX, int minY, int maxY);
49        void fling(Object scroller, int startX, int startY, int velX, int velY,
50                int minX, int maxX, int minY, int maxY, int overX, int overY);
51        void abortAnimation(Object scroller);
52        void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, int overX);
53        void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY);
54        boolean isOverScrolled(Object scroller);
55        int getFinalX(Object scroller);
56        int getFinalY(Object scroller);
57    }
58
59    static final int CHASE_FRAME_TIME = 16; // ms per target frame
60
61    static class Chaser {
62        private int mX;
63        private int mY;
64        private int mTargetX;
65        private int mTargetY;
66        private float mTranslateSmoothing = 2;
67        private long mLastTime;
68        private boolean mFinished = true;
69
70        @Override
71        public String toString() {
72            return "{x=" + mX + " y=" + mY + " targetX=" + mTargetX + " targetY=" + mTargetY +
73                    " smoothing=" + mTranslateSmoothing + " lastTime=" + mLastTime + "}";
74        }
75
76        public int getCurrX() {
77            return mX;
78        }
79
80        public int getCurrY() {
81            return mY;
82        }
83
84        public int getFinalX() {
85            return mTargetX;
86        }
87
88        public int getFinalY() {
89            return mTargetY;
90        }
91
92        public void setCurrentPosition(int x, int y) {
93            mX = x;
94            mY = y;
95            mFinished = false;
96        }
97
98        public void setSmoothing(float smoothing) {
99            if (smoothing < 0) {
100                throw new IllegalArgumentException("smoothing value must be positive");
101            }
102            mTranslateSmoothing = smoothing;
103        }
104
105        public boolean isSmoothingEnabled() {
106            return mTranslateSmoothing > 0;
107        }
108
109        public void setTarget(int targetX, int targetY) {
110            mTargetX = targetX;
111            mTargetY = targetY;
112        }
113
114        public void abort() {
115            mX = mTargetX;
116            mY = mTargetY;
117            mLastTime = AnimationUtils.currentAnimationTimeMillis();
118            mFinished = true;
119        }
120
121        public boolean isFinished() {
122            return mFinished || (mX == mTargetX && mY == mTargetY);
123        }
124
125        public boolean computeScrollOffset() {
126            if (isSmoothingEnabled() && !isFinished()) {
127                final long now = AnimationUtils.currentAnimationTimeMillis();
128                final long dt = now - mLastTime;
129                final float framesElapsed = (float) dt / CHASE_FRAME_TIME;
130
131                if (framesElapsed > 0) {
132                    for (int i = 0; i < framesElapsed; i++) {
133                        final int totalDx = mTargetX - mX;
134                        final int totalDy = mTargetY - mY;
135
136                        final int dx = (int) (totalDx / mTranslateSmoothing);
137                        final int dy = (int) (totalDy / mTranslateSmoothing);
138
139                        mX += dx;
140                        mY += dy;
141
142                        // Handle cropping at the end
143                        if (mX != mTargetX && dx == 0) {
144                            mX = mTargetX;
145                        }
146                        if (mY != mTargetY && dy == 0) {
147                            mY = mTargetY;
148                        }
149                    }
150
151                    mLastTime = now;
152                }
153                mFinished = mX == mTargetX && mY == mTargetY;
154                return true;
155            }
156            return false;
157        }
158    }
159
160    static class ScrollerCompatImplBase implements ScrollerCompatImpl {
161        private Chaser mChaser;
162
163        public ScrollerCompatImplBase() {
164            mChaser = createChaser();
165        }
166
167        protected Chaser createChaser() {
168            // Override if running on a platform version where this isn't needed
169            return new Chaser();
170        }
171
172        @Override
173        public Object createScroller(Context context, Interpolator interpolator) {
174            return interpolator != null ?
175                    new Scroller(context, interpolator) : new Scroller(context);
176        }
177
178        @Override
179        public boolean isFinished(Object scroller) {
180            return (!isSmoothingEnabled() || mChaser.isFinished()) &&
181                    ((Scroller) scroller).isFinished();
182        }
183
184        @Override
185        public int getCurrX(Object scroller) {
186            if (isSmoothingEnabled()) {
187                return mChaser.getCurrX();
188            }
189            return ((Scroller) scroller).getCurrX();
190        }
191
192        @Override
193        public int getCurrY(Object scroller) {
194            if (isSmoothingEnabled()) {
195                return mChaser.getCurrY();
196            }
197            return ((Scroller) scroller).getCurrY();
198        }
199
200        @Override
201        public float getCurrVelocity(Object scroller) {
202            return 0;
203        }
204
205        @Override
206        public boolean computeScrollOffset(Object scroller) {
207            final Scroller s = (Scroller) scroller;
208            final boolean result = s.computeScrollOffset();
209            if (isSmoothingEnabled()) {
210                mChaser.setTarget(s.getCurrX(), s.getCurrY());
211                if (isSmoothingEnabled() && !mChaser.isFinished()) {
212                    return mChaser.computeScrollOffset() || result;
213                }
214            }
215            return result;
216        }
217
218        private boolean isSmoothingEnabled() {
219            return mChaser != null && mChaser.isSmoothingEnabled();
220        }
221
222        @Override
223        public void startScroll(Object scroller, int startX, int startY, int dx, int dy) {
224            if (isSmoothingEnabled()) {
225                mChaser.abort();
226                mChaser.setCurrentPosition(startX, startY);
227            }
228            ((Scroller) scroller).startScroll(startX, startY, dx, dy);
229        }
230
231        @Override
232        public void startScroll(Object scroller, int startX, int startY, int dx, int dy,
233                int duration) {
234            if (isSmoothingEnabled()) {
235                mChaser.abort();
236                mChaser.setCurrentPosition(startX, startY);
237            }
238            ((Scroller) scroller).startScroll(startX, startY, dx, dy, duration);
239        }
240
241        @Override
242        public void fling(Object scroller, int startX, int startY, int velX, int velY,
243                int minX, int maxX, int minY, int maxY) {
244            if (isSmoothingEnabled()) {
245                mChaser.abort();
246                mChaser.setCurrentPosition(startX, startY);
247            }
248            ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY);
249        }
250
251        @Override
252        public void fling(Object scroller, int startX, int startY, int velX, int velY,
253                int minX, int maxX, int minY, int maxY, int overX, int overY) {
254            if (isSmoothingEnabled()) {
255                mChaser.abort();
256                mChaser.setCurrentPosition(startX, startY);
257            }
258            ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY);
259        }
260
261        @Override
262        public void abortAnimation(Object scroller) {
263            if (mChaser != null) {
264                mChaser.abort();
265            }
266            ((Scroller) scroller).abortAnimation();
267        }
268
269        @Override
270        public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX,
271                int overX) {
272            // No-op
273        }
274
275        @Override
276        public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) {
277            // No-op
278        }
279
280        @Override
281        public boolean isOverScrolled(Object scroller) {
282            // Always false
283            return false;
284        }
285
286        @Override
287        public int getFinalX(Object scroller) {
288            return ((Scroller) scroller).getFinalX();
289        }
290
291        @Override
292        public int getFinalY(Object scroller) {
293            return ((Scroller) scroller).getFinalY();
294        }
295    }
296
297    static class ScrollerCompatImplGingerbread implements ScrollerCompatImpl {
298        private Chaser mChaser;
299
300        public ScrollerCompatImplGingerbread() {
301            mChaser = createChaser();
302        }
303
304        @Override
305        public Object createScroller(Context context, Interpolator interpolator) {
306            return ScrollerCompatGingerbread.createScroller(context, interpolator);
307        }
308
309        protected Chaser createChaser() {
310            return new Chaser();
311        }
312
313        @Override
314        public boolean isFinished(Object scroller) {
315            return (!isSmoothingEnabled() || mChaser.isFinished()) &&
316                    ScrollerCompatGingerbread.isFinished(scroller);
317        }
318
319        @Override
320        public int getCurrX(Object scroller) {
321            if (isSmoothingEnabled()) {
322                return mChaser.getCurrX();
323            }
324            return ScrollerCompatGingerbread.getCurrX(scroller);
325        }
326
327        @Override
328        public int getCurrY(Object scroller) {
329            if (isSmoothingEnabled()) {
330                return mChaser.getCurrY();
331            }
332            return ScrollerCompatGingerbread.getCurrY(scroller);
333        }
334
335        @Override
336        public float getCurrVelocity(Object scroller) {
337            return 0;
338        }
339
340        @Override
341        public boolean computeScrollOffset(Object scroller) {
342            final boolean result = ScrollerCompatGingerbread.computeScrollOffset(scroller);
343            if (isSmoothingEnabled()) {
344                mChaser.setTarget(ScrollerCompatGingerbread.getCurrX(scroller),
345                        ScrollerCompatGingerbread.getCurrY(scroller));
346                if (!mChaser.isFinished()) {
347                    return mChaser.computeScrollOffset() || result;
348                }
349            }
350            return result;
351        }
352
353        private boolean isSmoothingEnabled() {
354            return mChaser != null && mChaser.isSmoothingEnabled();
355        }
356
357        @Override
358        public void startScroll(Object scroller, int startX, int startY, int dx, int dy) {
359            if (isSmoothingEnabled()) {
360                mChaser.abort();
361                mChaser.setCurrentPosition(startX, startY);
362            }
363            ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy);
364        }
365
366        @Override
367        public void startScroll(Object scroller, int startX, int startY, int dx, int dy,
368                int duration) {
369            if (isSmoothingEnabled()) {
370                mChaser.abort();
371                mChaser.setCurrentPosition(startX, startY);
372            }
373            ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy, duration);
374        }
375
376        @Override
377        public void fling(Object scroller, int startX, int startY, int velX, int velY,
378                int minX, int maxX, int minY, int maxY) {
379            if (isSmoothingEnabled()) {
380                mChaser.abort();
381                mChaser.setCurrentPosition(startX, startY);
382            }
383            ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY,
384                    minX, maxX, minY, maxY);
385        }
386
387        @Override
388        public void fling(Object scroller, int startX, int startY, int velX, int velY,
389                int minX, int maxX, int minY, int maxY, int overX, int overY) {
390            if (isSmoothingEnabled()) {
391                mChaser.abort();
392                mChaser.setCurrentPosition(startX, startY);
393            }
394            ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY,
395                    minX, maxX, minY, maxY, overX, overY);
396        }
397
398        @Override
399        public void abortAnimation(Object scroller) {
400            if (mChaser != null) {
401                mChaser.abort();
402            }
403            ScrollerCompatGingerbread.abortAnimation(scroller);
404        }
405
406        @Override
407        public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX,
408                int overX) {
409            ScrollerCompatGingerbread.notifyHorizontalEdgeReached(scroller, startX, finalX, overX);
410        }
411
412        @Override
413        public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) {
414            ScrollerCompatGingerbread.notifyVerticalEdgeReached(scroller, startY, finalY, overY);
415        }
416
417        @Override
418        public boolean isOverScrolled(Object scroller) {
419            return ScrollerCompatGingerbread.isOverScrolled(scroller);
420        }
421
422        @Override
423        public int getFinalX(Object scroller) {
424            return ScrollerCompatGingerbread.getFinalX(scroller);
425        }
426
427        @Override
428        public int getFinalY(Object scroller) {
429            return ScrollerCompatGingerbread.getFinalY(scroller);
430        }
431    }
432
433    static class ScrollerCompatImplIcs extends ScrollerCompatImplGingerbread {
434        @Override
435        public float getCurrVelocity(Object scroller) {
436            return ScrollerCompatIcs.getCurrVelocity(scroller);
437        }
438    }
439
440    public static ScrollerCompat create(Context context) {
441        return create(context, null);
442    }
443
444    public static ScrollerCompat create(Context context, Interpolator interpolator) {
445        return new ScrollerCompat(context, interpolator);
446    }
447
448    ScrollerCompat(Context context, Interpolator interpolator) {
449        final int version = Build.VERSION.SDK_INT;
450        if (version >= 14) { // ICS
451            mImpl = new ScrollerCompatImplIcs();
452        } else if (version >= 9) { // Gingerbread
453            mImpl = new ScrollerCompatImplGingerbread();
454        } else {
455            mImpl = new ScrollerCompatImplBase();
456        }
457        mScroller = mImpl.createScroller(context, interpolator);
458    }
459
460    /**
461     * Returns whether the scroller has finished scrolling.
462     *
463     * @return True if the scroller has finished scrolling, false otherwise.
464     */
465    public boolean isFinished() {
466        return mImpl.isFinished(mScroller);
467    }
468
469    /**
470     * Returns the current X offset in the scroll.
471     *
472     * @return The new X offset as an absolute distance from the origin.
473     */
474    public int getCurrX() {
475        return mImpl.getCurrX(mScroller);
476    }
477
478    /**
479     * Returns the current Y offset in the scroll.
480     *
481     * @return The new Y offset as an absolute distance from the origin.
482     */
483    public int getCurrY() {
484        return mImpl.getCurrY(mScroller);
485    }
486
487    /**
488     * @return The final X position for the scroll in progress, if known.
489     */
490    public int getFinalX() {
491        return mImpl.getFinalX(mScroller);
492    }
493
494    /**
495     * @return The final Y position for the scroll in progress, if known.
496     */
497    public int getFinalY() {
498        return mImpl.getFinalY(mScroller);
499    }
500
501    /**
502     * Returns the current velocity on platform versions that support it.
503     *
504     * <p>The device must support at least API level 14 (Ice Cream Sandwich).
505     * On older platform versions this method will return 0. This method should
506     * only be used as input for nonessential visual effects such as {@link EdgeEffectCompat}.</p>
507     *
508     * @return The original velocity less the deceleration. Result may be
509     * negative.
510     */
511    public float getCurrVelocity() {
512        return mImpl.getCurrVelocity(mScroller);
513    }
514
515    /**
516     * Call this when you want to know the new location.  If it returns true,
517     * the animation is not yet finished.  loc will be altered to provide the
518     * new location.
519     */
520    public boolean computeScrollOffset() {
521        return mImpl.computeScrollOffset(mScroller);
522    }
523
524    /**
525     * Start scrolling by providing a starting point and the distance to travel.
526     * The scroll will use the default value of 250 milliseconds for the
527     * duration.
528     *
529     * @param startX Starting horizontal scroll offset in pixels. Positive
530     *        numbers will scroll the content to the left.
531     * @param startY Starting vertical scroll offset in pixels. Positive numbers
532     *        will scroll the content up.
533     * @param dx Horizontal distance to travel. Positive numbers will scroll the
534     *        content to the left.
535     * @param dy Vertical distance to travel. Positive numbers will scroll the
536     *        content up.
537     */
538    public void startScroll(int startX, int startY, int dx, int dy) {
539        mImpl.startScroll(mScroller, startX, startY, dx, dy);
540    }
541
542    /**
543     * Start scrolling by providing a starting point and the distance to travel.
544     *
545     * @param startX Starting horizontal scroll offset in pixels. Positive
546     *        numbers will scroll the content to the left.
547     * @param startY Starting vertical scroll offset in pixels. Positive numbers
548     *        will scroll the content up.
549     * @param dx Horizontal distance to travel. Positive numbers will scroll the
550     *        content to the left.
551     * @param dy Vertical distance to travel. Positive numbers will scroll the
552     *        content up.
553     * @param duration Duration of the scroll in milliseconds.
554     */
555    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
556        mImpl.startScroll(mScroller, startX, startY, dx, dy, duration);
557    }
558
559    /**
560     * Start scrolling based on a fling gesture. The distance travelled will
561     * depend on the initial velocity of the fling.
562     *
563     * @param startX Starting point of the scroll (X)
564     * @param startY Starting point of the scroll (Y)
565     * @param velocityX Initial velocity of the fling (X) measured in pixels per
566     *        second.
567     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
568     *        second
569     * @param minX Minimum X value. The scroller will not scroll past this
570     *        point.
571     * @param maxX Maximum X value. The scroller will not scroll past this
572     *        point.
573     * @param minY Minimum Y value. The scroller will not scroll past this
574     *        point.
575     * @param maxY Maximum Y value. The scroller will not scroll past this
576     *        point.
577     */
578    public void fling(int startX, int startY, int velocityX, int velocityY,
579            int minX, int maxX, int minY, int maxY) {
580        mImpl.fling(mScroller, startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
581    }
582
583    /**
584     * Start scrolling based on a fling gesture. The distance travelled will
585     * depend on the initial velocity of the fling.
586     *
587     * @param startX Starting point of the scroll (X)
588     * @param startY Starting point of the scroll (Y)
589     * @param velocityX Initial velocity of the fling (X) measured in pixels per
590     *        second.
591     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
592     *        second
593     * @param minX Minimum X value. The scroller will not scroll past this
594     *        point.
595     * @param maxX Maximum X value. The scroller will not scroll past this
596     *        point.
597     * @param minY Minimum Y value. The scroller will not scroll past this
598     *        point.
599     * @param maxY Maximum Y value. The scroller will not scroll past this
600     *        point.
601     * @param overX Overfling range. If > 0, horizontal overfling in either
602     *            direction will be possible.
603     * @param overY Overfling range. If > 0, vertical overfling in either
604     *            direction will be possible.
605     */
606    public void fling(int startX, int startY, int velocityX, int velocityY,
607            int minX, int maxX, int minY, int maxY, int overX, int overY) {
608        mImpl.fling(mScroller, startX, startY, velocityX, velocityY,
609                minX, maxX, minY, maxY, overX, overY);
610    }
611
612    /**
613     * Stops the animation. Aborting the animation causes the scroller to move to the final x and y
614     * position.
615     */
616    public void abortAnimation() {
617        mImpl.abortAnimation(mScroller);
618    }
619
620
621    /**
622     * Notify the scroller that we've reached a horizontal boundary.
623     * Normally the information to handle this will already be known
624     * when the animation is started, such as in a call to one of the
625     * fling functions. However there are cases where this cannot be known
626     * in advance. This function will transition the current motion and
627     * animate from startX to finalX as appropriate.
628     *
629     * @param startX Starting/current X position
630     * @param finalX Desired final X position
631     * @param overX Magnitude of overscroll allowed. This should be the maximum
632     *              desired distance from finalX. Absolute value - must be positive.
633     */
634    public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
635        mImpl.notifyHorizontalEdgeReached(mScroller, startX, finalX, overX);
636    }
637
638    /**
639     * Notify the scroller that we've reached a vertical boundary.
640     * Normally the information to handle this will already be known
641     * when the animation is started, such as in a call to one of the
642     * fling functions. However there are cases where this cannot be known
643     * in advance. This function will animate a parabolic motion from
644     * startY to finalY.
645     *
646     * @param startY Starting/current Y position
647     * @param finalY Desired final Y position
648     * @param overY Magnitude of overscroll allowed. This should be the maximum
649     *              desired distance from finalY. Absolute value - must be positive.
650     */
651    public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
652        mImpl.notifyVerticalEdgeReached(mScroller, startY, finalY, overY);
653    }
654
655    /**
656     * Returns whether the current Scroller is currently returning to a valid position.
657     * Valid bounds were provided by the
658     * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
659     *
660     * One should check this value before calling
661     * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
662     * to restore a valid position will then be stopped. The caller has to take into account
663     * the fact that the started scroll will start from an overscrolled position.
664     *
665     * @return true when the current position is overscrolled and in the process of
666     *         interpolating back to a valid value.
667     */
668    public boolean isOverScrolled() {
669        return mImpl.isOverScrolled(mScroller);
670    }
671}
672