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 
17 package com.android.internal.widget;
18 
19 import android.content.res.Resources;
20 import android.os.SystemClock;
21 import android.util.DisplayMetrics;
22 import android.view.MotionEvent;
23 import android.view.View;
24 import android.view.ViewConfiguration;
25 import android.view.animation.AccelerateInterpolator;
26 import android.view.animation.AnimationUtils;
27 import android.view.animation.Interpolator;
28 import 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  */
83 public 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      */
AutoScrollHelper(View target)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      */
setEnabled(boolean enabled)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      */
isEnabled()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      */
setExclusive(boolean exclusive)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      */
isExclusive()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      */
setMaximumVelocity(float horizontalMax, float verticalMax)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      */
setMinimumVelocity(float horizontalMin, float verticalMin)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      */
setRelativeVelocity(float horizontal, float vertical)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      */
setEdgeType(int type)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      */
setRelativeEdges(float horizontal, float vertical)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      */
setMaximumEdges(float horizontalMax, float verticalMax)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      */
setActivationDelay(int delayMillis)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      */
setRampUpDuration(int durationMillis)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      */
setRampDownDuration(int durationMillis)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
onTouch(View v, MotionEvent event)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      */
shouldAnimate()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      */
startAnimating()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      */
requestStop()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 
computeTargetVelocity( int direction, float coordinate, float srcSize, float dstSize)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      */
scrollTargetBy(int deltaX, int deltaY)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      */
canTargetScrollHorizontally(int direction)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      */
canTargetScrollVertically(int direction)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      */
getEdgeValue(float relativeValue, float size, float maxValue, float current)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 
constrainEdgeValue(float current, float leading)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 
constrain(int value, int min, int max)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 
constrain(float value, float min, float max)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      */
cancelTargetTouch()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
run()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          */
ClampedScroller()742         public ClampedScroller() {
743             mStartTime = Long.MIN_VALUE;
744             mStopTime = -1;
745             mDeltaTime = 0;
746             mDeltaX = 0;
747             mDeltaY = 0;
748         }
749 
setRampUpDuration(int durationMillis)750         public void setRampUpDuration(int durationMillis) {
751             mRampUpDuration = durationMillis;
752         }
753 
setRampDownDuration(int durationMillis)754         public void setRampDownDuration(int durationMillis) {
755             mRampDownDuration = durationMillis;
756         }
757 
758         /**
759          * Starts the scroller at the current animation time.
760          */
start()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          */
requestStop()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 
isFinished()780         public boolean isFinished() {
781             return mStopTime > 0
782                     && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
783         }
784 
getValueAt(long currentTime)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          */
interpolateValue(float value)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          */
computeScrollDelta()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          */
setTargetVelocity(float x, float y)837         public void setTargetVelocity(float x, float y) {
838             mTargetVelocityX = x;
839             mTargetVelocityY = y;
840         }
841 
getHorizontalDirection()842         public int getHorizontalDirection() {
843             return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX));
844         }
845 
getVerticalDirection()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          */
getDeltaX()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          */
getDeltaY()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 
AbsListViewAutoScroller(AbsListView target)874         public AbsListViewAutoScroller(AbsListView target) {
875             super(target);
876 
877             mTarget = target;
878         }
879 
880         @Override
scrollTargetBy(int deltaX, int deltaY)881         public void scrollTargetBy(int deltaX, int deltaY) {
882             mTarget.scrollListBy(deltaY);
883         }
884 
885         @Override
canTargetScrollHorizontally(int direction)886         public boolean canTargetScrollHorizontally(int direction) {
887             // List do not scroll horizontally.
888             return false;
889         }
890 
891         @Override
canTargetScrollVertically(int direction)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