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