diff options
author | Alan Viverette <alanv@google.com> | 2013-08-30 14:33:13 -0700 |
---|---|---|
committer | Alan Viverette <alanv@google.com> | 2013-08-30 14:33:13 -0700 |
commit | cb168795fecd57ea5bf28de3c4973c4bc196ea7b (patch) | |
tree | 470b4dcde87697dd0b79fc6ef4e9da3851009d30 /core/java | |
parent | 116c12ef4c5fc2d5e5e90a417c4386c1f9a3ddf6 (diff) | |
download | frameworks_base-cb168795fecd57ea5bf28de3c4973c4bc196ea7b.zip frameworks_base-cb168795fecd57ea5bf28de3c4973c4bc196ea7b.tar.gz frameworks_base-cb168795fecd57ea5bf28de3c4973c4bc196ea7b.tar.bz2 |
Update internal AutoScrollHelper to match support lib version
BUG: 10547956
Change-Id: I7d5b1b9fb89ae8ff4294cc2dfd01e29b55ed3eba
Diffstat (limited to 'core/java')
-rw-r--r-- | core/java/com/android/internal/widget/AutoScrollHelper.java | 432 |
1 files changed, 294 insertions, 138 deletions
diff --git a/core/java/com/android/internal/widget/AutoScrollHelper.java b/core/java/com/android/internal/widget/AutoScrollHelper.java index f728e6a..c72cb1f 100644 --- a/core/java/com/android/internal/widget/AutoScrollHelper.java +++ b/core/java/com/android/internal/widget/AutoScrollHelper.java @@ -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; + } + + 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); } - final long currentTime = AnimationUtils.currentAnimationTimeMillis(); - final long elapsedSinceStart = currentTime - mStartTime; - return elapsedSinceStart > mDuration; } /** - * 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; } } } |