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