AutoScrollHelper.java revision 0d3da1232bf967e427477ab4d4c58eb3e933f17e
1/*
2 * Copyright (C) 2013 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 com.android.internal.widget;
18
19import android.content.res.Resources;
20import android.os.SystemClock;
21import android.util.DisplayMetrics;
22import android.view.MotionEvent;
23import android.view.View;
24import android.view.ViewConfiguration;
25import android.view.animation.AccelerateInterpolator;
26import android.view.animation.AnimationUtils;
27import android.view.animation.Interpolator;
28import android.widget.AbsListView;
29
30/**
31 * AutoScrollHelper is a utility class for adding automatic edge-triggered
32 * scrolling to Views.
33 * <p>
34 * <b>Note:</b> Implementing classes are responsible for overriding the
35 * {@link #onScrollBy} method to scroll the target view. See
36 * {@link AbsListViewAutoScroller} for an {@link android.widget.AbsListView}
37 * -specific implementation.
38 * <p>
39 * <h1>Activation</h1> Automatic scrolling starts when the user touches within
40 * an activation area. By default, activation areas are defined as the top,
41 * left, right, and bottom 20% of the host view's total area. Touching within
42 * the top activation area scrolls up, left scrolls to the left, and so on.
43 * <p>
44 * As the user touches closer to the extreme edge of the activation area,
45 * scrolling accelerates up to a maximum velocity. When using the default edge
46 * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds
47 * will scroll at the maximum velocity.
48 * <p>
49 * The following activation properties may be configured:
50 * <ul>
51 * <li>Delay after entering activation area before auto-scrolling begins, see
52 * {@link #setActivationDelay}. Default value is
53 * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps.
54 * <li>Location of activation areas, see {@link #setEdgeType}. Default value is
55 * {@link #EDGE_TYPE_INSIDE_EXTEND}.
56 * <li>Size of activation areas relative to view size, see
57 * {@link #setRelativeEdges}. Default value is 20% for both vertical and
58 * horizontal edges.
59 * <li>Maximum size used to constrain relative size, see
60 * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}.
61 * </ul>
62 * <h1>Scrolling</h1> When automatic scrolling is active, the helper will
63 * repeatedly call {@link #onScrollBy} to apply new scrolling offsets.
64 * <p>
65 * The following scrolling properties may be configured:
66 * <ul>
67 * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default
68 * value is 2.5 seconds.
69 * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}.
70 * Default value is 100% per second for both vertical and horizontal.
71 * <li>Minimum velocity used to constrain relative velocity, see
72 * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the
73 * larger of either this value or the relative target value. Default value is
74 * approximately 5 centimeters or 315 dips per second.
75 * <li>Maximum velocity used to constrain relative velocity, see
76 * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or
77 * 1575 dips per second.
78 * </ul>
79 */
80public abstract class AutoScrollHelper implements View.OnTouchListener {
81    /**
82     * Constant passed to {@link #setRelativeEdges} or
83     * {@link #setRelativeVelocity}. Using this value ensures that the computed
84     * relative value is ignored and the absolute maximum value is always used.
85     */
86    public static final float RELATIVE_UNSPECIFIED = 0;
87
88    /**
89     * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity},
90     * or {@link #setMinimumVelocity}. Using this value ensures that the
91     * computed relative value is always used without constraining to a
92     * particular minimum or maximum value.
93     */
94    public static final float NO_MAX = Float.MAX_VALUE;
95
96    /**
97     * Constant passed to {@link #setMaximumEdges}, or
98     * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this
99     * value ensures that the computed relative value is always used without
100     * constraining to a particular minimum or maximum value.
101     */
102    public static final float NO_MIN = 0;
103
104    /**
105     * Edge type that specifies an activation area starting at the view bounds
106     * and extending inward. Moving outside the view bounds will stop scrolling.
107     *
108     * @see #setEdgeType
109     */
110    public static final int EDGE_TYPE_INSIDE = 0;
111
112    /**
113     * Edge type that specifies an activation area starting at the view bounds
114     * and extending inward. After activation begins, moving outside the view
115     * bounds will continue scrolling.
116     *
117     * @see #setEdgeType
118     */
119    public static final int EDGE_TYPE_INSIDE_EXTEND = 1;
120
121    /**
122     * Edge type that specifies an activation area starting at the view bounds
123     * and extending outward. Moving inside the view bounds will stop scrolling.
124     *
125     * @see #setEdgeType
126     */
127    public static final int EDGE_TYPE_OUTSIDE = 2;
128
129    private static final int HORIZONTAL = 0;
130    private static final int VERTICAL = 1;
131
132    /** Scroller used to control acceleration toward maximum velocity. */
133    private final ClampedScroller mScroller = new ClampedScroller();
134
135    /** Interpolator used to scale velocity with touch position. */
136    private final Interpolator mEdgeInterpolator = new AccelerateInterpolator();
137
138    /** The view to auto-scroll. Might not be the source of touch events. */
139    private final View mTarget;
140
141    /** Runnable used to animate scrolling. */
142    private Runnable mRunnable;
143
144    /** Edge insets used to activate auto-scrolling. */
145    private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
146
147    /** Clamping values for edge insets used to activate auto-scrolling. */
148    private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX };
149
150    /** The type of edge being used. */
151    private int mEdgeType;
152
153    /** Delay after entering an activation edge before auto-scrolling begins. */
154    private int mActivationDelay;
155
156    /** Relative scrolling velocity at maximum edge distance. */
157    private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
158
159    /** Clamping values used for scrolling velocity. */
160    private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN };
161
162    /** Clamping values used for scrolling velocity. */
163    private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX };
164
165    /** Whether to start activation immediately. */
166    private boolean mSkipDelay;
167
168    /** Whether to reset the scroller start time on the next animation. */
169    private boolean mResetScroller;
170
171    /** Whether the auto-scroller is active. */
172    private boolean mActive;
173
174    /** Whether the auto-scroller is scrolling. */
175    private boolean mScrolling;
176
177    /** Whether the auto-scroller is enabled. */
178    private boolean mEnabled;
179
180    /** Whether the auto-scroller consumes events when scrolling. */
181    private boolean mExclusiveEnabled;
182
183    /** Down time of the most recent down touch event. */
184    private long mDownTime;
185
186    // Default values.
187    private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND;
188    private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315;
189    private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575;
190    private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX;
191    private static final float DEFAULT_RELATIVE_EDGE = 0.2f;
192    private static final float DEFAULT_RELATIVE_VELOCITY = 1f;
193    private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout();
194    private static final int DEFAULT_RAMP_UP_DURATION = 2500;
195    // TODO: RAMP_DOWN_DURATION of 500ms?
196
197    /**
198     * Creates a new helper for scrolling the specified target view.
199     * <p>
200     * The resulting helper may be configured by chaining setter calls and
201     * should be set as a touch listener on the target view.
202     * <p>
203     * By default, the helper is disabled and will not respond to touch events
204     * until it is enabled using {@link #setEnabled}.
205     *
206     * @param target The view to automatically scroll.
207     */
208    public AutoScrollHelper(View target) {
209        mTarget = target;
210
211        final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
212        final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
213        final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
214        setMaximumVelocity(maxVelocity, maxVelocity);
215        setMinimumVelocity(minVelocity, minVelocity);
216
217        setEdgeType(DEFAULT_EDGE_TYPE);
218        setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE);
219        setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE);
220        setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY);
221        setActivationDelay(DEFAULT_ACTIVATION_DELAY);
222        setRampUpDuration(DEFAULT_RAMP_UP_DURATION);
223
224        mEnabled = true;
225    }
226
227    /**
228     * Sets whether the scroll helper is enabled and should respond to touch
229     * events.
230     *
231     * @param enabled Whether the scroll helper is enabled.
232     * @return The scroll helper, which may used to chain setter calls.
233     */
234    public AutoScrollHelper setEnabled(boolean enabled) {
235        if (!enabled) {
236            stop(true);
237        }
238
239        mEnabled = enabled;
240        return this;
241    }
242
243    /**
244     * @return True if this helper is enabled and responding to touch events.
245     */
246    public boolean isEnabled() {
247        return mEnabled;
248    }
249
250    /**
251     * Enables or disables exclusive handling of touch events during scrolling.
252     * By default, exclusive handling is disabled and the target view receives
253     * all touch events.
254     * <p>
255     * When enabled, {@link #onTouch} will return true if the helper is
256     * currently scrolling and false otherwise.
257     *
258     * @param enabled True to exclusively handle touch events during scrolling,
259     *            false to allow the target view to receive all touch events.
260     * @see #isExclusiveEnabled()
261     * @see #onTouch(View, MotionEvent)
262     */
263    public void setExclusiveEnabled(boolean enabled) {
264        mExclusiveEnabled = enabled;
265    }
266
267    /**
268     * Indicates whether the scroll helper handles touch events exclusively
269     * during scrolling.
270     *
271     * @return True if exclusive handling of touch events during scrolling is
272     *         enabled, false otherwise.
273     * @see #setExclusiveEnabled(boolean)
274     */
275    public boolean isExclusiveEnabled() {
276        return mExclusiveEnabled;
277    }
278
279    /**
280     * Sets the absolute maximum scrolling velocity.
281     * <p>
282     * If relative velocity is not specified, scrolling will always reach the
283     * same maximum velocity. If both relative and maximum velocities are
284     * specified, the maximum velocity will be used to clamp the calculated
285     * relative velocity.
286     *
287     * @param horizontalMax The maximum horizontal scrolling velocity, or
288     *            {@link #NO_MAX} to leave the relative value unconstrained.
289     * @param verticalMax The maximum vertical scrolling velocity, or
290     *            {@link #NO_MAX} to leave the relative value unconstrained.
291     * @return The scroll helper, which may used to chain setter calls.
292     */
293    public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) {
294        mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f;
295        mMaximumVelocity[VERTICAL] = verticalMax / 1000f;
296        return this;
297    }
298
299    /**
300     * Sets the absolute minimum scrolling velocity.
301     * <p>
302     * If both relative and minimum velocities are specified, the minimum
303     * velocity will be used to clamp the calculated relative velocity.
304     *
305     * @param horizontalMin The minimum horizontal scrolling velocity, or
306     *            {@link #NO_MIN} to leave the relative value unconstrained.
307     * @param verticalMin The minimum vertical scrolling velocity, or
308     *            {@link #NO_MIN} to leave the relative value unconstrained.
309     * @return The scroll helper, which may used to chain setter calls.
310     */
311    public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) {
312        mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f;
313        mMinimumVelocity[VERTICAL] = verticalMin / 1000f;
314        return this;
315    }
316
317    /**
318     * Sets the target scrolling velocity relative to the host view's
319     * dimensions.
320     * <p>
321     * If both relative and maximum velocities are specified, the maximum
322     * velocity will be used to clamp the calculated relative velocity.
323     *
324     * @param horizontal The target horizontal velocity as a fraction of the
325     *            host view width per second, or {@link #RELATIVE_UNSPECIFIED}
326     *            to ignore.
327     * @param vertical The target vertical velocity as a fraction of the host
328     *            view height per second, or {@link #RELATIVE_UNSPECIFIED} to
329     *            ignore.
330     * @return The scroll helper, which may used to chain setter calls.
331     */
332    public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) {
333        mRelativeVelocity[HORIZONTAL] = horizontal / 1000f;
334        mRelativeVelocity[VERTICAL] = vertical / 1000f;
335        return this;
336    }
337
338    /**
339     * Sets the activation edge type, one of:
340     * <ul>
341     * <li>{@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside
342     * the bounds of the host view. If touch moves outside the bounds, scrolling
343     * will stop.
344     * <li>{@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to
345     * scroll when touch moves outside the bounds of the host view.
346     * <li>{@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches
347     * that move outside the bounds of the host view.
348     * </ul>
349     *
350     * @param type The type of edge to use.
351     * @return The scroll helper, which may used to chain setter calls.
352     */
353    public AutoScrollHelper setEdgeType(int type) {
354        mEdgeType = type;
355        return this;
356    }
357
358    /**
359     * Sets the activation edge size relative to the host view's dimensions.
360     * <p>
361     * If both relative and maximum edges are specified, the maximum edge will
362     * be used to constrain the calculated relative edge size.
363     *
364     * @param horizontal The horizontal edge size as a fraction of the host view
365     *            width, or {@link #RELATIVE_UNSPECIFIED} to always use the
366     *            maximum value.
367     * @param vertical The vertical edge size as a fraction of the host view
368     *            height, or {@link #RELATIVE_UNSPECIFIED} to always use the
369     *            maximum value.
370     * @return The scroll helper, which may used to chain setter calls.
371     */
372    public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) {
373        mRelativeEdges[HORIZONTAL] = horizontal;
374        mRelativeEdges[VERTICAL] = vertical;
375        return this;
376    }
377
378    /**
379     * Sets the absolute maximum edge size.
380     * <p>
381     * If relative edge size is not specified, activation edges will always be
382     * the maximum edge size. If both relative and maximum edges are specified,
383     * the maximum edge will be used to constrain the calculated relative edge
384     * size.
385     *
386     * @param horizontalMax The maximum horizontal edge size in pixels, or
387     *            {@link #NO_MAX} to use the unconstrained calculated relative
388     *            value.
389     * @param verticalMax The maximum vertical edge size in pixels, or
390     *            {@link #NO_MAX} to use the unconstrained calculated relative
391     *            value.
392     * @return The scroll helper, which may used to chain setter calls.
393     */
394    public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) {
395        mMaximumEdges[HORIZONTAL] = horizontalMax;
396        mMaximumEdges[VERTICAL] = verticalMax;
397        return this;
398    }
399
400    /**
401     * Sets the delay after entering an activation edge before activation of
402     * auto-scrolling. By default, the activation delay is set to
403     * {@link ViewConfiguration#getTapTimeout()}.
404     * <p>
405     * Specifying a delay of zero will start auto-scrolling immediately after
406     * the touch position enters an activation edge.
407     *
408     * @param delayMillis The activation delay in milliseconds.
409     * @return The scroll helper, which may used to chain setter calls.
410     */
411    public AutoScrollHelper setActivationDelay(int delayMillis) {
412        mActivationDelay = delayMillis;
413        return this;
414    }
415
416    /**
417     * Sets the amount of time after activation of auto-scrolling that is takes
418     * to reach target velocity for the current touch position.
419     * <p>
420     * Specifying a duration greater than zero prevents sudden jumps in
421     * velocity.
422     *
423     * @param durationMillis The ramp-up duration in milliseconds.
424     * @return The scroll helper, which may used to chain setter calls.
425     */
426    public AutoScrollHelper setRampUpDuration(int durationMillis) {
427        mScroller.setDuration(durationMillis);
428        return this;
429    }
430
431    /**
432     * Handles touch events by activating automatic scrolling, adjusting scroll
433     * velocity, or stopping.
434     * <p>
435     * If {@link #isExclusiveEnabled()} is false, always returns false so that
436     * the host view may handle touch events. Otherwise, returns true when
437     * automatic scrolling is active and false otherwise.
438     */
439    @Override
440    public boolean onTouch(View v, MotionEvent event) {
441        if (!mEnabled) {
442            return false;
443        }
444
445        final int action = event.getActionMasked();
446        switch (action) {
447            case MotionEvent.ACTION_DOWN:
448                mDownTime = event.getDownTime();
449            case MotionEvent.ACTION_MOVE:
450                final float xValue = getEdgeValue(mRelativeEdges[HORIZONTAL], v.getWidth(),
451                        mMaximumEdges[HORIZONTAL], event.getX());
452                final float yValue = getEdgeValue(mRelativeEdges[VERTICAL], v.getHeight(),
453                        mMaximumEdges[VERTICAL], event.getY());
454                final float maxVelX = constrain(mRelativeVelocity[HORIZONTAL] * mTarget.getWidth(),
455                        mMinimumVelocity[HORIZONTAL], mMaximumVelocity[HORIZONTAL]);
456                final float maxVelY = constrain(mRelativeVelocity[VERTICAL] * mTarget.getHeight(),
457                        mMinimumVelocity[VERTICAL], mMaximumVelocity[VERTICAL]);
458                mScroller.setTargetVelocity(xValue * maxVelX, yValue * maxVelY);
459
460                if ((xValue != 0 || yValue != 0) && !mActive) {
461                    mActive = true;
462                    mResetScroller = true;
463                    if (mRunnable == null) {
464                        mRunnable = new AutoScrollRunnable();
465                    }
466                    if (mSkipDelay) {
467                        mTarget.postOnAnimation(mRunnable);
468                    } else {
469                        mSkipDelay = true;
470                        mTarget.postOnAnimationDelayed(mRunnable, mActivationDelay);
471                    }
472                }
473                break;
474            case MotionEvent.ACTION_UP:
475            case MotionEvent.ACTION_CANCEL:
476                stop(true);
477                break;
478        }
479
480        return mExclusiveEnabled && mScrolling;
481    }
482
483    /**
484     * Override this method to scroll the target view by the specified number
485     * of pixels.
486     * <p>
487     * Returns whether the target view was able to scroll the requested amount.
488     *
489     * @param deltaX The amount to scroll in the X direction, in pixels.
490     * @param deltaY The amount to scroll in the Y direction, in pixels.
491     * @return true if the target view was able to scroll the requested amount.
492     */
493    public abstract boolean onScrollBy(int deltaX, int deltaY);
494
495    /**
496     * Returns the interpolated position of a touch point relative to an edge
497     * defined by its relative inset, its maximum absolute inset, and the edge
498     * interpolator.
499     *
500     * @param relativeValue The size of the inset relative to the total size.
501     * @param size Total size.
502     * @param maxValue The maximum size of the inset, used to clamp (relative *
503     *            total).
504     * @param current Touch position within within the total size.
505     * @return Interpolated value of the touch position within the edge.
506     */
507    private float getEdgeValue(float relativeValue, float size, float maxValue, float current) {
508        // For now, leading and trailing edges are always the same size.
509        final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue);
510        final float valueLeading = constrainEdgeValue(current, edgeSize);
511        final float valueTrailing = constrainEdgeValue(size - current, edgeSize);
512        final float value = (valueTrailing - valueLeading);
513        final float interpolated;
514        if (value < 0) {
515            interpolated = -mEdgeInterpolator.getInterpolation(-value);
516        } else if (value > 0) {
517            interpolated = mEdgeInterpolator.getInterpolation(value);
518        } else {
519            return 0;
520        }
521
522        return constrain(interpolated, -1, 1);
523    }
524
525    private float constrainEdgeValue(float current, float leading) {
526        if (leading == 0) {
527            return 0;
528        }
529
530        switch (mEdgeType) {
531            case EDGE_TYPE_INSIDE:
532            case EDGE_TYPE_INSIDE_EXTEND:
533                if (current < leading) {
534                    if (current >= 0) {
535                        // Movement up to the edge is scaled.
536                        return 1f - current / leading;
537                    } else if (mActive && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) {
538                        // Movement beyond the edge is always maximum.
539                        return 1f;
540                    }
541                }
542                break;
543            case EDGE_TYPE_OUTSIDE:
544                if (current < 0) {
545                    // Movement beyond the edge is scaled.
546                    return current / -leading;
547                }
548                break;
549        }
550
551        return 0;
552    }
553
554    private static float constrain(float value, float min, float max) {
555        if (value > max) {
556            return max;
557        } else if (value < min) {
558            return min;
559        } else {
560            return value;
561        }
562    }
563
564    /**
565     * Stops auto-scrolling immediately, optionally reseting the auto-scrolling
566     * delay.
567     *
568     * @param reset Whether to reset the auto-scrolling delay.
569     */
570    private void stop(boolean reset) {
571        mActive = false;
572        mScrolling = false;
573        mSkipDelay = !reset;
574
575        if (mRunnable != null) {
576            mTarget.removeCallbacks(mRunnable);
577        }
578    }
579
580    /**
581     * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view,
582     * canceling any ongoing touch events.
583     */
584    private void cancelTargetTouch() {
585        final MotionEvent cancel = MotionEvent.obtain(
586                mDownTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_CANCEL, 0, 0, 0);
587        cancel.setAction(MotionEvent.ACTION_CANCEL);
588        mTarget.onTouchEvent(cancel);
589        cancel.recycle();
590    }
591
592    private class AutoScrollRunnable implements Runnable {
593        @Override
594        public void run() {
595            if (!mActive) {
596                return;
597            }
598
599            if (mResetScroller) {
600                mResetScroller = false;
601                mScroller.start();
602            }
603
604            final View target = mTarget;
605            final ClampedScroller scroller = mScroller;
606            scroller.computeScrollDelta();
607
608            final int deltaX = scroller.getDeltaX();
609            final int deltaY = scroller.getDeltaY();
610            if ((deltaX != 0 || deltaY != 0 || !scroller.isFinished())
611                    && onScrollBy(deltaX, deltaY)) {
612                // Update whether we're actively scrolling.
613                final boolean scrolling = (deltaX != 0 || deltaY != 0);
614                if (mScrolling != scrolling) {
615                    mScrolling = scrolling;
616
617                    // If we just started actively scrolling, make sure any down
618                    // or move events send to the target view are canceled.
619                    if (mExclusiveEnabled && scrolling) {
620                        cancelTargetTouch();
621                    }
622                }
623
624                // Keep going until the scroller has permanently stopped or the
625                // view can't scroll any more. If the user moves their finger
626                // again, we'll repost the animation.
627                target.postOnAnimation(this);
628            } else {
629                stop(false);
630            }
631        }
632    }
633
634    /**
635     * Scroller whose velocity follows the curve of an {@link Interpolator} and
636     * is clamped to the interpolated 0f value before starting and the
637     * interpolated 1f value after a specified duration.
638     */
639    private static class ClampedScroller {
640        private final Interpolator mInterpolator = new AccelerateInterpolator();
641
642        private int mDuration;
643        private float mTargetVelocityX;
644        private float mTargetVelocityY;
645
646        private long mStartTime;
647        private long mDeltaTime;
648        private int mDeltaX;
649        private int mDeltaY;
650
651        /**
652         * Creates a new ramp-up scroller that reaches full velocity after a
653         * specified duration.
654         */
655        public ClampedScroller() {
656            reset();
657        }
658
659        public void setDuration(int durationMillis) {
660            mDuration = durationMillis;
661        }
662
663        /**
664         * Starts the scroller at the current animation time.
665         */
666        public void start() {
667            mStartTime = AnimationUtils.currentAnimationTimeMillis();
668            mDeltaTime = mStartTime;
669        }
670
671        /**
672         * Returns whether the scroller is finished, which means that its
673         * acceleration is zero.
674         *
675         * @return Whether the scroller is finished.
676         */
677        public boolean isFinished() {
678            if (mTargetVelocityX == 0 && mTargetVelocityY == 0) {
679                return true;
680            }
681            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
682            final long elapsedSinceStart = currentTime - mStartTime;
683            return elapsedSinceStart > mDuration;
684        }
685
686        /**
687         * Stops the scroller and resets its values.
688         */
689        public void reset() {
690            mStartTime = -1;
691            mDeltaTime = -1;
692            mDeltaX = 0;
693            mDeltaY = 0;
694        }
695
696        /**
697         * Computes the current scroll deltas. This usually only be called after
698         * starting the scroller with {@link #start()}.
699         *
700         * @see #getDeltaX()
701         * @see #getDeltaY()
702         */
703        public void computeScrollDelta() {
704            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
705            final long elapsedSinceStart = currentTime - mStartTime;
706            final float value;
707            if (mStartTime < 0) {
708                value = 0f;
709            } else if (elapsedSinceStart < mDuration) {
710                value = (float) elapsedSinceStart / mDuration;
711            } else {
712                value = 1f;
713            }
714
715            final float scale = mInterpolator.getInterpolation(value);
716            final long elapsedSinceDelta = currentTime - mDeltaTime;
717
718            mDeltaTime = currentTime;
719            mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX);
720            mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY);
721        }
722
723        /**
724         * Sets the target velocity for this scroller.
725         *
726         * @param x The target X velocity in pixels per millisecond.
727         * @param y The target Y velocity in pixels per millisecond.
728         */
729        public void setTargetVelocity(float x, float y) {
730            mTargetVelocityX = x;
731            mTargetVelocityY = y;
732        }
733
734        /**
735         * The distance traveled in the X-coordinate computed by the last call
736         * to {@link #computeScrollDelta()}.
737         */
738        public int getDeltaX() {
739            return mDeltaX;
740        }
741
742        /**
743         * The distance traveled in the Y-coordinate computed by the last call
744         * to {@link #computeScrollDelta()}.
745         */
746        public int getDeltaY() {
747            return mDeltaY;
748        }
749    }
750
751    /**
752     * Implementation of {@link AutoScrollHelper} that knows how to scroll
753     * generic {@link AbsListView}s.
754     */
755    public static class AbsListViewAutoScroller extends AutoScrollHelper {
756        private final AbsListView mTarget;
757
758        public AbsListViewAutoScroller(AbsListView target) {
759            super(target);
760            mTarget = target;
761        }
762
763        @Override
764        public boolean onScrollBy(int deltaX, int deltaY) {
765            return mTarget.scrollListBy(deltaY);
766        }
767    }
768}
769