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