diff --git a/core/java/com/android/internal/widget/AutoScrollHelper.java b/core/java/com/android/internal/widget/AutoScrollHelper.java index 6298e353202da..afa410338cdac 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. *
* Note: 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. *
@@ -60,12 +61,14 @@ import android.widget.AbsListView; * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}. * *
* The following scrolling properties may be configured: *
+ * 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. *
- * 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); + 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 ((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); - } + // 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. - *
- * Returns whether the target view was able to scroll the requested amount.
- *
- * @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.
+ * @return whether the target is able to scroll in the requested direction
*/
- public abstract boolean onScrollBy(int deltaX, int deltaY);
+ 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
@@ -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,6 +648,16 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
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;
@@ -561,73 +668,49 @@ 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);
- }
- }
-
/**
* 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(
- 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;
+ scrollTargetBy(deltaX, deltaY);
- // If we just started actively scrolling, make sure any down
- // or move events send to the target view are canceled.
- if (mExclusiveEnabled && scrolling) {
- cancelTargetTouch();
- }
- }
-
- // 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 setDuration(int durationMillis) {
- mDuration = durationMillis;
+ public void setRampUpDuration(int durationMillis) {
+ mRampUpDuration = durationMillis;
+ }
+
+ public void setRampDownDuration(int durationMillis) {
+ mRampDownDuration = durationMillis;
}
/**
@@ -665,34 +760,52 @@ public abstract class AutoScrollHelper implements View.OnTouchListener {
*/
public void start() {
mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mStopTime = -1;
mDeltaTime = mStartTime;
- }
-
- /**
- * Returns whether the scroller is finished, which means that its
- * acceleration is zero.
- *
- * @return Whether the scroller is finished.
- */
- public boolean isFinished() {
- if (mTargetVelocityX == 0 && mTargetVelocityY == 0) {
- return true;
- }
- final long currentTime = AnimationUtils.currentAnimationTimeMillis();
- final long elapsedSinceStart = currentTime - mStartTime;
- return elapsedSinceStart > mDuration;
- }
-
- /**
- * Stops the scroller and resets its values.
- */
- public void reset() {
- mStartTime = -1;
- mDeltaTime = -1;
+ 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
+ * y = -4x * (x-1).
+ *
+ * @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()}.
@@ -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;
}
}
}