• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.support.v4.widget;
18 
19 import android.content.res.Resources;
20 import android.os.SystemClock;
21 import android.support.v4.view.MotionEventCompat;
22 import android.support.v4.view.ViewCompat;
23 import android.util.DisplayMetrics;
24 import android.view.MotionEvent;
25 import android.view.View;
26 import android.view.ViewConfiguration;
27 import android.view.animation.AccelerateInterpolator;
28 import android.view.animation.AnimationUtils;
29 import 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  */
84 public 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      */
AutoScrollHelper(View target)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      */
setEnabled(boolean enabled)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      */
isEnabled()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      */
setExclusive(boolean exclusive)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      */
isExclusive()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      */
setMaximumVelocity(float horizontalMax, float verticalMax)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      */
setMinimumVelocity(float horizontalMin, float verticalMin)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      */
setRelativeVelocity(float horizontal, float vertical)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      */
setEdgeType(int type)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      */
setRelativeEdges(float horizontal, float vertical)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      */
setMaximumEdges(float horizontalMax, float verticalMax)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      */
setActivationDelay(int delayMillis)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      */
setRampUpDuration(int durationMillis)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      */
setRampDownDuration(int durationMillis)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
onTouch(View v, MotionEvent event)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      */
shouldAnimate()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      */
startAnimating()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      */
requestStop()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 
computeTargetVelocity( int direction, float coordinate, float srcSize, float dstSize)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      */
scrollTargetBy(int deltaX, int deltaY)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      */
canTargetScrollHorizontally(int direction)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      */
canTargetScrollVertically(int direction)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      */
getEdgeValue(float relativeValue, float size, float maxValue, float current)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 
constrainEdgeValue(float current, float leading)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 
constrain(int value, int min, int max)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 
constrain(float value, float min, float max)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      */
cancelTargetTouch()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
run()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          */
ClampedScroller()743         public ClampedScroller() {
744             mStartTime = Long.MIN_VALUE;
745             mStopTime = -1;
746             mDeltaTime = 0;
747             mDeltaX = 0;
748             mDeltaY = 0;
749         }
750 
setRampUpDuration(int durationMillis)751         public void setRampUpDuration(int durationMillis) {
752             mRampUpDuration = durationMillis;
753         }
754 
setRampDownDuration(int durationMillis)755         public void setRampDownDuration(int durationMillis) {
756             mRampDownDuration = durationMillis;
757         }
758 
759         /**
760          * Starts the scroller at the current animation time.
761          */
start()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          */
requestStop()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 
isFinished()781         public boolean isFinished() {
782             return mStopTime > 0
783                     && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
784         }
785 
getValueAt(long currentTime)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          */
interpolateValue(float value)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          */
computeScrollDelta()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          */
setTargetVelocity(float x, float y)838         public void setTargetVelocity(float x, float y) {
839             mTargetVelocityX = x;
840             mTargetVelocityY = y;
841         }
842 
getHorizontalDirection()843         public int getHorizontalDirection() {
844             return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX));
845         }
846 
getVerticalDirection()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          */
getDeltaX()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          */
getDeltaY()863         public int getDeltaY() {
864             return mDeltaY;
865         }
866     }
867 }
868