Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bc7e4b2c authored by Alan Viverette's avatar Alan Viverette Committed by Android (Google) Code Review
Browse files

Merge "Update internal AutoScrollHelper to match support lib version" into klp-dev

parents 580e60fd cb168795
Loading
Loading
Loading
Loading
+294 −138
Original line number Diff line number Diff line
@@ -32,7 +32,8 @@ import android.widget.AbsListView;
 * scrolling to Views.
 * <p>
 * <b>Note:</b> Implementing classes are responsible for overriding the
 * {@link #onScrollBy} method to scroll the target view. See
 * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and
 * {@link #canTargetScrollVertically} methods. See
 * {@link AbsListViewAutoScroller} for an {@link android.widget.AbsListView}
 * -specific implementation.
 * <p>
@@ -60,12 +61,14 @@ import android.widget.AbsListView;
 * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}.
 * </ul>
 * <h1>Scrolling</h1> When automatic scrolling is active, the helper will
 * repeatedly call {@link #onScrollBy} to apply new scrolling offsets.
 * 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 2.5 seconds.
 * value is 2500 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
@@ -163,25 +166,22 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
    private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX };

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

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

    /** Whether the auto-scroller is active. */
    private boolean mActive;
    /** Whether to send a cancel motion event to the target view. */
    private boolean mNeedsCancel;

    /** Whether the auto-scroller is scrolling. */
    private boolean mScrolling;
    /** 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 mExclusiveEnabled;

    /** Down time of the most recent down touch event. */
    private long mDownTime;
    private boolean mExclusive;

    // Default values.
    private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND;
@@ -192,7 +192,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
    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 = 2500;
    // TODO: RAMP_DOWN_DURATION of 500ms?
    private static final int DEFAULT_RAMP_DOWN_DURATION = 500;

    /**
     * Creates a new helper for scrolling the specified target view.
@@ -220,8 +220,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
        setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY);
        setActivationDelay(DEFAULT_ACTIVATION_DELAY);
        setRampUpDuration(DEFAULT_RAMP_UP_DURATION);

        mEnabled = true;
        setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION);
    }

    /**
@@ -232,8 +231,8 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setEnabled(boolean enabled) {
        if (!enabled) {
            stop(true);
        if (mEnabled && !enabled) {
            requestStop();
        }

        mEnabled = enabled;
@@ -255,13 +254,13 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
     * When enabled, {@link #onTouch} will return true if the helper is
     * currently scrolling and false otherwise.
     *
     * @param enabled True to exclusively handle touch events during scrolling,
     * @param exclusive True to exclusively handle touch events during scrolling,
     *            false to allow the target view to receive all touch events.
     * @see #isExclusiveEnabled()
     * @see #onTouch(View, MotionEvent)
     * @return The scroll helper, which may used to chain setter calls.
     */
    public void setExclusiveEnabled(boolean enabled) {
        mExclusiveEnabled = enabled;
    public AutoScrollHelper setExclusive(boolean exclusive) {
        mExclusive = exclusive;
        return this;
    }

    /**
@@ -270,10 +269,10 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
     *
     * @return True if exclusive handling of touch events during scrolling is
     *         enabled, false otherwise.
     * @see #setExclusiveEnabled(boolean)
     * @see #setExclusive(boolean)
     */
    public boolean isExclusiveEnabled() {
        return mExclusiveEnabled;
    public boolean isExclusive() {
        return mExclusive;
    }

    /**
@@ -424,7 +423,22 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
     * @return The scroll helper, which may used to chain setter calls.
     */
    public AutoScrollHelper setRampUpDuration(int durationMillis) {
        mScroller.setDuration(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;
    }

@@ -432,7 +446,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
     * Handles touch events by activating automatic scrolling, adjusting scroll
     * velocity, or stopping.
     * <p>
     * If {@link #isExclusiveEnabled()} is false, always returns false so that
     * 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.
     */
@@ -445,52 +459,135 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
        final int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownTime = event.getDownTime();
                mNeedsCancel = true;
                mAlreadyDelayed = false;
                // $FALL-THROUGH$
            case MotionEvent.ACTION_MOVE:
                final float xValue = getEdgeValue(mRelativeEdges[HORIZONTAL], v.getWidth(),
                        mMaximumEdges[HORIZONTAL], event.getX());
                final float yValue = getEdgeValue(mRelativeEdges[VERTICAL], v.getHeight(),
                        mMaximumEdges[VERTICAL], event.getY());
                final float maxVelX = constrain(mRelativeVelocity[HORIZONTAL] * mTarget.getWidth(),
                        mMinimumVelocity[HORIZONTAL], mMaximumVelocity[HORIZONTAL]);
                final float maxVelY = constrain(mRelativeVelocity[VERTICAL] * mTarget.getHeight(),
                        mMinimumVelocity[VERTICAL], mMaximumVelocity[VERTICAL]);
                mScroller.setTargetVelocity(xValue * maxVelX, yValue * maxVelY);

                if ((xValue != 0 || yValue != 0) && !mActive) {
                    mActive = true;
                    mResetScroller = true;
                    if (mRunnable == null) {
                        mRunnable = new AutoScrollRunnable();
                    }
                    if (mSkipDelay) {
                        mTarget.postOnAnimation(mRunnable);
                    } else {
                        mSkipDelay = true;
                        mTarget.postOnAnimationDelayed(mRunnable, mActivationDelay);
                    }
                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:
                stop(true);
                requestStop();
                break;
        }

        return mExclusiveEnabled && mScrolling;
        return mExclusive && mAnimating;
    }

    /**
     * Override this method to scroll the target view by the specified number
     * of pixels.
     * <p>
     * Returns whether the target view was able to scroll the requested amount.
     * @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 deltaX The amount to scroll in the X direction, in pixels.
     * @param deltaY The amount to scroll in the Y direction, in pixels.
     * @return true if the target view was able to scroll the requested amount.
     * @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 onScrollBy(int deltaX, int deltaY);
    public abstract boolean canTargetScrollVertically(int direction);

    /**
     * Returns the interpolated position of a touch point relative to an edge
@@ -534,7 +631,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
                    if (current >= 0) {
                        // Movement up to the edge is scaled.
                        return 1f - current / leading;
                    } else if (mActive && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) {
                    } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) {
                        // Movement beyond the edge is always maximum.
                        return 1f;
                    }
@@ -551,7 +648,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
        return 0;
    }

    private static float constrain(float value, float min, float max) {
    private static int constrain(int value, int min, int max) {
        if (value > max) {
            return max;
        } else if (value < min) {
@@ -561,19 +658,13 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
        }
    }

    /**
     * Stops auto-scrolling immediately, optionally reseting the auto-scrolling
     * delay.
     *
     * @param reset Whether to reset the auto-scrolling delay.
     */
    private void stop(boolean reset) {
        mActive = false;
        mScrolling = false;
        mSkipDelay = !reset;

        if (mRunnable != null) {
            mTarget.removeCallbacks(mRunnable);
    private static float constrain(float value, float min, float max) {
        if (value > max) {
            return max;
        } else if (value < min) {
            return min;
        } else {
            return value;
        }
    }

@@ -582,52 +673,44 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
     * canceling any ongoing touch events.
     */
    private void cancelTargetTouch() {
        final long eventTime = SystemClock.uptimeMillis();
        final MotionEvent cancel = MotionEvent.obtain(
                mDownTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_CANCEL, 0, 0, 0);
        cancel.setAction(MotionEvent.ACTION_CANCEL);
                eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0);
        mTarget.onTouchEvent(cancel);
        cancel.recycle();
    }

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

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

            final View target = mTarget;
            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();
            if ((deltaX != 0 || deltaY != 0 || !scroller.isFinished())
                    && onScrollBy(deltaX, deltaY)) {
                // Update whether we're actively scrolling.
                final boolean scrolling = (deltaX != 0 || deltaY != 0);
                if (mScrolling != scrolling) {
                    mScrolling = scrolling;

                    // If we just started actively scrolling, make sure any down
                    // or move events send to the target view are canceled.
                    if (mExclusiveEnabled && scrolling) {
                        cancelTargetTouch();
                    }
                }
            scrollTargetBy(deltaX,  deltaY);

                // Keep going until the scroller has permanently stopped or the
                // view can't scroll any more. If the user moves their finger
                // again, we'll repost the animation.
                target.postOnAnimation(this);
            } else {
                stop(false);
            }
            // Keep going until the scroller has permanently stopped.
            mTarget.postOnAnimation(this);
        }
    }

@@ -637,27 +720,39 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
     * interpolated 1f value after a specified duration.
     */
    private static class ClampedScroller {
        private final Interpolator mInterpolator = new AccelerateInterpolator();

        private int mDuration;
        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() {
            reset();
            mStartTime = Long.MIN_VALUE;
            mStopTime = -1;
            mDeltaTime = 0;
            mDeltaX = 0;
            mDeltaY = 0;
        }

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

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

        /**
@@ -665,32 +760,50 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
         */
        public void start() {
            mStartTime = AnimationUtils.currentAnimationTimeMillis();
            mStopTime = -1;
            mDeltaTime = mStartTime;
            mStopValue = 0.5f;
            mDeltaX = 0;
            mDeltaY = 0;
        }

        /**
         * Returns whether the scroller is finished, which means that its
         * acceleration is zero.
         *
         * @return Whether the scroller is finished.
         * 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() {
            if (mTargetVelocityX == 0 && mTargetVelocityY == 0) {
                return true;
            return mStopTime > 0
                    && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown;
        }
            final long currentTime = AnimationUtils.currentAnimationTimeMillis();

        private float getValueAt(long currentTime) {
            if (currentTime < mStartTime) {
                return 0f;
            } else if (mStopTime < 0 || currentTime < mStopTime) {
                final long elapsedSinceStart = currentTime - mStartTime;
            return elapsedSinceStart > mDuration;
                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);
            }
        }

        /**
         * Stops the scroller and resets its values.
         * 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.
         */
        public void reset() {
            mStartTime = -1;
            mDeltaTime = -1;
            mDeltaX = 0;
            mDeltaY = 0;
        private float interpolateValue(float value) {
            return -4 * value * value + 4 * value;
        }

        /**
@@ -701,18 +814,13 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
         * @see #getDeltaY()
         */
        public void computeScrollDelta() {
            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
            final long elapsedSinceStart = currentTime - mStartTime;
            final float value;
            if (mStartTime < 0) {
                value = 0f;
            } else if (elapsedSinceStart < mDuration) {
                value = (float) elapsedSinceStart / mDuration;
            } else {
                value = 1f;
            if (mDeltaTime == 0) {
                throw new RuntimeException("Cannot compute scroll delta before calling start()");
            }

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

            mDeltaTime = currentTime;
@@ -731,6 +839,14 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
            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()}.
@@ -749,20 +865,60 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
    }

    /**
     * Implementation of {@link AutoScrollHelper} that knows how to scroll
     * generic {@link AbsListView}s.
     * 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 boolean onScrollBy(int deltaX, int deltaY) {
            return mTarget.scrollListBy(deltaY);
        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();
            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;
        }
    }
}