public abstract class

AutoScrollHelper

extends java.lang.Object

 java.lang.Object

↳androidx.core.widget.AutoScrollHelper

Subclasses:

ListViewAutoScrollHelper

Gradle dependencies

compile group: 'androidx.core', name: 'core', version: '1.15.0-alpha02'

  • groupId: androidx.core
  • artifactId: core
  • version: 1.15.0-alpha02

Artifact androidx.core:core:1.15.0-alpha02 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.core:core com.android.support:support-compat

Androidx class mapping:

androidx.core.widget.AutoScrollHelper android.support.v4.widget.AutoScrollHelper

Overview

AutoScrollHelper is a utility class for adding automatic edge-triggered scrolling to Views.

Note: Implementing classes are responsible for overriding the AutoScrollHelper.scrollTargetBy(int, int), AutoScrollHelper.canTargetScrollHorizontally(int), and AutoScrollHelper.canTargetScrollVertically(int) methods. See ListViewAutoScrollHelper for a -specific implementation.

Activation

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.

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, AutoScrollHelper.EDGE_TYPE_INSIDE_EXTEND, moving outside of the view bounds will scroll at the maximum velocity.

The following activation properties may be configured:

Scrolling

When automatic scrolling is active, the helper will repeatedly call AutoScrollHelper.scrollTargetBy(int, int) to apply new scrolling offsets.

The following scrolling properties may be configured:

Summary

Fields
public static final intEDGE_TYPE_INSIDE

Edge type that specifies an activation area starting at the view bounds and extending inward.

public static final intEDGE_TYPE_INSIDE_EXTEND

Edge type that specifies an activation area starting at the view bounds and extending inward.

public static final intEDGE_TYPE_OUTSIDE

Edge type that specifies an activation area starting at the view bounds and extending outward.

public static final floatNO_MAX

Constant passed to AutoScrollHelper.setMaximumEdges(float, float), AutoScrollHelper.setMaximumVelocity(float, float), or AutoScrollHelper.setMinimumVelocity(float, float).

public static final floatNO_MIN

Constant passed to AutoScrollHelper.setMaximumEdges(float, float), or AutoScrollHelper.setMaximumVelocity(float, float), or AutoScrollHelper.setMinimumVelocity(float, float).

public static final floatRELATIVE_UNSPECIFIED

Constant passed to AutoScrollHelper.setRelativeEdges(float, float) or AutoScrollHelper.setRelativeVelocity(float, float).

Constructors
publicAutoScrollHelper(View target)

Creates a new helper for scrolling the specified target view.

Methods
public abstract booleancanTargetScrollHorizontally(int direction)

Override this method to return whether the target view can be scrolled horizontally in a certain direction.

public abstract booleancanTargetScrollVertically(int direction)

Override this method to return whether the target view can be scrolled vertically in a certain direction.

public booleanisEnabled()

public booleanisExclusive()

Indicates whether the scroll helper handles touch events exclusively during scrolling.

public booleanonTouch(View v, MotionEvent event)

Handles touch events by activating automatic scrolling, adjusting scroll velocity, or stopping.

public abstract voidscrollTargetBy(int deltaX, int deltaY)

Override this method to scroll the target view by the specified number of pixels.

public AutoScrollHelpersetActivationDelay(int delayMillis)

Sets the delay after entering an activation edge before activation of auto-scrolling.

public AutoScrollHelpersetEdgeType(int type)

Sets the activation edge type, one of:

public AutoScrollHelpersetEnabled(boolean enabled)

Sets whether the scroll helper is enabled and should respond to touch events.

public AutoScrollHelpersetExclusive(boolean exclusive)

Enables or disables exclusive handling of touch events during scrolling.

public AutoScrollHelpersetMaximumEdges(float horizontalMax, float verticalMax)

Sets the absolute maximum edge size.

public AutoScrollHelpersetMaximumVelocity(float horizontalMax, float verticalMax)

Sets the absolute maximum scrolling velocity.

public AutoScrollHelpersetMinimumVelocity(float horizontalMin, float verticalMin)

Sets the absolute minimum scrolling velocity.

public AutoScrollHelpersetRampDownDuration(int durationMillis)

Sets the amount of time after de-activation of auto-scrolling that is takes to slow to a stop.

public AutoScrollHelpersetRampUpDuration(int durationMillis)

Sets the amount of time after activation of auto-scrolling that is takes to reach target velocity for the current touch position.

public AutoScrollHelpersetRelativeEdges(float horizontal, float vertical)

Sets the activation edge size relative to the host view's dimensions.

public AutoScrollHelpersetRelativeVelocity(float horizontal, float vertical)

Sets the target scrolling velocity relative to the host view's dimensions.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final float RELATIVE_UNSPECIFIED

Constant passed to AutoScrollHelper.setRelativeEdges(float, float) or AutoScrollHelper.setRelativeVelocity(float, float). Using this value ensures that the computed relative value is ignored and the absolute maximum value is always used.

public static final float NO_MAX

Constant passed to AutoScrollHelper.setMaximumEdges(float, float), AutoScrollHelper.setMaximumVelocity(float, float), or AutoScrollHelper.setMinimumVelocity(float, float). 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

Constant passed to AutoScrollHelper.setMaximumEdges(float, float), or AutoScrollHelper.setMaximumVelocity(float, float), or AutoScrollHelper.setMinimumVelocity(float, float). Using this value ensures that the computed relative value is always used without constraining to a particular minimum or maximum value.

public static final int EDGE_TYPE_INSIDE

Edge type that specifies an activation area starting at the view bounds and extending inward. Moving outside the view bounds will stop scrolling.

See also: AutoScrollHelper.setEdgeType(int)

public static final int EDGE_TYPE_INSIDE_EXTEND

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 also: AutoScrollHelper.setEdgeType(int)

public static final int EDGE_TYPE_OUTSIDE

Edge type that specifies an activation area starting at the view bounds and extending outward. Moving inside the view bounds will stop scrolling.

See also: AutoScrollHelper.setEdgeType(int)

Constructors

public AutoScrollHelper(View target)

Creates a new helper for scrolling the specified target view.

The resulting helper may be configured by chaining setter calls and should be set as a touch listener on the target view.

By default, the helper is disabled and will not respond to touch events until it is enabled using AutoScrollHelper.setEnabled(boolean).

Parameters:

target: The view to automatically scroll.

Methods

public AutoScrollHelper setEnabled(boolean enabled)

Sets whether the scroll helper is enabled and should respond to touch events.

Parameters:

enabled: Whether the scroll helper is enabled.

Returns:

The scroll helper, which may used to chain setter calls.

public boolean isEnabled()

Returns:

True if this helper is enabled and responding to touch events.

public AutoScrollHelper setExclusive(boolean exclusive)

Enables or disables exclusive handling of touch events during scrolling. By default, exclusive handling is disabled and the target view receives all touch events.

When enabled, AutoScrollHelper.onTouch(View, MotionEvent) will return true if the helper is currently scrolling and false otherwise.

Parameters:

exclusive: True to exclusively handle touch events during scrolling, false to allow the target view to receive all touch events.

Returns:

The scroll helper, which may used to chain setter calls.

public boolean isExclusive()

Indicates whether the scroll helper handles touch events exclusively during scrolling.

Returns:

True if exclusive handling of touch events during scrolling is enabled, false otherwise.

See also: AutoScrollHelper.setExclusive(boolean)

public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax)

Sets the absolute maximum scrolling velocity.

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.

Parameters:

horizontalMax: The maximum horizontal scrolling velocity, or AutoScrollHelper.NO_MAX to leave the relative value unconstrained.
verticalMax: The maximum vertical scrolling velocity, or AutoScrollHelper.NO_MAX to leave the relative value unconstrained.

Returns:

The scroll helper, which may used to chain setter calls.

public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin)

Sets the absolute minimum scrolling velocity.

If both relative and minimum velocities are specified, the minimum velocity will be used to clamp the calculated relative velocity.

Parameters:

horizontalMin: The minimum horizontal scrolling velocity, or AutoScrollHelper.NO_MIN to leave the relative value unconstrained.
verticalMin: The minimum vertical scrolling velocity, or AutoScrollHelper.NO_MIN to leave the relative value unconstrained.

Returns:

The scroll helper, which may used to chain setter calls.

public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical)

Sets the target scrolling velocity relative to the host view's dimensions.

If both relative and maximum velocities are specified, the maximum velocity will be used to clamp the calculated relative velocity.

Parameters:

horizontal: The target horizontal velocity as a fraction of the host view width per second, or AutoScrollHelper.RELATIVE_UNSPECIFIED to ignore.
vertical: The target vertical velocity as a fraction of the host view height per second, or AutoScrollHelper.RELATIVE_UNSPECIFIED to ignore.

Returns:

The scroll helper, which may used to chain setter calls.

public AutoScrollHelper setEdgeType(int type)

Sets the activation edge type, one of:

Parameters:

type: The type of edge to use.

Returns:

The scroll helper, which may used to chain setter calls.

public AutoScrollHelper setRelativeEdges(float horizontal, float vertical)

Sets the activation edge size relative to the host view's dimensions.

If both relative and maximum edges are specified, the maximum edge will be used to constrain the calculated relative edge size.

Parameters:

horizontal: The horizontal edge size as a fraction of the host view width, or AutoScrollHelper.RELATIVE_UNSPECIFIED to always use the maximum value.
vertical: The vertical edge size as a fraction of the host view height, or AutoScrollHelper.RELATIVE_UNSPECIFIED to always use the maximum value.

Returns:

The scroll helper, which may used to chain setter calls.

public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax)

Sets the absolute maximum edge size.

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.

Parameters:

horizontalMax: The maximum horizontal edge size in pixels, or AutoScrollHelper.NO_MAX to use the unconstrained calculated relative value.
verticalMax: The maximum vertical edge size in pixels, or AutoScrollHelper.NO_MAX to use the unconstrained calculated relative value.

Returns:

The scroll helper, which may used to chain setter calls.

public AutoScrollHelper setActivationDelay(int delayMillis)

Sets the delay after entering an activation edge before activation of auto-scrolling. By default, the activation delay is set to .

Specifying a delay of zero will start auto-scrolling immediately after the touch position enters an activation edge.

Parameters:

delayMillis: The activation delay in milliseconds.

Returns:

The scroll helper, which may used to chain setter calls.

public AutoScrollHelper setRampUpDuration(int durationMillis)

Sets the amount of time after activation of auto-scrolling that is takes to reach target velocity for the current touch position.

Specifying a duration greater than zero prevents sudden jumps in velocity.

Parameters:

durationMillis: The ramp-up duration in milliseconds.

Returns:

The scroll helper, which may used to chain setter calls.

public AutoScrollHelper setRampDownDuration(int durationMillis)

Sets the amount of time after de-activation of auto-scrolling that is takes to slow to a stop.

Specifying a duration greater than zero prevents sudden jumps in velocity.

Parameters:

durationMillis: The ramp-down duration in milliseconds.

Returns:

The scroll helper, which may used to chain setter calls.

public boolean onTouch(View v, MotionEvent event)

Handles touch events by activating automatic scrolling, adjusting scroll velocity, or stopping.

If AutoScrollHelper.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.

public abstract void scrollTargetBy(int deltaX, int deltaY)

Override this method to scroll the target view by the specified number of pixels.

Parameters:

deltaX: The number of pixels to scroll by horizontally.
deltaY: The number of pixels to scroll by vertically.

public abstract boolean canTargetScrollHorizontally(int direction)

Override this method to return whether the target view can be scrolled horizontally in a certain direction.

Parameters:

direction: Negative to check scrolling left, positive to check scrolling right.

Returns:

true if the target view is able to horizontally scroll in the specified direction.

public abstract boolean canTargetScrollVertically(int direction)

Override this method to return whether the target view can be scrolled vertically in a certain direction.

Parameters:

direction: Negative to check scrolling up, positive to check scrolling down.

Returns:

true if the target view is able to vertically scroll in the specified direction.

Source

/*
 * 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 androidx.core.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 androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;

/**
 * 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 ListViewAutoScrollHelper} for a {@link android.widget.ListView}
 * -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. */
    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. */
    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. */
    boolean mNeedsReset;

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

    /** Whether the auto-scroller is actively scrolling. */
    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(@NonNull 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.
     */
    @NonNull
    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.
     */
    @NonNull
    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.
     */
    @NonNull
    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.
     */
    @NonNull
    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.
     */
    @NonNull
    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.
     */
    @NonNull
    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.
     */
    @NonNull
    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.
     */
    @NonNull
    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.
     */
    @NonNull
    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
     */
    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) {
            ViewCompat.postOnAnimationDelayed(mTarget, 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;
    }

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

    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.
     */
    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 {
        ScrollAnimationRunnable() {
        }

        @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.
            ViewCompat.postOnAnimation(mTarget, 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.
         */
        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;
        }
    }
}