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

Commit e73d13ea authored by Alan Viverette's avatar Alan Viverette Committed by Android Git Automerger
Browse files

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

* commit 'bc7e4b2c':
  Update internal AutoScrollHelper to match support lib version
parents 8fa18599 bc7e4b2c
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;
        }
    }
}