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 #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and
36 * {@link #canTargetScrollVertically} methods. See
37 * {@link AbsListViewAutoScroller} for an {@link android.widget.AbsListView}
38 * -specific implementation.
39 * <p>
40 * <h1>Activation</h1> Automatic scrolling starts when the user touches within
41 * an activation area. By default, activation areas are defined as the top,
42 * left, right, and bottom 20% of the host view's total area. Touching within
43 * the top activation area scrolls up, left scrolls to the left, and so on.
44 * <p>
45 * As the user touches closer to the extreme edge of the activation area,
46 * scrolling accelerates up to a maximum velocity. When using the default edge
47 * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds
48 * will scroll at the maximum velocity.
49 * <p>
50 * The following activation properties may be configured:
51 * <ul>
52 * <li>Delay after entering activation area before auto-scrolling begins, see
53 * {@link #setActivationDelay}. Default value is
54 * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps.
55 * <li>Location of activation areas, see {@link #setEdgeType}. Default value is
56 * {@link #EDGE_TYPE_INSIDE_EXTEND}.
57 * <li>Size of activation areas relative to view size, see
58 * {@link #setRelativeEdges}. Default value is 20% for both vertical and
59 * horizontal edges.
60 * <li>Maximum size used to constrain relative size, see
61 * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}.
62 * </ul>
63 * <h1>Scrolling</h1> When automatic scrolling is active, the helper will
64 * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets.
65 * <p>
66 * The following scrolling properties may be configured:
67 * <ul>
68 * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default
69 * value is 500 milliseconds.
70 * <li>Acceleration ramp-down duration, see {@link #setRampDownDuration}.
71 * Default value is 500 milliseconds.
72 * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}.
73 * Default value is 100% per second for both vertical and horizontal.
74 * <li>Minimum velocity used to constrain relative velocity, see
75 * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the
76 * larger of either this value or the relative target value. Default value is
77 * approximately 5 centimeters or 315 dips per second.
78 * <li>Maximum velocity used to constrain relative velocity, see
79 * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or
80 * 1575 dips per second.
81 * </ul>
82 */
83public abstract class AutoScrollHelper implements View.OnTouchListener {
84    /**
85     * Constant passed to {@link #setRelativeEdges} or
86     * {@link #setRelativeVelocity}. Using this value ensures that the computed
87     * relative value is ignored and the absolute maximum value is always used.
88     */
89    public static final float RELATIVE_UNSPECIFIED = 0;
90
91    /**
92     * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity},
93     * or {@link #setMinimumVelocity}. Using this value ensures that the
94     * computed relative value is always used without constraining to a
95     * particular minimum or maximum value.
96     */
97    public static final float NO_MAX = Float.MAX_VALUE;
98
99    /**
100     * Constant passed to {@link #setMaximumEdges}, or
101     * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this
102     * value ensures that the computed relative value is always used without
103     * constraining to a particular minimum or maximum value.
104     */
105    public static final float NO_MIN = 0;
106
107    /**
108     * Edge type that specifies an activation area starting at the view bounds
109     * and extending inward. Moving outside the view bounds will stop scrolling.
110     *
111     * @see #setEdgeType
112     */
113    public static final int EDGE_TYPE_INSIDE = 0;
114
115    /**
116     * Edge type that specifies an activation area starting at the view bounds
117     * and extending inward. After activation begins, moving outside the view
118     * bounds will continue scrolling.
119     *
120     * @see #setEdgeType
121     */
122    public static final int EDGE_TYPE_INSIDE_EXTEND = 1;
123
124    /**
125     * Edge type that specifies an activation area starting at the view bounds
126     * and extending outward. Moving inside the view bounds will stop scrolling.
127     *
128     * @see #setEdgeType
129     */
130    public static final int EDGE_TYPE_OUTSIDE = 2;
131
132    private static final int HORIZONTAL = 0;
133    private static final int VERTICAL = 1;
134
135    /** Scroller used to control acceleration toward maximum velocity. */
136    private final ClampedScroller mScroller = new ClampedScroller();
137
138    /** Interpolator used to scale velocity with touch position. */
139    private final Interpolator mEdgeInterpolator = new AccelerateInterpolator();
140
141    /** The view to auto-scroll. Might not be the source of touch events. */
142    private final View mTarget;
143
144    /** Runnable used to animate scrolling. */
145    private Runnable mRunnable;
146
147    /** Edge insets used to activate auto-scrolling. */
148    private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
149
150    /** Clamping values for edge insets used to activate auto-scrolling. */
151    private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX };
152
153    /** The type of edge being used. */
154    private int mEdgeType;
155
156    /** Delay after entering an activation edge before auto-scrolling begins. */
157    private int mActivationDelay;
158
159    /** Relative scrolling velocity at maximum edge distance. */
160    private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
161
162    /** Clamping values used for scrolling velocity. */
163    private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN };
164
165    /** Clamping values used for scrolling velocity. */
166    private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX };
167
168    /** Whether to start activation immediately. */
169    private boolean mAlreadyDelayed;
170
171    /** Whether to reset the scroller start time on the next animation. */
172    private boolean mNeedsReset;
173
174    /** Whether to send a cancel motion event to the target view. */
175    private boolean mNeedsCancel;
176
177    /** Whether the auto-scroller is actively scrolling. */
178    private boolean mAnimating;
179
180    /** Whether the auto-scroller is enabled. */
181    private boolean mEnabled;
182
183    /** Whether the auto-scroller consumes events when scrolling. */
184    private boolean mExclusive;
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 = 500;
195    private static final int DEFAULT_RAMP_DOWN_DURATION = 500;
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        setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION);
224    }
225
226    /**
227     * Sets whether the scroll helper is enabled and should respond to touch
228     * events.
229     *
230     * @param enabled Whether the scroll helper is enabled.
231     * @return The scroll helper, which may used to chain setter calls.
232     */
233    public AutoScrollHelper setEnabled(boolean enabled) {
234        if (mEnabled && !enabled) {
235            requestStop();
236        }
237
238        mEnabled = enabled;
239        return this;
240    }
241
242    /**
243     * @return True if this helper is enabled and responding to touch events.
244     */
245    public boolean isEnabled() {
246        return mEnabled;
247    }
248
249    /**
250     * Enables or disables exclusive handling of touch events during scrolling.
251     * By default, exclusive handling is disabled and the target view receives
252     * all touch events.
253     * <p>
254     * When enabled, {@link #onTouch} will return true if the helper is
255     * currently scrolling and false otherwise.
256     *
257     * @param exclusive True to exclusively handle touch events during scrolling,
258     *            false to allow the target view to receive all touch events.
259     * @return The scroll helper, which may used to chain setter calls.
260     */
261    public AutoScrollHelper setExclusive(boolean exclusive) {
262        mExclusive = exclusive;
263        return this;
264    }
265
266    /**
267     * Indicates whether the scroll helper handles touch events exclusively
268     * during scrolling.
269     *
270     * @return True if exclusive handling of touch events during scrolling is
271     *         enabled, false otherwise.
272     * @see #setExclusive(boolean)
273     */
274    public boolean isExclusive() {
275        return mExclusive;
276    }
277
278    /**
279     * Sets the absolute maximum scrolling velocity.
280     * <p>
281     * If relative velocity is not specified, scrolling will always reach the
282     * same maximum velocity. If both relative and maximum velocities are
283     * specified, the maximum velocity will be used to clamp the calculated
284     * relative velocity.
285     *
286     * @param horizontalMax The maximum horizontal scrolling velocity, or
287     *            {@link #NO_MAX} to leave the relative value unconstrained.
288     * @param verticalMax The maximum vertical scrolling velocity, or
289     *            {@link #NO_MAX} to leave the relative value unconstrained.
290     * @return The scroll helper, which may used to chain setter calls.
291     */
292    public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) {
293        mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f;
294        mMaximumVelocity[VERTICAL] = verticalMax / 1000f;
295        return this;
296    }
297
298    /**
299     * Sets the absolute minimum scrolling velocity.
300     * <p>
301     * If both relative and minimum velocities are specified, the minimum
302     * velocity will be used to clamp the calculated relative velocity.
303     *
304     * @param horizontalMin The minimum horizontal scrolling velocity, or
305     *            {@link #NO_MIN} to leave the relative value unconstrained.
306     * @param verticalMin The minimum vertical scrolling velocity, or
307     *            {@link #NO_MIN} to leave the relative value unconstrained.
308     * @return The scroll helper, which may used to chain setter calls.
309     */
310    public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) {
311        mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f;
312        mMinimumVelocity[VERTICAL] = verticalMin / 1000f;
313        return this;
314    }
315
316    /**
317     * Sets the target scrolling velocity relative to the host view's
318     * dimensions.
319     * <p>
320     * If both relative and maximum velocities are specified, the maximum
321     * velocity will be used to clamp the calculated relative velocity.
322     *
323     * @param horizontal The target horizontal velocity as a fraction of the
324     *            host view width per second, or {@link #RELATIVE_UNSPECIFIED}
325     *            to ignore.
326     * @param vertical The target vertical velocity as a fraction of the host
327     *            view height per second, or {@link #RELATIVE_UNSPECIFIED} to
328     *            ignore.
329     * @return The scroll helper, which may used to chain setter calls.
330     */
331    public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) {
332        mRelativeVelocity[HORIZONTAL] = horizontal / 1000f;
333        mRelativeVelocity[VERTICAL] = vertical / 1000f;
334        return this;
335    }
336
337    /**
338     * Sets the activation edge type, one of:
339     * <ul>
340     * <li>{@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside
341     * the bounds of the host view. If touch moves outside the bounds, scrolling
342     * will stop.
343     * <li>{@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to
344     * scroll when touch moves outside the bounds of the host view.
345     * <li>{@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches
346     * that move outside the bounds of the host view.
347     * </ul>
348     *
349     * @param type The type of edge to use.
350     * @return The scroll helper, which may used to chain setter calls.
351     */
352    public AutoScrollHelper setEdgeType(int type) {
353        mEdgeType = type;
354        return this;
355    }
356
357    /**
358     * Sets the activation edge size relative to the host view's dimensions.
359     * <p>
360     * If both relative and maximum edges are specified, the maximum edge will
361     * be used to constrain the calculated relative edge size.
362     *
363     * @param horizontal The horizontal edge size as a fraction of the host view
364     *            width, or {@link #RELATIVE_UNSPECIFIED} to always use the
365     *            maximum value.
366     * @param vertical The vertical edge size as a fraction of the host view
367     *            height, or {@link #RELATIVE_UNSPECIFIED} to always use the
368     *            maximum value.
369     * @return The scroll helper, which may used to chain setter calls.
370     */
371    public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) {
372        mRelativeEdges[HORIZONTAL] = horizontal;
373        mRelativeEdges[VERTICAL] = vertical;
374        return this;
375    }
376
377    /**
378     * Sets the absolute maximum edge size.
379     * <p>
380     * If relative edge size is not specified, activation edges will always be
381     * the maximum edge size. If both relative and maximum edges are specified,
382     * the maximum edge will be used to constrain the calculated relative edge
383     * size.
384     *
385     * @param horizontalMax The maximum horizontal edge size in pixels, or
386     *            {@link #NO_MAX} to use the unconstrained calculated relative
387     *            value.
388     * @param verticalMax The maximum vertical edge size in pixels, or
389     *            {@link #NO_MAX} to use the unconstrained calculated relative
390     *            value.
391     * @return The scroll helper, which may used to chain setter calls.
392     */
393    public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) {
394        mMaximumEdges[HORIZONTAL] = horizontalMax;
395        mMaximumEdges[VERTICAL] = verticalMax;
396        return this;
397    }
398
399    /**
400     * Sets the delay after entering an activation edge before activation of
401     * auto-scrolling. By default, the activation delay is set to
402     * {@link ViewConfiguration#getTapTimeout()}.
403     * <p>
404     * Specifying a delay of zero will start auto-scrolling immediately after
405     * the touch position enters an activation edge.
406     *
407     * @param delayMillis The activation delay in milliseconds.
408     * @return The scroll helper, which may used to chain setter calls.
409     */
410    public AutoScrollHelper setActivationDelay(int delayMillis) {
411        mActivationDelay = delayMillis;
412        return this;
413    }
414
415    /**
416     * Sets the amount of time after activation of auto-scrolling that is takes
417     * to reach target velocity for the current touch position.
418     * <p>
419     * Specifying a duration greater than zero prevents sudden jumps in
420     * velocity.
421     *
422     * @param durationMillis The ramp-up duration in milliseconds.
423     * @return The scroll helper, which may used to chain setter calls.
424     */
425    public AutoScrollHelper setRampUpDuration(int durationMillis) {
426        mScroller.setRampUpDuration(durationMillis);
427        return this;
428    }
429
430    /**
431     * Sets the amount of time after de-activation of auto-scrolling that is
432     * takes to slow to a stop.
433     * <p>
434     * Specifying a duration greater than zero prevents sudden jumps in
435     * velocity.
436     *
437     * @param durationMillis The ramp-down duration in milliseconds.
438     * @return The scroll helper, which may used to chain setter calls.
439     */
440    public AutoScrollHelper setRampDownDuration(int durationMillis) {
441        mScroller.setRampDownDuration(durationMillis);
442        return this;
443    }
444
445    /**
446     * Handles touch events by activating automatic scrolling, adjusting scroll
447     * velocity, or stopping.
448     * <p>
449     * If {@link #isExclusive()} is false, always returns false so that
450     * the host view may handle touch events. Otherwise, returns true when
451     * automatic scrolling is active and false otherwise.
452     */
453    @Override
454    public boolean onTouch(View v, MotionEvent event) {
455        if (!mEnabled) {
456            return false;
457        }
458
459        final int action = event.getActionMasked();
460        switch (action) {
461            case MotionEvent.ACTION_DOWN:
462                mNeedsCancel = true;
463                mAlreadyDelayed = false;
464                // $FALL-THROUGH$
465            case MotionEvent.ACTION_MOVE:
466                final float xTargetVelocity = computeTargetVelocity(
467                        HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth());
468                final float yTargetVelocity = computeTargetVelocity(
469                        VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight());
470                mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity);
471
472                // If the auto scroller was not previously active, but it should
473                // be, then update the state and start animations.
474                if (!mAnimating && shouldAnimate()) {
475                    startAnimating();
476                }
477                break;
478            case MotionEvent.ACTION_UP:
479            case MotionEvent.ACTION_CANCEL:
480                requestStop();
481                break;
482        }
483
484        return mExclusive && mAnimating;
485    }
486
487    /**
488     * @return whether the target is able to scroll in the requested direction
489     */
490    private boolean shouldAnimate() {
491        final ClampedScroller scroller = mScroller;
492        final int verticalDirection = scroller.getVerticalDirection();
493        final int horizontalDirection = scroller.getHorizontalDirection();
494
495        return verticalDirection != 0 && canTargetScrollVertically(verticalDirection)
496                || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection);
497    }
498
499    /**
500     * Starts the scroll animation.
501     */
502    private void startAnimating() {
503        if (mRunnable == null) {
504            mRunnable = new ScrollAnimationRunnable();
505        }
506
507        mAnimating = true;
508        mNeedsReset = true;
509
510        if (!mAlreadyDelayed && mActivationDelay > 0) {
511            mTarget.postOnAnimationDelayed(mRunnable, mActivationDelay);
512        } else {
513            mRunnable.run();
514        }
515
516        // If we start animating again before the user lifts their finger, we
517        // already know it's not a tap and don't need an activation delay.
518        mAlreadyDelayed = true;
519    }
520
521    /**
522     * Requests that the scroll animation slow to a stop. If there is an
523     * activation delay, this may occur between posting the animation and
524     * actually running it.
525     */
526    private void requestStop() {
527        if (mNeedsReset) {
528            // The animation has been posted, but hasn't run yet. Manually
529            // stopping animation will prevent it from running.
530            mAnimating = false;
531        } else {
532            mScroller.requestStop();
533        }
534    }
535
536    private float computeTargetVelocity(
537            int direction, float coordinate, float srcSize, float dstSize) {
538        final float relativeEdge = mRelativeEdges[direction];
539        final float maximumEdge = mMaximumEdges[direction];
540        final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate);
541        if (value == 0) {
542            // The edge in this direction is not activated.
543            return 0;
544        }
545
546        final float relativeVelocity = mRelativeVelocity[direction];
547        final float minimumVelocity = mMinimumVelocity[direction];
548        final float maximumVelocity = mMaximumVelocity[direction];
549        final float targetVelocity = relativeVelocity * dstSize;
550
551        // Target velocity is adjusted for interpolated edge position, then
552        // clamped to the minimum and maximum values. Later, this value will be
553        // adjusted for time-based acceleration.
554        if (value > 0) {
555            return constrain(value * targetVelocity, minimumVelocity, maximumVelocity);
556        } else {
557            return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity);
558        }
559    }
560
561    /**
562     * Override this method to scroll the target view by the specified number of
563     * pixels.
564     *
565     * @param deltaX The number of pixels to scroll by horizontally.
566     * @param deltaY The number of pixels to scroll by vertically.
567     */
568    public abstract void scrollTargetBy(int deltaX, int deltaY);
569
570    /**
571     * Override this method to return whether the target view can be scrolled
572     * horizontally in a certain direction.
573     *
574     * @param direction Negative to check scrolling left, positive to check
575     *            scrolling right.
576     * @return true if the target view is able to horizontally scroll in the
577     *         specified direction.
578     */
579    public abstract boolean canTargetScrollHorizontally(int direction);
580
581    /**
582     * Override this method to return whether the target view can be scrolled
583     * vertically in a certain direction.
584     *
585     * @param direction Negative to check scrolling up, positive to check
586     *            scrolling down.
587     * @return true if the target view is able to vertically scroll in the
588     *         specified direction.
589     */
590    public abstract boolean canTargetScrollVertically(int direction);
591
592    /**
593     * Returns the interpolated position of a touch point relative to an edge
594     * defined by its relative inset, its maximum absolute inset, and the edge
595     * interpolator.
596     *
597     * @param relativeValue The size of the inset relative to the total size.
598     * @param size Total size.
599     * @param maxValue The maximum size of the inset, used to clamp (relative *
600     *            total).
601     * @param current Touch position within within the total size.
602     * @return Interpolated value of the touch position within the edge.
603     */
604    private float getEdgeValue(float relativeValue, float size, float maxValue, float current) {
605        // For now, leading and trailing edges are always the same size.
606        final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue);
607        final float valueLeading = constrainEdgeValue(current, edgeSize);
608        final float valueTrailing = constrainEdgeValue(size - current, edgeSize);
609        final float value = (valueTrailing - valueLeading);
610        final float interpolated;
611        if (value < 0) {
612            interpolated = -mEdgeInterpolator.getInterpolation(-value);
613        } else if (value > 0) {
614            interpolated = mEdgeInterpolator.getInterpolation(value);
615        } else {
616            return 0;
617        }
618
619        return constrain(interpolated, -1, 1);
620    }
621
622    private float constrainEdgeValue(float current, float leading) {
623        if (leading == 0) {
624            return 0;
625        }
626
627        switch (mEdgeType) {
628            case EDGE_TYPE_INSIDE:
629            case EDGE_TYPE_INSIDE_EXTEND:
630                if (current < leading) {
631                    if (current >= 0) {
632                        // Movement up to the edge is scaled.
633                        return 1f - current / leading;
634                    } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) {
635                        // Movement beyond the edge is always maximum.
636                        return 1f;
637                    }
638                }
639                break;
640            case EDGE_TYPE_OUTSIDE:
641                if (current < 0) {
642                    // Movement beyond the edge is scaled.
643                    return current / -leading;
644                }
645                break;
646        }
647
648        return 0;
649    }
650
651    private static int constrain(int value, int min, int max) {
652        if (value > max) {
653            return max;
654        } else if (value < min) {
655            return min;
656        } else {
657            return value;
658        }
659    }
660
661    private static float constrain(float value, float min, float max) {
662        if (value > max) {
663            return max;
664        } else if (value < min) {
665            return min;
666        } else {
667            return value;
668        }
669    }
670
671    /**
672     * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view,
673     * canceling any ongoing touch events.
674     */
675    private void cancelTargetTouch() {
676        final long eventTime = SystemClock.uptimeMillis();
677        final MotionEvent cancel = MotionEvent.obtain(
678                eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0);
679        mTarget.onTouchEvent(cancel);
680        cancel.recycle();
681    }
682
683    private class ScrollAnimationRunnable implements Runnable {
684        @Override
685        public void run() {
686            if (!mAnimating) {
687                return;
688            }
689
690            if (mNeedsReset) {
691                mNeedsReset = false;
692                mScroller.start();
693            }
694
695            final ClampedScroller scroller = mScroller;
696            if (scroller.isFinished() || !shouldAnimate()) {
697                mAnimating = false;
698                return;
699            }
700
701            if (mNeedsCancel) {
702                mNeedsCancel = false;
703                cancelTargetTouch();
704            }
705
706            scroller.computeScrollDelta();
707
708            final int deltaX = scroller.getDeltaX();
709            final int deltaY = scroller.getDeltaY();
710            scrollTargetBy(deltaX,  deltaY);
711
712            // Keep going until the scroller has permanently stopped.
713            mTarget.postOnAnimation(this);
714        }
715    }
716
717    /**
718     * Scroller whose velocity follows the curve of an {@link Interpolator} and
719     * is clamped to the interpolated 0f value before starting and the
720     * interpolated 1f value after a specified duration.
721     */
722    private static class ClampedScroller {
723        private int mRampUpDuration;
724        private int mRampDownDuration;
725        private float mTargetVelocityX;
726        private float mTargetVelocityY;
727
728        private long mStartTime;
729
730        private long mDeltaTime;
731        private int mDeltaX;
732        private int mDeltaY;
733
734        private long mStopTime;
735        private float mStopValue;
736        private int mEffectiveRampDown;
737
738        /**
739         * Creates a new ramp-up scroller that reaches full velocity after a
740         * specified duration.
741         */
742        public ClampedScroller() {
743            mStartTime = Long.MIN_VALUE;
744            mStopTime = -1;
745            mDeltaTime = 0;
746            mDeltaX = 0;
747            mDeltaY = 0;
748        }
749
750        public void setRampUpDuration(int durationMillis) {
751            mRampUpDuration = durationMillis;
752        }
753
754        public void setRampDownDuration(int durationMillis) {
755            mRampDownDuration = durationMillis;
756        }
757
758        /**
759         * Starts the scroller at the current animation time.
760         */
761        public void start() {
762            mStartTime = AnimationUtils.currentAnimationTimeMillis();
763            mStopTime = -1;
764            mDeltaTime = mStartTime;
765            mStopValue = 0.5f;
766            mDeltaX = 0;
767            mDeltaY = 0;
768        }
769
770        /**
771         * Stops the scroller at the current animation time.
772         */
773        public void requestStop() {
774            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
775            mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration);
776            mStopValue = getValueAt(currentTime);
777            mStopTime = currentTime;
778        }
779
780        public boolean isFinished() {
781            return mStopTime > 0
782                    && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
783        }
784
785        private float getValueAt(long currentTime) {
786            if (currentTime < mStartTime) {
787                return 0f;
788            } else if (mStopTime < 0 || currentTime < mStopTime) {
789                final long elapsedSinceStart = currentTime - mStartTime;
790                return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1);
791            } else {
792                final long elapsedSinceEnd = currentTime - mStopTime;
793                return (1 - mStopValue) + mStopValue
794                        * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1);
795            }
796        }
797
798        /**
799         * Interpolates the value along a parabolic curve corresponding to the equation
800         * <code>y = -4x * (x-1)</code>.
801         *
802         * @param value The value to interpolate, between 0 and 1.
803         * @return the interpolated value, between 0 and 1.
804         */
805        private float interpolateValue(float value) {
806            return -4 * value * value + 4 * value;
807        }
808
809        /**
810         * Computes the current scroll deltas. This usually only be called after
811         * starting the scroller with {@link #start()}.
812         *
813         * @see #getDeltaX()
814         * @see #getDeltaY()
815         */
816        public void computeScrollDelta() {
817            if (mDeltaTime == 0) {
818                throw new RuntimeException("Cannot compute scroll delta before calling start()");
819            }
820
821            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
822            final float value = getValueAt(currentTime);
823            final float scale = interpolateValue(value);
824            final long elapsedSinceDelta = currentTime - mDeltaTime;
825
826            mDeltaTime = currentTime;
827            mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX);
828            mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY);
829        }
830
831        /**
832         * Sets the target velocity for this scroller.
833         *
834         * @param x The target X velocity in pixels per millisecond.
835         * @param y The target Y velocity in pixels per millisecond.
836         */
837        public void setTargetVelocity(float x, float y) {
838            mTargetVelocityX = x;
839            mTargetVelocityY = y;
840        }
841
842        public int getHorizontalDirection() {
843            return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX));
844        }
845
846        public int getVerticalDirection() {
847            return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY));
848        }
849
850        /**
851         * The distance traveled in the X-coordinate computed by the last call
852         * to {@link #computeScrollDelta()}.
853         */
854        public int getDeltaX() {
855            return mDeltaX;
856        }
857
858        /**
859         * The distance traveled in the Y-coordinate computed by the last call
860         * to {@link #computeScrollDelta()}.
861         */
862        public int getDeltaY() {
863            return mDeltaY;
864        }
865    }
866
867    /**
868     * An implementation of {@link AutoScrollHelper} that knows how to scroll
869     * through an {@link AbsListView}.
870     */
871    public static class AbsListViewAutoScroller extends AutoScrollHelper {
872        private final AbsListView mTarget;
873
874        public AbsListViewAutoScroller(AbsListView target) {
875            super(target);
876
877            mTarget = target;
878        }
879
880        @Override
881        public void scrollTargetBy(int deltaX, int deltaY) {
882            mTarget.scrollListBy(deltaY);
883        }
884
885        @Override
886        public boolean canTargetScrollHorizontally(int direction) {
887            // List do not scroll horizontally.
888            return false;
889        }
890
891        @Override
892        public boolean canTargetScrollVertically(int direction) {
893            final AbsListView target = mTarget;
894            final int itemCount = target.getCount();
895            if (itemCount == 0) {
896                return false;
897            }
898
899            final int childCount = target.getChildCount();
900            final int firstPosition = target.getFirstVisiblePosition();
901            final int lastPosition = firstPosition + childCount;
902
903            if (direction > 0) {
904                // Are we already showing the entire last item?
905                if (lastPosition >= itemCount) {
906                    final View lastView = target.getChildAt(childCount - 1);
907                    if (lastView.getBottom() <= target.getHeight()) {
908                        return false;
909                    }
910                }
911            } else if (direction < 0) {
912                // Are we already showing the entire first item?
913                if (firstPosition <= 0) {
914                    final View firstView = target.getChildAt(0);
915                    if (firstView.getTop() >= 0) {
916                        return false;
917                    }
918                }
919            } else {
920                // The behavior for direction 0 is undefined and we can return
921                // whatever we want.
922                return false;
923            }
924
925            return true;
926        }
927    }
928}
929