[go: nahoru, domu]

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 android.support.v4.widget;
18
19import android.content.res.Resources;
20import android.os.SystemClock;
21import android.support.v4.view.MotionEventCompat;
22import android.support.v4.view.ViewCompat;
23import android.util.DisplayMetrics;
24import android.view.MotionEvent;
25import android.view.View;
26import android.view.ViewConfiguration;
27import android.view.animation.AccelerateInterpolator;
28import android.view.animation.AnimationUtils;
29import android.view.animation.Interpolator;
30
31/**
32 * AutoScrollHelper is a utility class for adding automatic edge-triggered
33 * scrolling to Views.
34 * <p>
35 * <b>Note:</b> Implementing classes are responsible for overriding the
36 * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and
37 * {@link #canTargetScrollVertically} methods. See
38 * {@link ListViewAutoScrollHelper} for a {@link android.widget.ListView}
39 * -specific implementation.
40 * <p>
41 * <h1>Activation</h1> Automatic scrolling starts when the user touches within
42 * an activation area. By default, activation areas are defined as the top,
43 * left, right, and bottom 20% of the host view's total area. Touching within
44 * the top activation area scrolls up, left scrolls to the left, and so on.
45 * <p>
46 * As the user touches closer to the extreme edge of the activation area,
47 * scrolling accelerates up to a maximum velocity. When using the default edge
48 * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds
49 * will scroll at the maximum velocity.
50 * <p>
51 * The following activation properties may be configured:
52 * <ul>
53 * <li>Delay after entering activation area before auto-scrolling begins, see
54 * {@link #setActivationDelay}. Default value is
55 * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps.
56 * <li>Location of activation areas, see {@link #setEdgeType}. Default value is
57 * {@link #EDGE_TYPE_INSIDE_EXTEND}.
58 * <li>Size of activation areas relative to view size, see
59 * {@link #setRelativeEdges}. Default value is 20% for both vertical and
60 * horizontal edges.
61 * <li>Maximum size used to constrain relative size, see
62 * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}.
63 * </ul>
64 * <h1>Scrolling</h1> When automatic scrolling is active, the helper will
65 * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets.
66 * <p>
67 * The following scrolling properties may be configured:
68 * <ul>
69 * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default
70 * value is 500 milliseconds.
71 * <li>Acceleration ramp-down duration, see {@link #setRampDownDuration}.
72 * Default value is 500 milliseconds.
73 * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}.
74 * Default value is 100% per second for both vertical and horizontal.
75 * <li>Minimum velocity used to constrain relative velocity, see
76 * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the
77 * larger of either this value or the relative target value. Default value is
78 * approximately 5 centimeters or 315 dips per second.
79 * <li>Maximum velocity used to constrain relative velocity, see
80 * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or
81 * 1575 dips per second.
82 * </ul>
83 */
84public abstract class AutoScrollHelper implements View.OnTouchListener {
85    /**
86     * Constant passed to {@link #setRelativeEdges} or
87     * {@link #setRelativeVelocity}. Using this value ensures that the computed
88     * relative value is ignored and the absolute maximum value is always used.
89     */
90    public static final float RELATIVE_UNSPECIFIED = 0;
91
92    /**
93     * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity},
94     * or {@link #setMinimumVelocity}. Using this value ensures that the
95     * computed relative value is always used without constraining to a
96     * particular minimum or maximum value.
97     */
98    public static final float NO_MAX = Float.MAX_VALUE;
99
100    /**
101     * Constant passed to {@link #setMaximumEdges}, or
102     * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this
103     * value ensures that the computed relative value is always used without
104     * constraining to a particular minimum or maximum value.
105     */
106    public static final float NO_MIN = 0;
107
108    /**
109     * Edge type that specifies an activation area starting at the view bounds
110     * and extending inward. Moving outside the view bounds will stop scrolling.
111     *
112     * @see #setEdgeType
113     */
114    public static final int EDGE_TYPE_INSIDE = 0;
115
116    /**
117     * Edge type that specifies an activation area starting at the view bounds
118     * and extending inward. After activation begins, moving outside the view
119     * bounds will continue scrolling.
120     *
121     * @see #setEdgeType
122     */
123    public static final int EDGE_TYPE_INSIDE_EXTEND = 1;
124
125    /**
126     * Edge type that specifies an activation area starting at the view bounds
127     * and extending outward. Moving inside the view bounds will stop scrolling.
128     *
129     * @see #setEdgeType
130     */
131    public static final int EDGE_TYPE_OUTSIDE = 2;
132
133    private static final int HORIZONTAL = 0;
134    private static final int VERTICAL = 1;
135
136    /** Scroller used to control acceleration toward maximum velocity. */
137    private final ClampedScroller mScroller = new ClampedScroller();
138
139    /** Interpolator used to scale velocity with touch position. */
140    private final Interpolator mEdgeInterpolator = new AccelerateInterpolator();
141
142    /** The view to auto-scroll. Might not be the source of touch events. */
143    private final View mTarget;
144
145    /** Runnable used to animate scrolling. */
146    private Runnable mRunnable;
147
148    /** Edge insets used to activate auto-scrolling. */
149    private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
150
151    /** Clamping values for edge insets used to activate auto-scrolling. */
152    private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX };
153
154    /** The type of edge being used. */
155    private int mEdgeType;
156
157    /** Delay after entering an activation edge before auto-scrolling begins. */
158    private int mActivationDelay;
159
160    /** Relative scrolling velocity at maximum edge distance. */
161    private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
162
163    /** Clamping values used for scrolling velocity. */
164    private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN };
165
166    /** Clamping values used for scrolling velocity. */
167    private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX };
168
169    /** Whether to start activation immediately. */
170    private boolean mAlreadyDelayed;
171
172    /** Whether to reset the scroller start time on the next animation. */
173    private boolean mNeedsReset;
174
175    /** Whether to send a cancel motion event to the target view. */
176    private boolean mNeedsCancel;
177
178    /** Whether the auto-scroller is actively scrolling. */
179    private boolean mAnimating;
180
181    /** Whether the auto-scroller is enabled. */
182    private boolean mEnabled;
183
184    /** Whether the auto-scroller consumes events when scrolling. */
185    private boolean mExclusive;
186
187    // Default values.
188    private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND;
189    private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315;
190    private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575;
191    private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX;
192    private static final float DEFAULT_RELATIVE_EDGE = 0.2f;
193    private static final float DEFAULT_RELATIVE_VELOCITY = 1f;
194    private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout();
195    private static final int DEFAULT_RAMP_UP_DURATION = 500;
196    private static final int DEFAULT_RAMP_DOWN_DURATION = 500;
197
198    /**
199     * Creates a new helper for scrolling the specified target view.
200     * <p>
201     * The resulting helper may be configured by chaining setter calls and
202     * should be set as a touch listener on the target view.
203     * <p>
204     * By default, the helper is disabled and will not respond to touch events
205     * until it is enabled using {@link #setEnabled}.
206     *
207     * @param target The view to automatically scroll.
208     */
209    public AutoScrollHelper(View target) {
210        mTarget = target;
211
212        final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
213        final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
214        final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
215        setMaximumVelocity(maxVelocity, maxVelocity);
216        setMinimumVelocity(minVelocity, minVelocity);
217
218        setEdgeType(DEFAULT_EDGE_TYPE);
219        setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE);
220        setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE);
221        setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY);
222        setActivationDelay(DEFAULT_ACTIVATION_DELAY);
223        setRampUpDuration(DEFAULT_RAMP_UP_DURATION);
224        setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION);
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 (mEnabled && !enabled) {
236            requestStop();
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 exclusive True to exclusively handle touch events during scrolling,
259     *            false to allow the target view to receive all touch events.
260     * @return The scroll helper, which may used to chain setter calls.
261     */
262    public AutoScrollHelper setExclusive(boolean exclusive) {
263        mExclusive = exclusive;
264        return this;
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 #setExclusive(boolean)
274     */
275    public boolean isExclusive() {
276        return mExclusive;
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.setRampUpDuration(durationMillis);
428        return this;
429    }
430
431    /**
432     * Sets the amount of time after de-activation of auto-scrolling that is
433     * takes to slow to a stop.
434     * <p>
435     * Specifying a duration greater than zero prevents sudden jumps in
436     * velocity.
437     *
438     * @param durationMillis The ramp-down duration in milliseconds.
439     * @return The scroll helper, which may used to chain setter calls.
440     */
441    public AutoScrollHelper setRampDownDuration(int durationMillis) {
442        mScroller.setRampDownDuration(durationMillis);
443        return this;
444    }
445
446    /**
447     * Handles touch events by activating automatic scrolling, adjusting scroll
448     * velocity, or stopping.
449     * <p>
450     * If {@link #isExclusive()} is false, always returns false so that
451     * the host view may handle touch events. Otherwise, returns true when
452     * automatic scrolling is active and false otherwise.
453     */
454    @Override
455    public boolean onTouch(View v, MotionEvent event) {
456        if (!mEnabled) {
457            return false;
458        }
459
460        final int action = MotionEventCompat.getActionMasked(event);
461        switch (action) {
462            case MotionEvent.ACTION_DOWN:
463                mNeedsCancel = true;
464                mAlreadyDelayed = false;
465                // $FALL-THROUGH$
466            case MotionEvent.ACTION_MOVE:
467                final float xTargetVelocity = computeTargetVelocity(
468                        HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth());
469                final float yTargetVelocity = computeTargetVelocity(
470                        VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight());
471                mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity);
472
473                // If the auto scroller was not previously active, but it should
474                // be, then update the state and start animations.
475                if (!mAnimating && shouldAnimate()) {
476                    startAnimating();
477                }
478                break;
479            case MotionEvent.ACTION_UP:
480            case MotionEvent.ACTION_CANCEL:
481                requestStop();
482                break;
483        }
484
485        return mExclusive && mAnimating;
486    }
487
488    /**
489     * @return whether the target is able to scroll in the requested direction
490     */
491    private boolean shouldAnimate() {
492        final ClampedScroller scroller = mScroller;
493        final int verticalDirection = scroller.getVerticalDirection();
494        final int horizontalDirection = scroller.getHorizontalDirection();
495
496        return verticalDirection != 0 && canTargetScrollVertically(verticalDirection)
497                || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection);
498    }
499
500    /**
501     * Starts the scroll animation.
502     */
503    private void startAnimating() {
504        if (mRunnable == null) {
505            mRunnable = new ScrollAnimationRunnable();
506        }
507
508        mAnimating = true;
509        mNeedsReset = true;
510
511        if (!mAlreadyDelayed && mActivationDelay > 0) {
512            ViewCompat.postOnAnimationDelayed(mTarget, mRunnable, mActivationDelay);
513        } else {
514            mRunnable.run();
515        }
516
517        // If we start animating again before the user lifts their finger, we
518        // already know it's not a tap and don't need an activation delay.
519        mAlreadyDelayed = true;
520    }
521
522    /**
523     * Requests that the scroll animation slow to a stop. If there is an
524     * activation delay, this may occur between posting the animation and
525     * actually running it.
526     */
527    private void requestStop() {
528        if (mNeedsReset) {
529            // The animation has been posted, but hasn't run yet. Manually
530            // stopping animation will prevent it from running.
531            mAnimating = false;
532        } else {
533            mScroller.requestStop();
534        }
535    }
536
537    private float computeTargetVelocity(
538            int direction, float coordinate, float srcSize, float dstSize) {
539        final float relativeEdge = mRelativeEdges[direction];
540        final float maximumEdge = mMaximumEdges[direction];
541        final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate);
542        if (value == 0) {
543            // The edge in this direction is not activated.
544            return 0;
545        }
546
547        final float relativeVelocity = mRelativeVelocity[direction];
548        final float minimumVelocity = mMinimumVelocity[direction];
549        final float maximumVelocity = mMaximumVelocity[direction];
550        final float targetVelocity = relativeVelocity * dstSize;
551
552        // Target velocity is adjusted for interpolated edge position, then
553        // clamped to the minimum and maximum values. Later, this value will be
554        // adjusted for time-based acceleration.
555        if (value > 0) {
556            return constrain(value * targetVelocity, minimumVelocity, maximumVelocity);
557        } else {
558            return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity);
559        }
560    }
561
562    /**
563     * Override this method to scroll the target view by the specified number of
564     * pixels.
565     *
566     * @param deltaX The number of pixels to scroll by horizontally.
567     * @param deltaY The number of pixels to scroll by vertically.
568     */
569    public abstract void scrollTargetBy(int deltaX, int deltaY);
570
571    /**
572     * Override this method to return whether the target view can be scrolled
573     * horizontally in a certain direction.
574     *
575     * @param direction Negative to check scrolling left, positive to check
576     *            scrolling right.
577     * @return true if the target view is able to horizontally scroll in the
578     *         specified direction.
579     */
580    public abstract boolean canTargetScrollHorizontally(int direction);
581
582    /**
583     * Override this method to return whether the target view can be scrolled
584     * vertically in a certain direction.
585     *
586     * @param direction Negative to check scrolling up, positive to check
587     *            scrolling down.
588     * @return true if the target view is able to vertically scroll in the
589     *         specified direction.
590     */
591    public abstract boolean canTargetScrollVertically(int direction);
592
593    /**
594     * Returns the interpolated position of a touch point relative to an edge
595     * defined by its relative inset, its maximum absolute inset, and the edge
596     * interpolator.
597     *
598     * @param relativeValue The size of the inset relative to the total size.
599     * @param size Total size.
600     * @param maxValue The maximum size of the inset, used to clamp (relative *
601     *            total).
602     * @param current Touch position within within the total size.
603     * @return Interpolated value of the touch position within the edge.
604     */
605    private float getEdgeValue(float relativeValue, float size, float maxValue, float current) {
606        // For now, leading and trailing edges are always the same size.
607        final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue);
608        final float valueLeading = constrainEdgeValue(current, edgeSize);
609        final float valueTrailing = constrainEdgeValue(size - current, edgeSize);
610        final float value = (valueTrailing - valueLeading);
611        final float interpolated;
612        if (value < 0) {
613            interpolated = -mEdgeInterpolator.getInterpolation(-value);
614        } else if (value > 0) {
615            interpolated = mEdgeInterpolator.getInterpolation(value);
616        } else {
617            return 0;
618        }
619
620        return constrain(interpolated, -1, 1);
621    }
622
623    private float constrainEdgeValue(float current, float leading) {
624        if (leading == 0) {
625            return 0;
626        }
627
628        switch (mEdgeType) {
629            case EDGE_TYPE_INSIDE:
630            case EDGE_TYPE_INSIDE_EXTEND:
631                if (current < leading) {
632                    if (current >= 0) {
633                        // Movement up to the edge is scaled.
634                        return 1f - current / leading;
635                    } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) {
636                        // Movement beyond the edge is always maximum.
637                        return 1f;
638                    }
639                }
640                break;
641            case EDGE_TYPE_OUTSIDE:
642                if (current < 0) {
643                    // Movement beyond the edge is scaled.
644                    return current / -leading;
645                }
646                break;
647        }
648
649        return 0;
650    }
651
652    private static int constrain(int value, int min, int max) {
653        if (value > max) {
654            return max;
655        } else if (value < min) {
656            return min;
657        } else {
658            return value;
659        }
660    }
661
662    private static float constrain(float value, float min, float max) {
663        if (value > max) {
664            return max;
665        } else if (value < min) {
666            return min;
667        } else {
668            return value;
669        }
670    }
671
672    /**
673     * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view,
674     * canceling any ongoing touch events.
675     */
676    private void cancelTargetTouch() {
677        final long eventTime = SystemClock.uptimeMillis();
678        final MotionEvent cancel = MotionEvent.obtain(
679                eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0);
680        mTarget.onTouchEvent(cancel);
681        cancel.recycle();
682    }
683
684    private class ScrollAnimationRunnable implements Runnable {
685        @Override
686        public void run() {
687            if (!mAnimating) {
688                return;
689            }
690
691            if (mNeedsReset) {
692                mNeedsReset = false;
693                mScroller.start();
694            }
695
696            final ClampedScroller scroller = mScroller;
697            if (scroller.isFinished() || !shouldAnimate()) {
698                mAnimating = false;
699                return;
700            }
701
702            if (mNeedsCancel) {
703                mNeedsCancel = false;
704                cancelTargetTouch();
705            }
706
707            scroller.computeScrollDelta();
708
709            final int deltaX = scroller.getDeltaX();
710            final int deltaY = scroller.getDeltaY();
711            scrollTargetBy(deltaX,  deltaY);
712
713            // Keep going until the scroller has permanently stopped.
714            ViewCompat.postOnAnimation(mTarget, this);
715        }
716    }
717
718    /**
719     * Scroller whose velocity follows the curve of an {@link Interpolator} and
720     * is clamped to the interpolated 0f value before starting and the
721     * interpolated 1f value after a specified duration.
722     */
723    private static class ClampedScroller {
724        private int mRampUpDuration;
725        private int mRampDownDuration;
726        private float mTargetVelocityX;
727        private float mTargetVelocityY;
728
729        private long mStartTime;
730
731        private long mDeltaTime;
732        private int mDeltaX;
733        private int mDeltaY;
734
735        private long mStopTime;
736        private float mStopValue;
737        private int mEffectiveRampDown;
738
739        /**
740         * Creates a new ramp-up scroller that reaches full velocity after a
741         * specified duration.
742         */
743        public ClampedScroller() {
744            mStartTime = Long.MIN_VALUE;
745            mStopTime = -1;
746            mDeltaTime = 0;
747            mDeltaX = 0;
748            mDeltaY = 0;
749        }
750
751        public void setRampUpDuration(int durationMillis) {
752            mRampUpDuration = durationMillis;
753        }
754
755        public void setRampDownDuration(int durationMillis) {
756            mRampDownDuration = durationMillis;
757        }
758
759        /**
760         * Starts the scroller at the current animation time.
761         */
762        public void start() {
763            mStartTime = AnimationUtils.currentAnimationTimeMillis();
764            mStopTime = -1;
765            mDeltaTime = mStartTime;
766            mStopValue = 0.5f;
767            mDeltaX = 0;
768            mDeltaY = 0;
769        }
770
771        /**
772         * Stops the scroller at the current animation time.
773         */
774        public void requestStop() {
775            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
776            mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration);
777            mStopValue = getValueAt(currentTime);
778            mStopTime = currentTime;
779        }
780
781        public boolean isFinished() {
782            return mStopTime > 0
783                    && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
784        }
785
786        private float getValueAt(long currentTime) {
787            if (currentTime < mStartTime) {
788                return 0f;
789            } else if (mStopTime < 0 || currentTime < mStopTime) {
790                final long elapsedSinceStart = currentTime - mStartTime;
791                return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1);
792            } else {
793                final long elapsedSinceEnd = currentTime - mStopTime;
794                return (1 - mStopValue) + mStopValue
795                        * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1);
796            }
797        }
798
799        /**
800         * Interpolates the value along a parabolic curve corresponding to the equation
801         * <code>y = -4x * (x-1)</code>.
802         *
803         * @param value The value to interpolate, between 0 and 1.
804         * @return the interpolated value, between 0 and 1.
805         */
806        private float interpolateValue(float value) {
807            return -4 * value * value + 4 * value;
808        }
809
810        /**
811         * Computes the current scroll deltas. This usually only be called after
812         * starting the scroller with {@link #start()}.
813         *
814         * @see #getDeltaX()
815         * @see #getDeltaY()
816         */
817        public void computeScrollDelta() {
818            if (mDeltaTime == 0) {
819                throw new RuntimeException("Cannot compute scroll delta before calling start()");
820            }
821
822            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
823            final float value = getValueAt(currentTime);
824            final float scale = interpolateValue(value);
825            final long elapsedSinceDelta = currentTime - mDeltaTime;
826
827            mDeltaTime = currentTime;
828            mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX);
829            mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY);
830        }
831
832        /**
833         * Sets the target velocity for this scroller.
834         *
835         * @param x The target X velocity in pixels per millisecond.
836         * @param y The target Y velocity in pixels per millisecond.
837         */
838        public void setTargetVelocity(float x, float y) {
839            mTargetVelocityX = x;
840            mTargetVelocityY = y;
841        }
842
843        public int getHorizontalDirection() {
844            return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX));
845        }
846
847        public int getVerticalDirection() {
848            return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY));
849        }
850
851        /**
852         * The distance traveled in the X-coordinate computed by the last call
853         * to {@link #computeScrollDelta()}.
854         */
855        public int getDeltaX() {
856            return mDeltaX;
857        }
858
859        /**
860         * The distance traveled in the Y-coordinate computed by the last call
861         * to {@link #computeScrollDelta()}.
862         */
863        public int getDeltaY() {
864            return mDeltaY;
865        }
866    }
867}
868