From 17dfce15d5c3e7eae7a3c129019f48e7c5f65063 Mon Sep 17 00:00:00 2001 From: Adam Powell Date: Mon, 25 Jan 2010 18:38:22 -0800 Subject: [PATCH] Added OverScroller and overscroll effects for ScrollView and HorizontalScrollView. --- core/java/android/widget/HorizontalScrollView.java | 52 ++- core/java/android/widget/OverScroller.java | 354 +++++++++++++++++++++ core/java/android/widget/ScrollView.java | 56 ++-- 3 files changed, 394 insertions(+), 68 deletions(-) create mode 100644 core/java/android/widget/OverScroller.java diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 52f56a7550ae..4cc3b9e98970 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -63,7 +63,7 @@ public class HorizontalScrollView extends FrameLayout { private long mLastScroll; private final Rect mTempRect = new Rect(); - private Scroller mScroller; + private OverScroller mScroller; /** * Flag to indicate that we are moving focus ourselves. This is so the @@ -177,7 +177,7 @@ public class HorizontalScrollView extends FrameLayout { private void initScrollView() { - mScroller = new Scroller(getContext()); + mScroller = new OverScroller(getContext()); setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setWillNotDraw(false); @@ -380,11 +380,6 @@ public class HorizontalScrollView extends FrameLayout { return true; } - if (!canScroll()) { - mIsBeingDragged = false; - return false; - } - final float x = ev.getX(); switch (action) { @@ -440,10 +435,6 @@ public class HorizontalScrollView extends FrameLayout { return false; } - if (!canScroll()) { - return false; - } - if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } @@ -470,25 +461,23 @@ public class HorizontalScrollView extends FrameLayout { final int deltaX = (int) (mLastMotionX - x); mLastMotionX = x; - if (deltaX < 0) { - if (mScrollX > 0) { - scrollBy(deltaX, 0); - } - } else if (deltaX > 0) { - final int rightEdge = getWidth() - mPaddingRight; - final int availableToScroll = getChildAt(0).getRight() - mScrollX - rightEdge; - if (availableToScroll > 0) { - scrollBy(Math.min(availableToScroll, deltaX), 0); - } - } + super.scrollTo(mScrollX + deltaX, mScrollY); break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getXVelocity(); - if ((Math.abs(initialVelocity) > mMinimumVelocity) && getChildCount() > 0) { - fling(-initialVelocity); + if (getChildCount() > 0) { + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + fling(-initialVelocity); + } else { + final int right = Math.max(0, getChildAt(0).getHeight() - + (getHeight() - mPaddingRight - mPaddingLeft)); + if (mScroller.springback(mScrollX, mScrollY, 0, 0, right, 0)) { + invalidate(); + } + } } if (mVelocityTracker != null) { @@ -913,14 +902,10 @@ public class HorizontalScrollView extends FrameLayout { int oldY = mScrollY; int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); - if (getChildCount() > 0) { - View child = getChildAt(0); - mScrollX = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth()); - mScrollY = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight()); - } else { - mScrollX = x; - mScrollY = y; - } + + mScrollX = x; + mScrollY = y; + if (oldX != mScrollX || oldY != mScrollY) { onScrollChanged(mScrollX, mScrollY, oldX, oldY); } @@ -1156,7 +1141,8 @@ public class HorizontalScrollView extends FrameLayout { int width = getWidth() - mPaddingRight - mPaddingLeft; int right = getChildAt(0).getWidth(); - mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0, right - width, 0, 0); + mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0, + Math.max(0, right - width), 0, 0, width/2, 0); final boolean movingRight = velocityX > 0; diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java new file mode 100644 index 000000000000..3fd5dcc52e10 --- /dev/null +++ b/core/java/android/widget/OverScroller.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import android.content.Context; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +/** + * This class encapsulates scrolling with the ability to overshoot the bounds + * of a scrolling operation. This class attempts to be a drop-in replacement + * for {@link android.widget.Scroller} in most cases. + * + * @hide Pending API approval + */ +public class OverScroller { + private static final int SPRINGBACK_DURATION = 150; + private static final int OVERFLING_DURATION = 150; + + private static final int MODE_DEFAULT = 0; + private static final int MODE_OVERFLING = 1; + private static final int MODE_SPRINGBACK = 2; + + private Scroller mDefaultScroller; + private Scroller mDecelScroller; + private Scroller mAccelDecelScroller; + private Scroller mCurrScroller; + + private int mScrollMode = MODE_DEFAULT; + + private int mMinimumX; + private int mMinimumY; + private int mMaximumX; + private int mMaximumY; + + public OverScroller(Context context) { + mDefaultScroller = new Scroller(context); + mDecelScroller = new Scroller(context, new DecelerateInterpolator(3.f)); + mAccelDecelScroller = new Scroller(context, new AccelerateDecelerateInterpolator()); + mCurrScroller = mDefaultScroller; + } + + /** + * Call this when you want to know the new location. If it returns true, + * the animation is not yet finished. loc will be altered to provide the + * new location. + */ + public boolean computeScrollOffset() { + boolean inProgress = mCurrScroller.computeScrollOffset(); + + switch (mScrollMode) { + case MODE_OVERFLING: + if (!inProgress) { + // Overfling ended + if (springback(mCurrScroller.getCurrX(), mCurrScroller.getCurrY(), + mMinimumX, mMaximumX, mMinimumY, mMaximumY, mAccelDecelScroller)) { + return mCurrScroller.computeScrollOffset(); + } else { + mCurrScroller = mDefaultScroller; + mScrollMode = MODE_DEFAULT; + } + } + break; + + case MODE_SPRINGBACK: + if (!inProgress) { + mCurrScroller = mDefaultScroller; + mScrollMode = MODE_DEFAULT; + } + break; + + case MODE_DEFAULT: + // Fling/autoscroll - did we go off the edge? + if (inProgress) { + Scroller scroller = mCurrScroller; + final int x = scroller.getCurrX(); + final int y = scroller.getCurrY(); + final int minX = mMinimumX; + final int maxX = mMaximumX; + final int minY = mMinimumY; + final int maxY = mMaximumY; + if (x < minX || x > maxX || y < minY || y > maxY) { + final int startx = scroller.getStartX(); + final int starty = scroller.getStartY(); + final int time = scroller.timePassed(); + final float timeSecs = time / 1000.f; + final float xvel = ((x - startx) / timeSecs); + final float yvel = ((y - starty) / timeSecs); + + if ((x < minX && xvel > 0) || (y < minY && yvel > 0) || + (x > maxX && xvel < 0) || (y > maxY && yvel < 0)) { + // If our velocity would take us back into valid areas, + // try to springback rather than overfling. + if (springback(x, y, minX, maxX, minY, maxY)) { + return mCurrScroller.computeScrollOffset(); + } + } else { + overfling(x, y, xvel, yvel); + return mCurrScroller.computeScrollOffset(); + } + } + } + break; + } + + return inProgress; + } + + private void overfling(int startx, int starty, float xvel, float yvel) { + Scroller scroller = mDecelScroller; + final float durationSecs = (OVERFLING_DURATION / 1000.f); + int dx = (int)(xvel * durationSecs) / 8; + int dy = (int)(yvel * durationSecs) / 8; + scroller.startScroll(startx, starty, dx, dy, OVERFLING_DURATION); + mCurrScroller.abortAnimation(); + mCurrScroller = scroller; + mScrollMode = MODE_OVERFLING; + } + + /** + * Call this when you want to 'spring back' into a valid coordinate range. + * + * @param startX Starting X coordinate + * @param startY Starting Y coordinate + * @param minX Minimum valid X value + * @param maxX Maximum valid X value + * @param minY Minimum valid Y value + * @param maxY Minimum valid Y value + * @return true if a springback was initiated, false if startX/startY was + * already within the valid range. + */ + public boolean springback(int startX, int startY, int minX, int maxX, + int minY, int maxY) { + return springback(startX, startY, minX, maxX, minY, maxY, mDecelScroller); + } + + private boolean springback(int startX, int startY, int minX, int maxX, + int minY, int maxY, Scroller scroller) { + int xoff = 0; + int yoff = 0; + if (startX < minX) { + xoff = minX - startX; + } else if (startX > maxX) { + xoff = maxX - startX; + } + if (startY < minY) { + yoff = minY - startY; + } else if (startY > maxY) { + yoff = maxY - startY; + } + + if (xoff != 0 || yoff != 0) { + scroller.startScroll(startX, startY, xoff, yoff, SPRINGBACK_DURATION); + mCurrScroller.abortAnimation(); + mCurrScroller = scroller; + mScrollMode = MODE_SPRINGBACK; + return true; + } + + return false; + } + + /** + * + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + public final boolean isFinished() { + return mCurrScroller.isFinished(); + } + + /** + * Returns the current X offset in the scroll. + * + * @return The new X offset as an absolute distance from the origin. + */ + public final int getCurrX() { + return mCurrScroller.getCurrX(); + } + + /** + * Returns the current Y offset in the scroll. + * + * @return The new Y offset as an absolute distance from the origin. + */ + public final int getCurrY() { + return mCurrScroller.getCurrY(); + } + + /** + * Stops the animation, resets any springback/overfling and completes + * any standard flings/scrolls in progress. + */ + public void abortAnimation() { + mCurrScroller.abortAnimation(); + mCurrScroller = mDefaultScroller; + mScrollMode = MODE_DEFAULT; + mCurrScroller.abortAnimation(); + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * The scroll will use the default value of 250 milliseconds for the + * duration. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + */ + public void startScroll(int startX, int startY, int dx, int dy) { + mCurrScroller.abortAnimation(); + mCurrScroller = mDefaultScroller; + mScrollMode = MODE_DEFAULT; + mMinimumX = Math.min(startX, startX + dx); + mMinimumY = Math.min(startY, startY + dy); + mMaximumX = Math.max(startX, startX + dx); + mMaximumY = Math.max(startY, startY + dy); + mCurrScroller.startScroll(startX, startY, dx, dy); + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + * @param duration Duration of the scroll in milliseconds. + */ + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + mCurrScroller.abortAnimation(); + mCurrScroller = mDefaultScroller; + mScrollMode = MODE_DEFAULT; + mMinimumX = Math.min(startX, startX + dx); + mMinimumY = Math.min(startY, startY + dy); + mMaximumX = Math.max(startX, startX + dx); + mMaximumY = Math.max(startY, startY + dy); + mCurrScroller.startScroll(startX, startY, dx, dy, duration); + } + + /** + * Returns the duration of the active scroll in progress; standard, fling, + * springback, or overfling. Does not account for any overflings or springback + * that may result. + */ + public int getDuration() { + return mCurrScroller.getDuration(); + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + this.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point unless overX > 0. If overfling is allowed, it will use minX + * as a springback boundary. + * @param maxX Maximum X value. The scroller will not scroll past this + * point unless overX > 0. If overfling is allowed, it will use maxX + * as a springback boundary. + * @param minY Minimum Y value. The scroller will not scroll past this + * point unless overY > 0. If overfling is allowed, it will use minY + * as a springback boundary. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point unless overY > 0. If overfling is allowed, it will use maxY + * as a springback boundary. + * @param overX Overfling range. If > 0, horizontal overfling in either + * direction will be possible. + * @param overY Overfling range. If > 0, vertical overfling in either + * direction will be possible. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + mCurrScroller = mDefaultScroller; + mScrollMode = MODE_DEFAULT; + mMinimumX = minX; + mMaximumX = maxX; + mMinimumY = minY; + mMaximumY = maxY; + mCurrScroller.fling(startX, startY, velocityX, velocityY, + minX - overX, maxX + overX, minY - overY, maxY + overY); + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final X offset as an absolute distance from the origin. + */ + public int getFinalX() { + return mCurrScroller.getFinalX(); + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final Y offset as an absolute distance from the origin. + */ + public int getFinalY() { + return mCurrScroller.getFinalY(); + } +} diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index bf16e289075b..62797f3c384a 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -16,6 +16,8 @@ package android.widget; +import com.android.internal.R; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; @@ -30,8 +32,6 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.AnimationUtils; -import com.android.internal.R; - import java.util.List; /** @@ -59,7 +59,7 @@ public class ScrollView extends FrameLayout { private long mLastScroll; private final Rect mTempRect = new Rect(); - private Scroller mScroller; + private OverScroller mScroller; /** * Flag to indicate that we are moving focus ourselves. This is so the @@ -173,7 +173,7 @@ public class ScrollView extends FrameLayout { private void initScrollView() { - mScroller = new Scroller(getContext()); + mScroller = new OverScroller(getContext()); setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setWillNotDraw(false); @@ -378,11 +378,6 @@ public class ScrollView extends FrameLayout { return true; } - if (!canScroll()) { - mIsBeingDragged = false; - return false; - } - final float y = ev.getY(); switch (action) { @@ -437,10 +432,6 @@ public class ScrollView extends FrameLayout { // descendants. return false; } - - if (!canScroll()) { - return false; - } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); @@ -468,25 +459,23 @@ public class ScrollView extends FrameLayout { final int deltaY = (int) (mLastMotionY - y); mLastMotionY = y; - if (deltaY < 0) { - if (mScrollY > 0) { - scrollBy(0, deltaY); - } - } else if (deltaY > 0) { - final int bottomEdge = getHeight() - mPaddingBottom; - final int availableToScroll = getChildAt(0).getBottom() - mScrollY - bottomEdge; - if (availableToScroll > 0) { - scrollBy(0, Math.min(availableToScroll, deltaY)); - } - } + super.scrollTo(mScrollX, mScrollY + deltaY); break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(); - if ((Math.abs(initialVelocity) > mMinimumVelocity) && getChildCount() > 0) { - fling(-initialVelocity); + if (getChildCount() > 0) { + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + fling(-initialVelocity); + } else { + final int bottom = Math.max(0, getChildAt(0).getHeight() - + (getHeight() - mPaddingBottom - mPaddingTop)); + if (mScroller.springback(mScrollX, mScrollY, 0, 0, 0, bottom)) { + invalidate(); + } + } } if (mVelocityTracker != null) { @@ -915,14 +904,10 @@ public class ScrollView extends FrameLayout { int oldY = mScrollY; int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); - if (getChildCount() > 0) { - View child = getChildAt(0); - mScrollX = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth()); - mScrollY = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight()); - } else { - mScrollX = x; - mScrollY = y; - } + + mScrollX = x; + mScrollY = y; + if (oldX != mScrollX || oldY != mScrollY) { onScrollChanged(mScrollX, mScrollY, oldX, oldY); } @@ -1159,7 +1144,8 @@ public class ScrollView extends FrameLayout { int height = getHeight() - mPaddingBottom - mPaddingTop; int bottom = getChildAt(0).getHeight(); - mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, bottom - height); + mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, + Math.max(0, bottom - height), 0, height/2); final boolean movingDown = velocityY > 0; -- 2.11.0