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 android.support.v4.widget;
     18 
     19 import android.content.res.Resources;
     20 import android.os.SystemClock;
     21 import android.support.v4.view.ViewCompat;
     22 import android.util.DisplayMetrics;
     23 import android.view.MotionEvent;
     24 import android.view.View;
     25 import android.view.ViewConfiguration;
     26 import android.view.animation.AccelerateInterpolator;
     27 import android.view.animation.AnimationUtils;
     28 import android.view.animation.Interpolator;
     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 ListViewAutoScrollHelper} for a {@link android.widget.ListView}
     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     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     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     boolean mNeedsReset;
    173 
    174     /** Whether to send a cancel motion event to the target view. */
    175     boolean mNeedsCancel;
    176 
    177     /** Whether the auto-scroller is actively scrolling. */
    178     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     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             ViewCompat.postOnAnimationDelayed(mTarget, 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     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     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     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         ScrollAnimationRunnable() {
    685         }
    686 
    687         @Override
    688         public void run() {
    689             if (!mAnimating) {
    690                 return;
    691             }
    692 
    693             if (mNeedsReset) {
    694                 mNeedsReset = false;
    695                 mScroller.start();
    696             }
    697 
    698             final ClampedScroller scroller = mScroller;
    699             if (scroller.isFinished() || !shouldAnimate()) {
    700                 mAnimating = false;
    701                 return;
    702             }
    703 
    704             if (mNeedsCancel) {
    705                 mNeedsCancel = false;
    706                 cancelTargetTouch();
    707             }
    708 
    709             scroller.computeScrollDelta();
    710 
    711             final int deltaX = scroller.getDeltaX();
    712             final int deltaY = scroller.getDeltaY();
    713             scrollTargetBy(deltaX,  deltaY);
    714 
    715             // Keep going until the scroller has permanently stopped.
    716             ViewCompat.postOnAnimation(mTarget, this);
    717         }
    718     }
    719 
    720     /**
    721      * Scroller whose velocity follows the curve of an {@link Interpolator} and
    722      * is clamped to the interpolated 0f value before starting and the
    723      * interpolated 1f value after a specified duration.
    724      */
    725     private static class ClampedScroller {
    726         private int mRampUpDuration;
    727         private int mRampDownDuration;
    728         private float mTargetVelocityX;
    729         private float mTargetVelocityY;
    730 
    731         private long mStartTime;
    732 
    733         private long mDeltaTime;
    734         private int mDeltaX;
    735         private int mDeltaY;
    736 
    737         private long mStopTime;
    738         private float mStopValue;
    739         private int mEffectiveRampDown;
    740 
    741         /**
    742          * Creates a new ramp-up scroller that reaches full velocity after a
    743          * specified duration.
    744          */
    745         ClampedScroller() {
    746             mStartTime = Long.MIN_VALUE;
    747             mStopTime = -1;
    748             mDeltaTime = 0;
    749             mDeltaX = 0;
    750             mDeltaY = 0;
    751         }
    752 
    753         public void setRampUpDuration(int durationMillis) {
    754             mRampUpDuration = durationMillis;
    755         }
    756 
    757         public void setRampDownDuration(int durationMillis) {
    758             mRampDownDuration = durationMillis;
    759         }
    760 
    761         /**
    762          * Starts the scroller at the current animation time.
    763          */
    764         public void start() {
    765             mStartTime = AnimationUtils.currentAnimationTimeMillis();
    766             mStopTime = -1;
    767             mDeltaTime = mStartTime;
    768             mStopValue = 0.5f;
    769             mDeltaX = 0;
    770             mDeltaY = 0;
    771         }
    772 
    773         /**
    774          * Stops the scroller at the current animation time.
    775          */
    776         public void requestStop() {
    777             final long currentTime = AnimationUtils.currentAnimationTimeMillis();
    778             mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration);
    779             mStopValue = getValueAt(currentTime);
    780             mStopTime = currentTime;
    781         }
    782 
    783         public boolean isFinished() {
    784             return mStopTime > 0
    785                     && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
    786         }
    787 
    788         private float getValueAt(long currentTime) {
    789             if (currentTime < mStartTime) {
    790                 return 0f;
    791             } else if (mStopTime < 0 || currentTime < mStopTime) {
    792                 final long elapsedSinceStart = currentTime - mStartTime;
    793                 return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1);
    794             } else {
    795                 final long elapsedSinceEnd = currentTime - mStopTime;
    796                 return (1 - mStopValue) + mStopValue
    797                         * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1);
    798             }
    799         }
    800 
    801         /**
    802          * Interpolates the value along a parabolic curve corresponding to the equation
    803          * <code>y = -4x * (x-1)</code>.
    804          *
    805          * @param value The value to interpolate, between 0 and 1.
    806          * @return the interpolated value, between 0 and 1.
    807          */
    808         private float interpolateValue(float value) {
    809             return -4 * value * value + 4 * value;
    810         }
    811 
    812         /**
    813          * Computes the current scroll deltas. This usually only be called after
    814          * starting the scroller with {@link #start()}.
    815          *
    816          * @see #getDeltaX()
    817          * @see #getDeltaY()
    818          */
    819         public void computeScrollDelta() {
    820             if (mDeltaTime == 0) {
    821                 throw new RuntimeException("Cannot compute scroll delta before calling start()");
    822             }
    823 
    824             final long currentTime = AnimationUtils.currentAnimationTimeMillis();
    825             final float value = getValueAt(currentTime);
    826             final float scale = interpolateValue(value);
    827             final long elapsedSinceDelta = currentTime - mDeltaTime;
    828 
    829             mDeltaTime = currentTime;
    830             mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX);
    831             mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY);
    832         }
    833 
    834         /**
    835          * Sets the target velocity for this scroller.
    836          *
    837          * @param x The target X velocity in pixels per millisecond.
    838          * @param y The target Y velocity in pixels per millisecond.
    839          */
    840         public void setTargetVelocity(float x, float y) {
    841             mTargetVelocityX = x;
    842             mTargetVelocityY = y;
    843         }
    844 
    845         public int getHorizontalDirection() {
    846             return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX));
    847         }
    848 
    849         public int getVerticalDirection() {
    850             return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY));
    851         }
    852 
    853         /**
    854          * The distance traveled in the X-coordinate computed by the last call
    855          * to {@link #computeScrollDelta()}.
    856          */
    857         public int getDeltaX() {
    858             return mDeltaX;
    859         }
    860 
    861         /**
    862          * The distance traveled in the Y-coordinate computed by the last call
    863          * to {@link #computeScrollDelta()}.
    864          */
    865         public int getDeltaY() {
    866             return mDeltaY;
    867         }
    868     }
    869 }
    870