content: /*
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.widget;
  
  import android.content.res.Resources;
  import android.os.SystemClock;
  import android.util.DisplayMetrics;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.ViewConfiguration;
  import android.view.animation.AccelerateInterpolator;
  import android.view.animation.AnimationUtils;
  import android.view.animation.Interpolator;
  import android.widget.AbsListView;
  
  /**
   * AutoScrollHelper is a utility class for adding automatic edge-triggered
   * scrolling to Views.
   * <p>
   * <b>Note:</b> Implementing classes are responsible for overriding the
   * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and
   * {@link #canTargetScrollVertically} methods. See
   * {@link AbsListViewAutoScroller} for an {@link android.widget.AbsListView}
   * -specific implementation.
   * <p>
   * <h1>Activation</h1> Automatic scrolling starts when the user touches within
   * an activation area. By default, activation areas are defined as the top,
   * left, right, and bottom 20% of the host view's total area. Touching within
   * the top activation area scrolls up, left scrolls to the left, and so on.
   * <p>
   * As the user touches closer to the extreme edge of the activation area,
   * scrolling accelerates up to a maximum velocity. When using the default edge
   * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds
   * will scroll at the maximum velocity.
   * <p>
   * The following activation properties may be configured:
   * <ul>
   * <li>Delay after entering activation area before auto-scrolling begins, see
   * {@link #setActivationDelay}. Default value is
   * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps.
   * <li>Location of activation areas, see {@link #setEdgeType}. Default value is
   * {@link #EDGE_TYPE_INSIDE_EXTEND}.
   * <li>Size of activation areas relative to view size, see
   * {@link #setRelativeEdges}. Default value is 20% for both vertical and
   * horizontal edges.
   * <li>Maximum size used to constrain relative size, see
   * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}.
   * </ul>
   * <h1>Scrolling</h1> When automatic scrolling is active, the helper will
   * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets.
   * <p>
   * The following scrolling properties may be configured:
   * <ul>
   * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default
   * value is 500 milliseconds.
   * <li>Acceleration ramp-down duration, see {@link #setRampDownDuration}.
   * Default value is 500 milliseconds.
   * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}.
   * Default value is 100% per second for both vertical and horizontal.
   * <li>Minimum velocity used to constrain relative velocity, see
   * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the
   * larger of either this value or the relative target value. Default value is
   * approximately 5 centimeters or 315 dips per second.
   * <li>Maximum velocity used to constrain relative velocity, see
   * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or
   * 1575 dips per second.
   * </ul>
   */
  public abstract class AutoScrollHelper implements View.OnTouchListener {
      /**
       * Constant passed to {@link #setRelativeEdges} or
       * {@link #setRelativeVelocity}. Using this value ensures that the computed
       * relative value is ignored and the absolute maximum value is always used.
       */
      public static final float RELATIVE_UNSPECIFIED = 0;
  
      /**
       * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity},
       * or {@link #setMinimumVelocity}. Using this value ensures that the
       * computed relative value is always used without constraining to a
       * particular minimum or maximum value.
       */
      public static final float NO_MAX = Float.MAX_VALUE;
  
      /**
       * Constant passed to {@link #setMaximumEdges}, or
       * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this
       * value ensures that the computed relative value is always used without
       * constraining to a particular minimum or maximum value.
       */
      public static final float NO_MIN = 0;
  
      /**
       * Edge type that specifies an activation area starting at the view bounds
       * and extending inward. Moving outside the view bounds will stop scrolling.
       *
       * @see #setEdgeType
       */
      public static final int EDGE_TYPE_INSIDE = 0;
  
      /**
       * Edge type that specifies an activation area starting at the view bounds
       * and extending inward. After activation begins, moving outside the view
       * bounds will continue scrolling.
       *
       * @see #setEdgeType
       */
      public static final int EDGE_TYPE_INSIDE_EXTEND = 1;
  
      /**
       * Edge type that specifies an activation area starting at the view bounds
       * and extending outward. Moving inside the view bounds will stop scrolling.
       *
       * @see #setEdgeType
       */
      public static final int EDGE_TYPE_OUTSIDE = 2;
  
      private static final int HORIZONTAL = 0;
      private static final int VERTICAL = 1;
  
      /** Scroller used to control acceleration toward maximum velocity. */
      private final ClampedScroller mScroller = new ClampedScroller();
  
      /** Interpolator used to scale velocity with touch position. */
      private final Interpolator mEdgeInterpolator = new AccelerateInterpolator();
  
      /** The view to auto-scroll. Might not be the source of touch events. */
      private final View mTarget;
  
      /** Runnable used to animate scrolling. */
      private Runnable mRunnable;
  
      /** Edge insets used to activate auto-scrolling. */
      private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
  
      /** Clamping values for edge insets used to activate auto-scrolling. */
      private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX };
  
      /** The type of edge being used. */
      private int mEdgeType;
  
      /** Delay after entering an activation edge before auto-scrolling begins. */
      private int mActivationDelay;
  
      /** Relative scrolling velocity at maximum edge distance. */
      private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED };
  
      /** Clamping values used for scrolling velocity. */
      private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN };
  
      /** Clamping values used for scrolling velocity. */
      private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX };
  
      /** Whether to start activation immediately. */
      private boolean mAlreadyDelayed;
  
      /** Whether to reset the scroller start time on the next animation. */
      private boolean mNeedsReset;
  
      /** Whether to send a cancel motion event to the target view. */
      private boolean mNeedsCancel;
  
      /** Whether the auto-scroller is actively scrolling. */
      private boolean mAnimating;
  
      /** Whether the auto-scroller is enabled. */
      private boolean mEnabled;
  
      /** Whether the auto-scroller consumes events when scrolling. */
      private boolean mExclusive;
  
      // Default values.
      private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND;
      private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315;
      private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575;
      private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX;
      private static final float DEFAULT_RELATIVE_EDGE = 0.2f;
      private static final float DEFAULT_RELATIVE_VELOCITY = 1f;
      private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout();
      private static final int DEFAULT_RAMP_UP_DURATION = 500;
      private static final int DEFAULT_RAMP_DOWN_DURATION = 500;
  
      /**
       * Creates a new helper for scrolling the specified target view.
       * <p>
       * The resulting helper may be configured by chaining setter calls and
       * should be set as a touch listener on the target view.
       * <p>
       * By default, the helper is disabled and will not respond to touch events
       * until it is enabled using {@link #setEnabled}.
       *
       * @param target The view to automatically scroll.
       */
      public AutoScrollHelper(View target) {
          mTarget = target;
  
          final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
          final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
          final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f);
          setMaximumVelocity(maxVelocity, maxVelocity);
          setMinimumVelocity(minVelocity, minVelocity);
  
          setEdgeType(DEFAULT_EDGE_TYPE);
          setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE);
          setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE);
          setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY);
          setActivationDelay(DEFAULT_ACTIVATION_DELAY);
          setRampUpDuration(DEFAULT_RAMP_UP_DURATION);
          setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION);
      }
  
      /**
       * Sets whether the scroll helper is enabled and should respond to touch
       * events.
       *
       * @param enabled Whether the scroll helper is enabled.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setEnabled(boolean enabled) {
          if (mEnabled && !enabled) {
              requestStop();
          }
  
          mEnabled = enabled;
          return this;
      }
  
      /**
       * @return True if this helper is enabled and responding to touch events.
       */
      public boolean isEnabled() {
          return mEnabled;
      }
  
      /**
       * Enables or disables exclusive handling of touch events during scrolling.
       * By default, exclusive handling is disabled and the target view receives
       * all touch events.
       * <p>
       * When enabled, {@link #onTouch} will return true if the helper is
       * currently scrolling and false otherwise.
       *
       * @param exclusive True to exclusively handle touch events during scrolling,
       *            false to allow the target view to receive all touch events.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setExclusive(boolean exclusive) {
          mExclusive = exclusive;
          return this;
      }
  
      /**
       * Indicates whether the scroll helper handles touch events exclusively
       * during scrolling.
       *
       * @return True if exclusive handling of touch events during scrolling is
       *         enabled, false otherwise.
       * @see #setExclusive(boolean)
       */
      public boolean isExclusive() {
          return mExclusive;
      }
  
      /**
       * Sets the absolute maximum scrolling velocity.
       * <p>
       * If relative velocity is not specified, scrolling will always reach the
       * same maximum velocity. If both relative and maximum velocities are
       * specified, the maximum velocity will be used to clamp the calculated
       * relative velocity.
       *
       * @param horizontalMax The maximum horizontal scrolling velocity, or
       *            {@link #NO_MAX} to leave the relative value unconstrained.
       * @param verticalMax The maximum vertical scrolling velocity, or
       *            {@link #NO_MAX} to leave the relative value unconstrained.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) {
          mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f;
          mMaximumVelocity[VERTICAL] = verticalMax / 1000f;
          return this;
      }
  
      /**
       * Sets the absolute minimum scrolling velocity.
       * <p>
       * If both relative and minimum velocities are specified, the minimum
       * velocity will be used to clamp the calculated relative velocity.
       *
       * @param horizontalMin The minimum horizontal scrolling velocity, or
       *            {@link #NO_MIN} to leave the relative value unconstrained.
       * @param verticalMin The minimum vertical scrolling velocity, or
       *            {@link #NO_MIN} to leave the relative value unconstrained.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) {
          mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f;
          mMinimumVelocity[VERTICAL] = verticalMin / 1000f;
          return this;
      }
  
      /**
       * Sets the target scrolling velocity relative to the host view's
       * dimensions.
       * <p>
       * If both relative and maximum velocities are specified, the maximum
       * velocity will be used to clamp the calculated relative velocity.
       *
       * @param horizontal The target horizontal velocity as a fraction of the
       *            host view width per second, or {@link #RELATIVE_UNSPECIFIED}
       *            to ignore.
       * @param vertical The target vertical velocity as a fraction of the host
       *            view height per second, or {@link #RELATIVE_UNSPECIFIED} to
       *            ignore.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) {
          mRelativeVelocity[HORIZONTAL] = horizontal / 1000f;
          mRelativeVelocity[VERTICAL] = vertical / 1000f;
          return this;
      }
  
      /**
       * Sets the activation edge type, one of:
       * <ul>
       * <li>{@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside
       * the bounds of the host view. If touch moves outside the bounds, scrolling
       * will stop.
       * <li>{@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to
       * scroll when touch moves outside the bounds of the host view.
       * <li>{@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches
       * that move outside the bounds of the host view.
       * </ul>
       *
       * @param type The type of edge to use.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setEdgeType(int type) {
          mEdgeType = type;
          return this;
      }
  
      /**
       * Sets the activation edge size relative to the host view's dimensions.
       * <p>
       * If both relative and maximum edges are specified, the maximum edge will
       * be used to constrain the calculated relative edge size.
       *
       * @param horizontal The horizontal edge size as a fraction of the host view
       *            width, or {@link #RELATIVE_UNSPECIFIED} to always use the
       *            maximum value.
       * @param vertical The vertical edge size as a fraction of the host view
       *            height, or {@link #RELATIVE_UNSPECIFIED} to always use the
       *            maximum value.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) {
          mRelativeEdges[HORIZONTAL] = horizontal;
          mRelativeEdges[VERTICAL] = vertical;
          return this;
      }
  
      /**
       * Sets the absolute maximum edge size.
       * <p>
       * If relative edge size is not specified, activation edges will always be
       * the maximum edge size. If both relative and maximum edges are specified,
       * the maximum edge will be used to constrain the calculated relative edge
       * size.
       *
       * @param horizontalMax The maximum horizontal edge size in pixels, or
       *            {@link #NO_MAX} to use the unconstrained calculated relative
       *            value.
       * @param verticalMax The maximum vertical edge size in pixels, or
       *            {@link #NO_MAX} to use the unconstrained calculated relative
       *            value.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) {
          mMaximumEdges[HORIZONTAL] = horizontalMax;
          mMaximumEdges[VERTICAL] = verticalMax;
          return this;
      }
  
      /**
       * Sets the delay after entering an activation edge before activation of
       * auto-scrolling. By default, the activation delay is set to
       * {@link ViewConfiguration#getTapTimeout()}.
       * <p>
       * Specifying a delay of zero will start auto-scrolling immediately after
       * the touch position enters an activation edge.
       *
       * @param delayMillis The activation delay in milliseconds.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setActivationDelay(int delayMillis) {
          mActivationDelay = delayMillis;
          return this;
      }
  
      /**
       * Sets the amount of time after activation of auto-scrolling that is takes
       * to reach target velocity for the current touch position.
       * <p>
       * Specifying a duration greater than zero prevents sudden jumps in
       * velocity.
       *
       * @param durationMillis The ramp-up duration in milliseconds.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setRampUpDuration(int durationMillis) {
          mScroller.setRampUpDuration(durationMillis);
          return this;
      }
  
      /**
       * Sets the amount of time after de-activation of auto-scrolling that is
       * takes to slow to a stop.
       * <p>
       * Specifying a duration greater than zero prevents sudden jumps in
       * velocity.
       *
       * @param durationMillis The ramp-down duration in milliseconds.
       * @return The scroll helper, which may used to chain setter calls.
       */
      public AutoScrollHelper setRampDownDuration(int durationMillis) {
          mScroller.setRampDownDuration(durationMillis);
          return this;
      }
  
      /**
       * Handles touch events by activating automatic scrolling, adjusting scroll
       * velocity, or stopping.
       * <p>
       * If {@link #isExclusive()} is false, always returns false so that
       * the host view may handle touch events. Otherwise, returns true when
       * automatic scrolling is active and false otherwise.
       */
      @Override
      public boolean onTouch(View v, MotionEvent event) {
          if (!mEnabled) {
              return false;
          }
  
          final int action = event.getActionMasked();
          switch (action) {
              case MotionEvent.ACTION_DOWN:
                  mNeedsCancel = true;
                  mAlreadyDelayed = false;
                  // $FALL-THROUGH$
              case MotionEvent.ACTION_MOVE:
                  final float xTargetVelocity = computeTargetVelocity(
                          HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth());
                  final float yTargetVelocity = computeTargetVelocity(
                          VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight());
                  mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity);
  
                  // If the auto scroller was not previously active, but it should
                  // be, then update the state and start animations.
                  if (!mAnimating && shouldAnimate()) {
                      startAnimating();
                  }
                  break;
              case MotionEvent.ACTION_UP:
              case MotionEvent.ACTION_CANCEL:
                  requestStop();
                  break;
          }
  
          return mExclusive && mAnimating;
      }
  
      /**
       * @return whether the target is able to scroll in the requested direction
       */
      private boolean shouldAnimate() {
          final ClampedScroller scroller = mScroller;
          final int verticalDirection = scroller.getVerticalDirection();
          final int horizontalDirection = scroller.getHorizontalDirection();
  
          return verticalDirection != 0 && canTargetScrollVertically(verticalDirection)
                  || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection);
      }
  
      /**
       * Starts the scroll animation.
       */
      private void startAnimating() {
          if (mRunnable == null) {
              mRunnable = new ScrollAnimationRunnable();
          }
  
          mAnimating = true;
          mNeedsReset = true;
  
          if (!mAlreadyDelayed && mActivationDelay > 0) {
              mTarget.postOnAnimationDelayed(mRunnable, mActivationDelay);
          } else {
              mRunnable.run();
          }
  
          // If we start animating again before the user lifts their finger, we
          // already know it's not a tap and don't need an activation delay.
          mAlreadyDelayed = true;
      }
  
      /**
       * Requests that the scroll animation slow to a stop. If there is an
       * activation delay, this may occur between posting the animation and
       * actually running it.
       */
      private void requestStop() {
          if (mNeedsReset) {
              // The animation has been posted, but hasn't run yet. Manually
              // stopping animation will prevent it from running.
              mAnimating = false;
          } else {
              mScroller.requestStop();
          }
      }
  
      private float computeTargetVelocity(
              int direction, float coordinate, float srcSize, float dstSize) {
          final float relativeEdge = mRelativeEdges[direction];
          final float maximumEdge = mMaximumEdges[direction];
          final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate);
          if (value == 0) {
              // The edge in this direction is not activated.
              return 0;
          }
  
          final float relativeVelocity = mRelativeVelocity[direction];
          final float minimumVelocity = mMinimumVelocity[direction];
          final float maximumVelocity = mMaximumVelocity[direction];
          final float targetVelocity = relativeVelocity * dstSize;
  
          // Target velocity is adjusted for interpolated edge position, then
          // clamped to the minimum and maximum values. Later, this value will be
          // adjusted for time-based acceleration.
          if (value > 0) {
              return constrain(value * targetVelocity, minimumVelocity, maximumVelocity);
          } else {
              return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity);
          }
      }
  
      /**
       * Override this method to scroll the target view by the specified number of
       * pixels.
       *
       * @param deltaX The number of pixels to scroll by horizontally.
       * @param deltaY The number of pixels to scroll by vertically.
       */
      public abstract void scrollTargetBy(int deltaX, int deltaY);
  
      /**
       * Override this method to return whether the target view can be scrolled
       * horizontally in a certain direction.
       *
       * @param direction Negative to check scrolling left, positive to check
       *            scrolling right.
       * @return true if the target view is able to horizontally scroll in the
       *         specified direction.
       */
      public abstract boolean canTargetScrollHorizontally(int direction);
  
      /**
       * Override this method to return whether the target view can be scrolled
       * vertically in a certain direction.
       *
       * @param direction Negative to check scrolling up, positive to check
       *            scrolling down.
       * @return true if the target view is able to vertically scroll in the
       *         specified direction.
       */
      public abstract boolean canTargetScrollVertically(int direction);
  
      /**
       * Returns the interpolated position of a touch point relative to an edge
       * defined by its relative inset, its maximum absolute inset, and the edge
       * interpolator.
       *
       * @param relativeValue The size of the inset relative to the total size.
       * @param size Total size.
       * @param maxValue The maximum size of the inset, used to clamp (relative *
       *            total).
       * @param current Touch position within within the total size.
       * @return Interpolated value of the touch position within the edge.
       */
      private float getEdgeValue(float relativeValue, float size, float maxValue, float current) {
          // For now, leading and trailing edges are always the same size.
          final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue);
          final float valueLeading = constrainEdgeValue(current, edgeSize);
          final float valueTrailing = constrainEdgeValue(size - current, edgeSize);
          final float value = (valueTrailing - valueLeading);
          final float interpolated;
          if (value < 0) {
              interpolated = -mEdgeInterpolator.getInterpolation(-value);
          } else if (value > 0) {
              interpolated = mEdgeInterpolator.getInterpolation(value);
          } else {
              return 0;
          }
  
          return constrain(interpolated, -1, 1);
      }
  
      private float constrainEdgeValue(float current, float leading) {
          if (leading == 0) {
              return 0;
          }
  
          switch (mEdgeType) {
              case EDGE_TYPE_INSIDE:
              case EDGE_TYPE_INSIDE_EXTEND:
                  if (current < leading) {
                      if (current >= 0) {
                          // Movement up to the edge is scaled.
                          return 1f - current / leading;
                      } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) {
                          // Movement beyond the edge is always maximum.
                          return 1f;
                      }
                  }
                  break;
              case EDGE_TYPE_OUTSIDE:
                  if (current < 0) {
                      // Movement beyond the edge is scaled.
                      return current / -leading;
                  }
                  break;
          }
  
          return 0;
      }
  
      private static int constrain(int value, int min, int max) {
          if (value > max) {
              return max;
          } else if (value < min) {
              return min;
          } else {
              return value;
          }
      }
  
      private static float constrain(float value, float min, float max) {
          if (value > max) {
              return max;
          } else if (value < min) {
              return min;
          } else {
              return value;
          }
      }
  
      /**
       * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view,
       * canceling any ongoing touch events.
       */
      private void cancelTargetTouch() {
          final long eventTime = SystemClock.uptimeMillis();
          final MotionEvent cancel = MotionEvent.obtain(
                  eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0);
          mTarget.onTouchEvent(cancel);
          cancel.recycle();
      }
  
      private class ScrollAnimationRunnable implements Runnable {
          @Override
          public void run() {
              if (!mAnimating) {
                  return;
              }
  
              if (mNeedsReset) {
                  mNeedsReset = false;
                  mScroller.start();
              }
  
              final ClampedScroller scroller = mScroller;
              if (scroller.isFinished() || !shouldAnimate()) {
                  mAnimating = false;
                  return;
              }
  
              if (mNeedsCancel) {
                  mNeedsCancel = false;
                  cancelTargetTouch();
              }
  
              scroller.computeScrollDelta();
  
              final int deltaX = scroller.getDeltaX();
              final int deltaY = scroller.getDeltaY();
              scrollTargetBy(deltaX,  deltaY);
  
              // Keep going until the scroller has permanently stopped.
              mTarget.postOnAnimation(this);
          }
      }
  
      /**
       * Scroller whose velocity follows the curve of an {@link Interpolator} and
       * is clamped to the interpolated 0f value before starting and the
       * interpolated 1f value after a specified duration.
       */
      private static class ClampedScroller {
          private int mRampUpDuration;
          private int mRampDownDuration;
          private float mTargetVelocityX;
          private float mTargetVelocityY;
  
          private long mStartTime;
  
          private long mDeltaTime;
          private int mDeltaX;
          private int mDeltaY;
  
          private long mStopTime;
          private float mStopValue;
          private int mEffectiveRampDown;
  
          /**
           * Creates a new ramp-up scroller that reaches full velocity after a
           * specified duration.
           */
          public ClampedScroller() {
              mStartTime = Long.MIN_VALUE;
              mStopTime = -1;
              mDeltaTime = 0;
              mDeltaX = 0;
              mDeltaY = 0;
          }
  
          public void setRampUpDuration(int durationMillis) {
              mRampUpDuration = durationMillis;
          }
  
          public void setRampDownDuration(int durationMillis) {
              mRampDownDuration = durationMillis;
          }
  
          /**
           * Starts the scroller at the current animation time.
           */
          public void start() {
              mStartTime = AnimationUtils.currentAnimationTimeMillis();
              mStopTime = -1;
              mDeltaTime = mStartTime;
              mStopValue = 0.5f;
              mDeltaX = 0;
              mDeltaY = 0;
          }
  
          /**
           * Stops the scroller at the current animation time.
           */
          public void requestStop() {
              final long currentTime = AnimationUtils.currentAnimationTimeMillis();
              mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration);
              mStopValue = getValueAt(currentTime);
              mStopTime = currentTime;
          }
  
          public boolean isFinished() {
              return mStopTime > 0
                      && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
          }
  
          private float getValueAt(long currentTime) {
              if (currentTime < mStartTime) {
                  return 0f;
              } else if (mStopTime < 0 || currentTime < mStopTime) {
                  final long elapsedSinceStart = currentTime - mStartTime;
                  return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1);
              } else {
                  final long elapsedSinceEnd = currentTime - mStopTime;
                  return (1 - mStopValue) + mStopValue
                          * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1);
              }
          }
  
          /**
           * Interpolates the value along a parabolic curve corresponding to the equation
           * <code>y = -4x * (x-1)</code>.
           *
           * @param value The value to interpolate, between 0 and 1.
           * @return the interpolated value, between 0 and 1.
           */
          private float interpolateValue(float value) {
              return -4 * value * value + 4 * value;
          }
  
          /**
           * Computes the current scroll deltas. This usually only be called after
           * starting the scroller with {@link #start()}.
           *
           * @see #getDeltaX()
           * @see #getDeltaY()
           */
          public void computeScrollDelta() {
              if (mDeltaTime == 0) {
                  throw new RuntimeException("Cannot compute scroll delta before calling start()");
              }
  
              final long currentTime = AnimationUtils.currentAnimationTimeMillis();
              final float value = getValueAt(currentTime);
              final float scale = interpolateValue(value);
              final long elapsedSinceDelta = currentTime - mDeltaTime;
  
              mDeltaTime = currentTime;
              mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX);
              mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY);
          }
  
          /**
           * Sets the target velocity for this scroller.
           *
           * @param x The target X velocity in pixels per millisecond.
           * @param y The target Y velocity in pixels per millisecond.
           */
          public void setTargetVelocity(float x, float y) {
              mTargetVelocityX = x;
              mTargetVelocityY = y;
          }
  
          public int getHorizontalDirection() {
              return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX));
          }
  
          public int getVerticalDirection() {
              return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY));
          }
  
          /**
           * The distance traveled in the X-coordinate computed by the last call
           * to {@link #computeScrollDelta()}.
           */
          public int getDeltaX() {
              return mDeltaX;
          }
  
          /**
           * The distance traveled in the Y-coordinate computed by the last call
           * to {@link #computeScrollDelta()}.
           */
          public int getDeltaY() {
              return mDeltaY;
          }
      }
  
      /**
       * An implementation of {@link AutoScrollHelper} that knows how to scroll
       * through an {@link AbsListView}.
       */
      public static class AbsListViewAutoScroller extends AutoScrollHelper {
          private final AbsListView mTarget;
  
          public AbsListViewAutoScroller(AbsListView target) {
              super(target);
  
              mTarget = target;
          }
  
          @Override
          public void scrollTargetBy(int deltaX, int deltaY) {
              mTarget.scrollListBy(deltaY);
          }
  
          @Override
          public boolean canTargetScrollHorizontally(int direction) {
              // List do not scroll horizontally.
              return false;
          }
  
          @Override
          public boolean canTargetScrollVertically(int direction) {
              final AbsListView target = mTarget;
              final int itemCount = target.getCount();
              if (itemCount == 0) {
                  return false;
              }
  
              final int childCount = target.getChildCount();
              final int firstPosition = target.getFirstVisiblePosition();
              final int lastPosition = firstPosition + childCount;
  
              if (direction > 0) {
                  // Are we already showing the entire last item?
                  if (lastPosition >= itemCount) {
                      final View lastView = target.getChildAt(childCount - 1);
                      if (lastView.getBottom() <= target.getHeight()) {
                          return false;
                      }
                  }
              } else if (direction < 0) {
                  // Are we already showing the entire first item?
                  if (firstPosition <= 0) {
                      final View firstView = target.getChildAt(0);
                      if (firstView.getTop() >= 0) {
                          return false;
                      }
                  }
              } else {
                  // The behavior for direction 0 is undefined and we can return
                  // whatever we want.
                  return false;
              }
  
              return true;
          }
      }
  }
  