summaryrefslogtreecommitdiffstats
path: root/core/java/com/android/internal/widget/AutoScrollHelper.java
blob: 0d468ca24941b1f882fafd5b05d8055c081bbda1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.widget;

import android.content.res.Resources;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.AbsListView;

/**
 * AutoScrollHelper is a utility class for adding automatic edge-triggered
 * scrolling to Views.
 * <p>
 * <b>Note:</b> Implementing classes are responsible for overriding the
 * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and
 * {@link #canTargetScrollVertically} methods. See
 * {@link AbsListViewAutoScroller} for an {@link android.widget.AbsListView}
 * -specific implementation.
 * <p>
 * <h1>Activation</h1> Automatic scrolling starts when the user touches within
 * an activation area. By default, activation areas are defined as the top,
 * left, right, and bottom 20% of the host view's total area. Touching within
 * the top activation area scrolls up, left scrolls to the left, and so on.
 * <p>
 * As the user touches closer to the extreme edge of the activation area,
 * scrolling accelerates up to a maximum velocity. When using the default edge
 * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds
 * will scroll at the maximum velocity.
 * <p>
 * The following activation properties may be configured:
 * <ul>
 * <li>Delay after entering activation area before auto-scrolling begins, see
 * {@link #setActivationDelay}. Default value is
 * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps.
 * <li>Location of activation areas, see {@link #setEdgeType}. Default value is
 * {@link #EDGE_TYPE_INSIDE_EXTEND}.
 * <li>Size of activation areas relative to view size, see
 * {@link #setRelativeEdges}. Default value is 20% for both vertical and
 * horizontal edges.
 * <li>Maximum size used to constrain relative size, see
 * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}.
 * </ul>
 * <h1>Scrolling</h1> When automatic scrolling is active, the helper will
 * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets.
 * <p>
 * The following scrolling properties may be configured:
 * <ul>
 * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default
 * value is 500 milliseconds.
 * <li>Acceleration ramp-down duration, see {@link #setRampDownDuration}.
 * Default value is 500 milliseconds.
 * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}.
 * Default value is 100% per second for both vertical and horizontal.
 * <li>Minimum velocity used to constrain relative velocity, see
 * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the
 * larger of either this value or the relative target value. Default value is
 * approximately 5 centimeters or 315 dips per second.
 * <li>Maximum velocity used to constrain relative velocity, see
 * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or
 * 1575 dips per second.
 * </ul>
 */
public abstract class AutoScrollHelper implements View.OnTouchListener {
    /**
     * Constant passed to {@link #setRelativeEdges} or
     * {@link #setRelativeVelocity}. Using this value ensures that the computed
     * relative value is ignored and the absolute maximum value is always used.
     */
    public static final float RELATIVE_UNSPECIFIED = 0;

    /**
     * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity},
     * or {@link #setMinimumVelocity}. Using this value ensures that the
     * computed relative value is always used without constraining to a
     * particular minimum or maximum value.
     */
    public static final float NO_MAX = Float.MAX_VALUE;

    /**
     * Constant passed to {@link #setMaximumEdges}, or
     * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this
     * value ensures that the computed relative value is always used without
     * constraining to a particular minimum or maximum value.
     */
    public static final float NO_MIN = 0;

    /**
     * Edge type that specifies an activation area starting at the view bounds
     * and extending inward. Moving outside the view bounds will stop scrolling.
     *
     * @see #setEdgeType
     */
    public static final int EDGE_TYPE_INSIDE = 0;

    /**
     * Edge type that specifies an activation area starting at the view bounds
     * and extending inward. After activation begins, moving outside the view
     * bounds will continue scrolling.
     *
     * @see #setEdgeType
     */
    public static final int EDGE_TYPE_INSIDE_EXTEND = 1;

    /**
     * Edge type that specifies an activation area starting at the view bounds
     * and extending outward. Moving inside the view bounds will stop scrolling.
     *
     * @see #setEdgeType
     */
    public static final int EDGE_TYPE_OUTSIDE = 2;

    private static final int HORIZONTAL = 0;
    private static final int VERTICAL = 1;

    /** Scroller used to control acceleration toward maximum velocity. */
    private final ClampedScroller mScroller = new ClampedScroller();

    /** Interpolator used to scale velocity with touch position. */
    private final Interpolator mEdgeInterpolator = new AccelerateInterpolator();

    /** The view to auto-scroll. Might not be the source of touch events. */
    private final View mTarget;

    /** Runnable used to animate scrolling. */
    private Runnable mRunnable;

    /** Edge insets used to activate auto-scrolling. */
    private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };

    /** Clamping values for edge insets used to activate auto-scrolling. */
    private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX };

    /** The type of edge being used. */
    private int mEdgeType;

    /** Delay after entering an activation edge before auto-scrolling begins. */
    private int mActivationDelay;

    /** Relative scrolling velocity at maximum edge distance. */
    private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };

    /** Clamping values used for scrolling velocity. */
    private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN };

    /** Clamping values used for scrolling velocity. */
    private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX };

    /** Whether to start activation immediately. */
    private boolean mAlreadyDelayed;

    /** Whether to reset the scroller start time on the next animation. */
    private boolean mNeedsReset;

    /** Whether to send a cancel motion event to the target view. */
    private boolean mNeedsCancel;

    /** Whether the auto-scroller is actively scrolling. */
    private boolean mAnimating;

    /** Whether the auto-scroller is enabled. */
    private boolean mEnabled;

    /** Whether the auto-scroller consumes events when scrolling. */
    private boolean mExclusive;

    // Default values.
    private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND;
    private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315;
    private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575;
    private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX;
    private static final float DEFAULT_RELATIVE_EDGE = 0.2f;
    private static final float DEFAULT_RELATIVE_VELOCITY = 1f;
    private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout();
    private static final int DEFAULT_RAMP_UP_DURATION = 500;
    private static final int DEFAULT_RAMP_DOWN_DURATION = 500;

    /**
     * Creates a new helper for scrolling the specified target view.
     * <p>
     * The resulting helper may be configured by chaining setter calls and
     * should be set as a touch listener on the target view.
     * <p>
     * By default, the helper is disabled and will not respond to touch events
     * until it is enabled using {@link #setEnabled}.
     *
     * @param target The view to automatically scroll.
     */
    public AutoScrollHelper(View target) {
        mTarget = target;

        final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
        final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
        final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
        setMaximumVelocity(maxVelocity, maxVelocity);
        setMinimumVelocity(minVelocity, minVelocity);

        setEdgeType(DEFAULT_EDGE_TYPE);
        setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE);
        setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE);
        setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY);
        setActivationDelay(DEFAULT_ACTIVATION_DELAY);
        setRampUpDuration(DEFAULT_RAMP_UP_DURATION);
        setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION);
    }

    /**
     * Sets whether the scroll helper is enabled and should respond to touch
     * events.
     *
     * @param enabled Whether the scroll helper is enabled.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setEnabled(boolean enabled) {
        if (mEnabled && !enabled) {
            requestStop();
        }

        mEnabled = enabled;
        return this;
    }

    /**
     * @return True if this helper is enabled and responding to touch events.
     */
    public boolean isEnabled() {
        return mEnabled;
    }

    /**
     * Enables or disables exclusive handling of touch events during scrolling.
     * By default, exclusive handling is disabled and the target view receives
     * all touch events.
     * <p>
     * When enabled, {@link #onTouch} will return true if the helper is
     * currently scrolling and false otherwise.
     *
     * @param exclusive True to exclusively handle touch events during scrolling,
     *            false to allow the target view to receive all touch events.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setExclusive(boolean exclusive) {
        mExclusive = exclusive;
        return this;
    }

    /**
     * Indicates whether the scroll helper handles touch events exclusively
     * during scrolling.
     *
     * @return True if exclusive handling of touch events during scrolling is
     *         enabled, false otherwise.
     * @see #setExclusive(boolean)
     */
    public boolean isExclusive() {
        return mExclusive;
    }

    /**
     * Sets the absolute maximum scrolling velocity.
     * <p>
     * If relative velocity is not specified, scrolling will always reach the
     * same maximum velocity. If both relative and maximum velocities are
     * specified, the maximum velocity will be used to clamp the calculated
     * relative velocity.
     *
     * @param horizontalMax The maximum horizontal scrolling velocity, or
     *            {@link #NO_MAX} to leave the relative value unconstrained.
     * @param verticalMax The maximum vertical scrolling velocity, or
     *            {@link #NO_MAX} to leave the relative value unconstrained.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) {
        mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f;
        mMaximumVelocity[VERTICAL] = verticalMax / 1000f;
        return this;
    }

    /**
     * Sets the absolute minimum scrolling velocity.
     * <p>
     * If both relative and minimum velocities are specified, the minimum
     * velocity will be used to clamp the calculated relative velocity.
     *
     * @param horizontalMin The minimum horizontal scrolling velocity, or
     *            {@link #NO_MIN} to leave the relative value unconstrained.
     * @param verticalMin The minimum vertical scrolling velocity, or
     *            {@link #NO_MIN} to leave the relative value unconstrained.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) {
        mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f;
        mMinimumVelocity[VERTICAL] = verticalMin / 1000f;
        return this;
    }

    /**
     * Sets the target scrolling velocity relative to the host view's
     * dimensions.
     * <p>
     * If both relative and maximum velocities are specified, the maximum
     * velocity will be used to clamp the calculated relative velocity.
     *
     * @param horizontal The target horizontal velocity as a fraction of the
     *            host view width per second, or {@link #RELATIVE_UNSPECIFIED}
     *            to ignore.
     * @param vertical The target vertical velocity as a fraction of the host
     *            view height per second, or {@link #RELATIVE_UNSPECIFIED} to
     *            ignore.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) {
        mRelativeVelocity[HORIZONTAL] = horizontal / 1000f;
        mRelativeVelocity[VERTICAL] = vertical / 1000f;
        return this;
    }

    /**
     * Sets the activation edge type, one of:
     * <ul>
     * <li>{@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside
     * the bounds of the host view. If touch moves outside the bounds, scrolling
     * will stop.
     * <li>{@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to
     * scroll when touch moves outside the bounds of the host view.
     * <li>{@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches
     * that move outside the bounds of the host view.
     * </ul>
     *
     * @param type The type of edge to use.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setEdgeType(int type) {
        mEdgeType = type;
        return this;
    }

    /**
     * Sets the activation edge size relative to the host view's dimensions.
     * <p>
     * If both relative and maximum edges are specified, the maximum edge will
     * be used to constrain the calculated relative edge size.
     *
     * @param horizontal The horizontal edge size as a fraction of the host view
     *            width, or {@link #RELATIVE_UNSPECIFIED} to always use the
     *            maximum value.
     * @param vertical The vertical edge size as a fraction of the host view
     *            height, or {@link #RELATIVE_UNSPECIFIED} to always use the
     *            maximum value.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) {
        mRelativeEdges[HORIZONTAL] = horizontal;
        mRelativeEdges[VERTICAL] = vertical;
        return this;
    }

    /**
     * Sets the absolute maximum edge size.
     * <p>
     * If relative edge size is not specified, activation edges will always be
     * the maximum edge size. If both relative and maximum edges are specified,
     * the maximum edge will be used to constrain the calculated relative edge
     * size.
     *
     * @param horizontalMax The maximum horizontal edge size in pixels, or
     *            {@link #NO_MAX} to use the unconstrained calculated relative
     *            value.
     * @param verticalMax The maximum vertical edge size in pixels, or
     *            {@link #NO_MAX} to use the unconstrained calculated relative
     *            value.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) {
        mMaximumEdges[HORIZONTAL] = horizontalMax;
        mMaximumEdges[VERTICAL] = verticalMax;
        return this;
    }

    /**
     * Sets the delay after entering an activation edge before activation of
     * auto-scrolling. By default, the activation delay is set to
     * {@link ViewConfiguration#getTapTimeout()}.
     * <p>
     * Specifying a delay of zero will start auto-scrolling immediately after
     * the touch position enters an activation edge.
     *
     * @param delayMillis The activation delay in milliseconds.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setActivationDelay(int delayMillis) {
        mActivationDelay = delayMillis;
        return this;
    }

    /**
     * Sets the amount of time after activation of auto-scrolling that is takes
     * to reach target velocity for the current touch position.
     * <p>
     * Specifying a duration greater than zero prevents sudden jumps in
     * velocity.
     *
     * @param durationMillis The ramp-up duration in milliseconds.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setRampUpDuration(int durationMillis) {
        mScroller.setRampUpDuration(durationMillis);
        return this;
    }

    /**
     * Sets the amount of time after de-activation of auto-scrolling that is
     * takes to slow to a stop.
     * <p>
     * Specifying a duration greater than zero prevents sudden jumps in
     * velocity.
     *
     * @param durationMillis The ramp-down duration in milliseconds.
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setRampDownDuration(int durationMillis) {
        mScroller.setRampDownDuration(durationMillis);
        return this;
    }

    /**
     * Handles touch events by activating automatic scrolling, adjusting scroll
     * velocity, or stopping.
     * <p>
     * If {@link #isExclusive()} is false, always returns false so that
     * the host view may handle touch events. Otherwise, returns true when
     * automatic scrolling is active and false otherwise.
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (!mEnabled) {
            return false;
        }

        final int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mNeedsCancel = true;
                mAlreadyDelayed = false;
                // $FALL-THROUGH$
            case MotionEvent.ACTION_MOVE:
                final float xTargetVelocity = computeTargetVelocity(
                        HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth());
                final float yTargetVelocity = computeTargetVelocity(
                        VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight());
                mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity);

                // If the auto scroller was not previously active, but it should
                // be, then update the state and start animations.
                if (!mAnimating && shouldAnimate()) {
                    startAnimating();
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                requestStop();
                break;
        }

        return mExclusive && mAnimating;
    }

    /**
     * @return whether the target is able to scroll in the requested direction
     */
    private boolean shouldAnimate() {
        final ClampedScroller scroller = mScroller;
        final int verticalDirection = scroller.getVerticalDirection();
        final int horizontalDirection = scroller.getHorizontalDirection();

        return verticalDirection != 0 && canTargetScrollVertically(verticalDirection)
                || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection);
    }

    /**
     * Starts the scroll animation.
     */
    private void startAnimating() {
        if (mRunnable == null) {
            mRunnable = new ScrollAnimationRunnable();
        }

        mAnimating = true;
        mNeedsReset = true;

        if (!mAlreadyDelayed && mActivationDelay > 0) {
            mTarget.postOnAnimationDelayed(mRunnable, mActivationDelay);
        } else {
            mRunnable.run();
        }

        // If we start animating again before the user lifts their finger, we
        // already know it's not a tap and don't need an activation delay.
        mAlreadyDelayed = true;
    }

    /**
     * Requests that the scroll animation slow to a stop. If there is an
     * activation delay, this may occur between posting the animation and
     * actually running it.
     */
    private void requestStop() {
        if (mNeedsReset) {
            // The animation has been posted, but hasn't run yet. Manually
            // stopping animation will prevent it from running.
            mAnimating = false;
        } else {
            mScroller.requestStop();
        }
    }

    private float computeTargetVelocity(
            int direction, float coordinate, float srcSize, float dstSize) {
        final float relativeEdge = mRelativeEdges[direction];
        final float maximumEdge = mMaximumEdges[direction];
        final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate);
        if (value == 0) {
            // The edge in this direction is not activated.
            return 0;
        }

        final float relativeVelocity = mRelativeVelocity[direction];
        final float minimumVelocity = mMinimumVelocity[direction];
        final float maximumVelocity = mMaximumVelocity[direction];
        final float targetVelocity = relativeVelocity * dstSize;

        // Target velocity is adjusted for interpolated edge position, then
        // clamped to the minimum and maximum values. Later, this value will be
        // adjusted for time-based acceleration.
        if (value > 0) {
            return constrain(value * targetVelocity, minimumVelocity, maximumVelocity);
        } else {
            return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity);
        }
    }

    /**
     * Override this method to scroll the target view by the specified number of
     * pixels.
     *
     * @param deltaX The number of pixels to scroll by horizontally.
     * @param deltaY The number of pixels to scroll by vertically.
     */
    public abstract void scrollTargetBy(int deltaX, int deltaY);

    /**
     * Override this method to return whether the target view can be scrolled
     * horizontally in a certain direction.
     *
     * @param direction Negative to check scrolling left, positive to check
     *            scrolling right.
     * @return true if the target view is able to horizontally scroll in the
     *         specified direction.
     */
    public abstract boolean canTargetScrollHorizontally(int direction);

    /**
     * Override this method to return whether the target view can be scrolled
     * vertically in a certain direction.
     *
     * @param direction Negative to check scrolling up, positive to check
     *            scrolling down.
     * @return true if the target view is able to vertically scroll in the
     *         specified direction.
     */
    public abstract boolean canTargetScrollVertically(int direction);

    /**
     * Returns the interpolated position of a touch point relative to an edge
     * defined by its relative inset, its maximum absolute inset, and the edge
     * interpolator.
     *
     * @param relativeValue The size of the inset relative to the total size.
     * @param size Total size.
     * @param maxValue The maximum size of the inset, used to clamp (relative *
     *            total).
     * @param current Touch position within within the total size.
     * @return Interpolated value of the touch position within the edge.
     */
    private float getEdgeValue(float relativeValue, float size, float maxValue, float current) {
        // For now, leading and trailing edges are always the same size.
        final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue);
        final float valueLeading = constrainEdgeValue(current, edgeSize);
        final float valueTrailing = constrainEdgeValue(size - current, edgeSize);
        final float value = (valueTrailing - valueLeading);
        final float interpolated;
        if (value < 0) {
            interpolated = -mEdgeInterpolator.getInterpolation(-value);
        } else if (value > 0) {
            interpolated = mEdgeInterpolator.getInterpolation(value);
        } else {
            return 0;
        }

        return constrain(interpolated, -1, 1);
    }

    private float constrainEdgeValue(float current, float leading) {
        if (leading == 0) {
            return 0;
        }

        switch (mEdgeType) {
            case EDGE_TYPE_INSIDE:
            case EDGE_TYPE_INSIDE_EXTEND:
                if (current < leading) {
                    if (current >= 0) {
                        // Movement up to the edge is scaled.
                        return 1f - current / leading;
                    } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) {
                        // Movement beyond the edge is always maximum.
                        return 1f;
                    }
                }
                break;
            case EDGE_TYPE_OUTSIDE:
                if (current < 0) {
                    // Movement beyond the edge is scaled.
                    return current / -leading;
                }
                break;
        }

        return 0;
    }

    private static int constrain(int value, int min, int max) {
        if (value > max) {
            return max;
        } else if (value < min) {
            return min;
        } else {
            return value;
        }
    }

    private static float constrain(float value, float min, float max) {
        if (value > max) {
            return max;
        } else if (value < min) {
            return min;
        } else {
            return value;
        }
    }

    /**
     * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view,
     * canceling any ongoing touch events.
     */
    private void cancelTargetTouch() {
        final long eventTime = SystemClock.uptimeMillis();
        final MotionEvent cancel = MotionEvent.obtain(
                eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0);
        mTarget.onTouchEvent(cancel);
        cancel.recycle();
    }

    private class ScrollAnimationRunnable implements Runnable {
        @Override
        public void run() {
            if (!mAnimating) {
                return;
            }

            if (mNeedsReset) {
                mNeedsReset = false;
                mScroller.start();
            }

            final ClampedScroller scroller = mScroller;
            if (scroller.isFinished() || !shouldAnimate()) {
                mAnimating = false;
                return;
            }

            if (mNeedsCancel) {
                mNeedsCancel = false;
                cancelTargetTouch();
            }

            scroller.computeScrollDelta();

            final int deltaX = scroller.getDeltaX();
            final int deltaY = scroller.getDeltaY();
            scrollTargetBy(deltaX,  deltaY);

            // Keep going until the scroller has permanently stopped.
            mTarget.postOnAnimation(this);
        }
    }

    /**
     * Scroller whose velocity follows the curve of an {@link Interpolator} and
     * is clamped to the interpolated 0f value before starting and the
     * interpolated 1f value after a specified duration.
     */
    private static class ClampedScroller {
        private int mRampUpDuration;
        private int mRampDownDuration;
        private float mTargetVelocityX;
        private float mTargetVelocityY;

        private long mStartTime;

        private long mDeltaTime;
        private int mDeltaX;
        private int mDeltaY;

        private long mStopTime;
        private float mStopValue;
        private int mEffectiveRampDown;

        /**
         * Creates a new ramp-up scroller that reaches full velocity after a
         * specified duration.
         */
        public ClampedScroller() {
            mStartTime = Long.MIN_VALUE;
            mStopTime = -1;
            mDeltaTime = 0;
            mDeltaX = 0;
            mDeltaY = 0;
        }

        public void setRampUpDuration(int durationMillis) {
            mRampUpDuration = durationMillis;
        }

        public void setRampDownDuration(int durationMillis) {
            mRampDownDuration = durationMillis;
        }

        /**
         * Starts the scroller at the current animation time.
         */
        public void start() {
            mStartTime = AnimationUtils.currentAnimationTimeMillis();
            mStopTime = -1;
            mDeltaTime = mStartTime;
            mStopValue = 0.5f;
            mDeltaX = 0;
            mDeltaY = 0;
        }

        /**
         * Stops the scroller at the current animation time.
         */
        public void requestStop() {
            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
            mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration);
            mStopValue = getValueAt(currentTime);
            mStopTime = currentTime;
        }

        public boolean isFinished() {
            return mStopTime > 0
                    && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
        }

        private float getValueAt(long currentTime) {
            if (currentTime < mStartTime) {
                return 0f;
            } else if (mStopTime < 0 || currentTime < mStopTime) {
                final long elapsedSinceStart = currentTime - mStartTime;
                return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1);
            } else {
                final long elapsedSinceEnd = currentTime - mStopTime;
                return (1 - mStopValue) + mStopValue
                        * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1);
            }
        }

        /**
         * Interpolates the value along a parabolic curve corresponding to the equation
         * <code>y = -4x * (x-1)</code>.
         *
         * @param value The value to interpolate, between 0 and 1.
         * @return the interpolated value, between 0 and 1.
         */
        private float interpolateValue(float value) {
            return -4 * value * value + 4 * value;
        }

        /**
         * Computes the current scroll deltas. This usually only be called after
         * starting the scroller with {@link #start()}.
         *
         * @see #getDeltaX()
         * @see #getDeltaY()
         */
        public void computeScrollDelta() {
            if (mDeltaTime == 0) {
                throw new RuntimeException("Cannot compute scroll delta before calling start()");
            }

            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
            final float value = getValueAt(currentTime);
            final float scale = interpolateValue(value);
            final long elapsedSinceDelta = currentTime - mDeltaTime;

            mDeltaTime = currentTime;
            mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX);
            mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY);
        }

        /**
         * Sets the target velocity for this scroller.
         *
         * @param x The target X velocity in pixels per millisecond.
         * @param y The target Y velocity in pixels per millisecond.
         */
        public void setTargetVelocity(float x, float y) {
            mTargetVelocityX = x;
            mTargetVelocityY = y;
        }

        public int getHorizontalDirection() {
            return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX));
        }

        public int getVerticalDirection() {
            return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY));
        }

        /**
         * The distance traveled in the X-coordinate computed by the last call
         * to {@link #computeScrollDelta()}.
         */
        public int getDeltaX() {
            return mDeltaX;
        }

        /**
         * The distance traveled in the Y-coordinate computed by the last call
         * to {@link #computeScrollDelta()}.
         */
        public int getDeltaY() {
            return mDeltaY;
        }
    }

    /**
     * An implementation of {@link AutoScrollHelper} that knows how to scroll
     * through an {@link AbsListView}.
     */
    public static class AbsListViewAutoScroller extends AutoScrollHelper {
        private final AbsListView mTarget;

        public AbsListViewAutoScroller(AbsListView target) {
            super(target);

            mTarget = target;
        }

        @Override
        public void scrollTargetBy(int deltaX, int deltaY) {
            mTarget.scrollListBy(deltaY);
        }

        @Override
        public boolean canTargetScrollHorizontally(int direction) {
            // List do not scroll horizontally.
            return false;
        }

        @Override
        public boolean canTargetScrollVertically(int direction) {
            final AbsListView target = mTarget;
            final int itemCount = target.getCount();
            if (itemCount == 0) {
                return false;
            }

            final int childCount = target.getChildCount();
            final int firstPosition = target.getFirstVisiblePosition();
            final int lastPosition = firstPosition + childCount;

            if (direction > 0) {
                // Are we already showing the entire last item?
                if (lastPosition >= itemCount) {
                    final View lastView = target.getChildAt(childCount - 1);
                    if (lastView.getBottom() <= target.getHeight()) {
                        return false;
                    }
                }
            } else if (direction < 0) {
                // Are we already showing the entire first item?
                if (firstPosition <= 0) {
                    final View firstView = target.getChildAt(0);
                    if (firstView.getTop() >= 0) {
                        return false;
                    }
                }
            } else {
                // The behavior for direction 0 is undefined and we can return
                // whatever we want.
                return false;
            }

            return true;
        }
    }
}