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 ScrollerCompatImplBase implements ScrollerCompatImpl {
62        @Override
63        public Object createScroller(Context context, Interpolator interpolator) {
64            return interpolator != null ?
65                    new Scroller(context, interpolator) : new Scroller(context);
66        }
67
68        @Override
69        public boolean isFinished(Object scroller) {
70            return ((Scroller) scroller).isFinished();
71        }
72
73        @Override
74        public int getCurrX(Object scroller) {
75            return ((Scroller) scroller).getCurrX();
76        }
77
78        @Override
79        public int getCurrY(Object scroller) {
80            return ((Scroller) scroller).getCurrY();
81        }
82
83        @Override
84        public float getCurrVelocity(Object scroller) {
85            return 0;
86        }
87
88        @Override
89        public boolean computeScrollOffset(Object scroller) {
90            final Scroller s = (Scroller) scroller;
91            return s.computeScrollOffset();
92        }
93
94        @Override
95        public void startScroll(Object scroller, int startX, int startY, int dx, int dy) {
96            ((Scroller) scroller).startScroll(startX, startY, dx, dy);
97        }
98
99        @Override
100        public void startScroll(Object scroller, int startX, int startY, int dx, int dy,
101                int duration) {
102            ((Scroller) scroller).startScroll(startX, startY, dx, dy, duration);
103        }
104
105        @Override
106        public void fling(Object scroller, int startX, int startY, int velX, int velY,
107                int minX, int maxX, int minY, int maxY) {
108            ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY);
109        }
110
111        @Override
112        public void fling(Object scroller, int startX, int startY, int velX, int velY,
113                int minX, int maxX, int minY, int maxY, int overX, int overY) {
114            ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY);
115        }
116
117        @Override
118        public void abortAnimation(Object scroller) {
119            ((Scroller) scroller).abortAnimation();
120        }
121
122        @Override
123        public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX,
124                int overX) {
125            // No-op
126        }
127
128        @Override
129        public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) {
130            // No-op
131        }
132
133        @Override
134        public boolean isOverScrolled(Object scroller) {
135            // Always false
136            return false;
137        }
138
139        @Override
140        public int getFinalX(Object scroller) {
141            return ((Scroller) scroller).getFinalX();
142        }
143
144        @Override
145        public int getFinalY(Object scroller) {
146            return ((Scroller) scroller).getFinalY();
147        }
148    }
149
150    static class ScrollerCompatImplGingerbread implements ScrollerCompatImpl {
151        @Override
152        public Object createScroller(Context context, Interpolator interpolator) {
153            return ScrollerCompatGingerbread.createScroller(context, interpolator);
154        }
155
156        @Override
157        public boolean isFinished(Object scroller) {
158            return ScrollerCompatGingerbread.isFinished(scroller);
159        }
160
161        @Override
162        public int getCurrX(Object scroller) {
163            return ScrollerCompatGingerbread.getCurrX(scroller);
164        }
165
166        @Override
167        public int getCurrY(Object scroller) {
168            return ScrollerCompatGingerbread.getCurrY(scroller);
169        }
170
171        @Override
172        public float getCurrVelocity(Object scroller) {
173            return 0;
174        }
175
176        @Override
177        public boolean computeScrollOffset(Object scroller) {
178            return ScrollerCompatGingerbread.computeScrollOffset(scroller);
179        }
180
181        @Override
182        public void startScroll(Object scroller, int startX, int startY, int dx, int dy) {
183            ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy);
184        }
185
186        @Override
187        public void startScroll(Object scroller, int startX, int startY, int dx, int dy,
188                int duration) {
189            ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy, duration);
190        }
191
192        @Override
193        public void fling(Object scroller, int startX, int startY, int velX, int velY,
194                int minX, int maxX, int minY, int maxY) {
195            ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY,
196                    minX, maxX, minY, maxY);
197        }
198
199        @Override
200        public void fling(Object scroller, int startX, int startY, int velX, int velY,
201                int minX, int maxX, int minY, int maxY, int overX, int overY) {
202            ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY,
203                    minX, maxX, minY, maxY, overX, overY);
204        }
205
206        @Override
207        public void abortAnimation(Object scroller) {
208            ScrollerCompatGingerbread.abortAnimation(scroller);
209        }
210
211        @Override
212        public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX,
213                int overX) {
214            ScrollerCompatGingerbread.notifyHorizontalEdgeReached(scroller, startX, finalX, overX);
215        }
216
217        @Override
218        public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) {
219            ScrollerCompatGingerbread.notifyVerticalEdgeReached(scroller, startY, finalY, overY);
220        }
221
222        @Override
223        public boolean isOverScrolled(Object scroller) {
224            return ScrollerCompatGingerbread.isOverScrolled(scroller);
225        }
226
227        @Override
228        public int getFinalX(Object scroller) {
229            return ScrollerCompatGingerbread.getFinalX(scroller);
230        }
231
232        @Override
233        public int getFinalY(Object scroller) {
234            return ScrollerCompatGingerbread.getFinalY(scroller);
235        }
236    }
237
238    static class ScrollerCompatImplIcs extends ScrollerCompatImplGingerbread {
239        @Override
240        public float getCurrVelocity(Object scroller) {
241            return ScrollerCompatIcs.getCurrVelocity(scroller);
242        }
243    }
244
245    public static ScrollerCompat create(Context context) {
246        return create(context, null);
247    }
248
249    public static ScrollerCompat create(Context context, Interpolator interpolator) {
250        return new ScrollerCompat(context, interpolator);
251    }
252
253    ScrollerCompat(Context context, Interpolator interpolator) {
254        this(Build.VERSION.SDK_INT, context, interpolator);
255
256    }
257
258    /**
259     * Private constructer where API version can be provided.
260     * Useful for unit testing.
261     */
262    private ScrollerCompat(int apiVersion, Context context, Interpolator interpolator) {
263        if (apiVersion >= 14) { // ICS
264            mImpl = new ScrollerCompatImplIcs();
265        } else if (apiVersion>= 9) { // Gingerbread
266            mImpl = new ScrollerCompatImplGingerbread();
267        } else {
268            mImpl = new ScrollerCompatImplBase();
269        }
270        mScroller = mImpl.createScroller(context, interpolator);
271    }
272
273    /**
274     * Returns whether the scroller has finished scrolling.
275     *
276     * @return True if the scroller has finished scrolling, false otherwise.
277     */
278    public boolean isFinished() {
279        return mImpl.isFinished(mScroller);
280    }
281
282    /**
283     * Returns the current X offset in the scroll.
284     *
285     * @return The new X offset as an absolute distance from the origin.
286     */
287    public int getCurrX() {
288        return mImpl.getCurrX(mScroller);
289    }
290
291    /**
292     * Returns the current Y offset in the scroll.
293     *
294     * @return The new Y offset as an absolute distance from the origin.
295     */
296    public int getCurrY() {
297        return mImpl.getCurrY(mScroller);
298    }
299
300    /**
301     * @return The final X position for the scroll in progress, if known.
302     */
303    public int getFinalX() {
304        return mImpl.getFinalX(mScroller);
305    }
306
307    /**
308     * @return The final Y position for the scroll in progress, if known.
309     */
310    public int getFinalY() {
311        return mImpl.getFinalY(mScroller);
312    }
313
314    /**
315     * Returns the current velocity on platform versions that support it.
316     *
317     * <p>The device must support at least API level 14 (Ice Cream Sandwich).
318     * On older platform versions this method will return 0. This method should
319     * only be used as input for nonessential visual effects such as {@link EdgeEffectCompat}.</p>
320     *
321     * @return The original velocity less the deceleration. Result may be
322     * negative.
323     */
324    public float getCurrVelocity() {
325        return mImpl.getCurrVelocity(mScroller);
326    }
327
328    /**
329     * Call this when you want to know the new location.  If it returns true,
330     * the animation is not yet finished.  loc will be altered to provide the
331     * new location.
332     */
333    public boolean computeScrollOffset() {
334        return mImpl.computeScrollOffset(mScroller);
335    }
336
337    /**
338     * Start scrolling by providing a starting point and the distance to travel.
339     * The scroll will use the default value of 250 milliseconds for the
340     * duration.
341     *
342     * @param startX Starting horizontal scroll offset in pixels. Positive
343     *        numbers will scroll the content to the left.
344     * @param startY Starting vertical scroll offset in pixels. Positive numbers
345     *        will scroll the content up.
346     * @param dx Horizontal distance to travel. Positive numbers will scroll the
347     *        content to the left.
348     * @param dy Vertical distance to travel. Positive numbers will scroll the
349     *        content up.
350     */
351    public void startScroll(int startX, int startY, int dx, int dy) {
352        mImpl.startScroll(mScroller, startX, startY, dx, dy);
353    }
354
355    /**
356     * Start scrolling by providing a starting point and the distance to travel.
357     *
358     * @param startX Starting horizontal scroll offset in pixels. Positive
359     *        numbers will scroll the content to the left.
360     * @param startY Starting vertical scroll offset in pixels. Positive numbers
361     *        will scroll the content up.
362     * @param dx Horizontal distance to travel. Positive numbers will scroll the
363     *        content to the left.
364     * @param dy Vertical distance to travel. Positive numbers will scroll the
365     *        content up.
366     * @param duration Duration of the scroll in milliseconds.
367     */
368    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
369        mImpl.startScroll(mScroller, startX, startY, dx, dy, duration);
370    }
371
372    /**
373     * Start scrolling based on a fling gesture. The distance travelled will
374     * depend on the initial velocity of the fling.
375     *
376     * @param startX Starting point of the scroll (X)
377     * @param startY Starting point of the scroll (Y)
378     * @param velocityX Initial velocity of the fling (X) measured in pixels per
379     *        second.
380     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
381     *        second
382     * @param minX Minimum X value. The scroller will not scroll past this
383     *        point.
384     * @param maxX Maximum X value. The scroller will not scroll past this
385     *        point.
386     * @param minY Minimum Y value. The scroller will not scroll past this
387     *        point.
388     * @param maxY Maximum Y value. The scroller will not scroll past this
389     *        point.
390     */
391    public void fling(int startX, int startY, int velocityX, int velocityY,
392            int minX, int maxX, int minY, int maxY) {
393        mImpl.fling(mScroller, startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
394    }
395
396    /**
397     * Start scrolling based on a fling gesture. The distance travelled will
398     * depend on the initial velocity of the fling.
399     *
400     * @param startX Starting point of the scroll (X)
401     * @param startY Starting point of the scroll (Y)
402     * @param velocityX Initial velocity of the fling (X) measured in pixels per
403     *        second.
404     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
405     *        second
406     * @param minX Minimum X value. The scroller will not scroll past this
407     *        point.
408     * @param maxX Maximum X value. The scroller will not scroll past this
409     *        point.
410     * @param minY Minimum Y value. The scroller will not scroll past this
411     *        point.
412     * @param maxY Maximum Y value. The scroller will not scroll past this
413     *        point.
414     * @param overX Overfling range. If > 0, horizontal overfling in either
415     *            direction will be possible.
416     * @param overY Overfling range. If > 0, vertical overfling in either
417     *            direction will be possible.
418     */
419    public void fling(int startX, int startY, int velocityX, int velocityY,
420            int minX, int maxX, int minY, int maxY, int overX, int overY) {
421        mImpl.fling(mScroller, startX, startY, velocityX, velocityY,
422                minX, maxX, minY, maxY, overX, overY);
423    }
424
425    /**
426     * Stops the animation. Aborting the animation causes the scroller to move to the final x and y
427     * position.
428     */
429    public void abortAnimation() {
430        mImpl.abortAnimation(mScroller);
431    }
432
433
434    /**
435     * Notify the scroller that we've reached a horizontal boundary.
436     * Normally the information to handle this will already be known
437     * when the animation is started, such as in a call to one of the
438     * fling functions. However there are cases where this cannot be known
439     * in advance. This function will transition the current motion and
440     * animate from startX to finalX as appropriate.
441     *
442     * @param startX Starting/current X position
443     * @param finalX Desired final X position
444     * @param overX Magnitude of overscroll allowed. This should be the maximum
445     *              desired distance from finalX. Absolute value - must be positive.
446     */
447    public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
448        mImpl.notifyHorizontalEdgeReached(mScroller, startX, finalX, overX);
449    }
450
451    /**
452     * Notify the scroller that we've reached a vertical boundary.
453     * Normally the information to handle this will already be known
454     * when the animation is started, such as in a call to one of the
455     * fling functions. However there are cases where this cannot be known
456     * in advance. This function will animate a parabolic motion from
457     * startY to finalY.
458     *
459     * @param startY Starting/current Y position
460     * @param finalY Desired final Y position
461     * @param overY Magnitude of overscroll allowed. This should be the maximum
462     *              desired distance from finalY. Absolute value - must be positive.
463     */
464    public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
465        mImpl.notifyVerticalEdgeReached(mScroller, startY, finalY, overY);
466    }
467
468    /**
469     * Returns whether the current Scroller is currently returning to a valid position.
470     * Valid bounds were provided by the
471     * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
472     *
473     * One should check this value before calling
474     * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
475     * to restore a valid position will then be stopped. The caller has to take into account
476     * the fact that the started scroll will start from an overscrolled position.
477     *
478     * @return true when the current position is overscrolled and in the process of
479     *         interpolating back to a valid value.
480     */
481    public boolean isOverScrolled() {
482        return mImpl.isOverScrolled(mScroller);
483    }
484}
485