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
private void initScrollView() {
- mScroller = new Scroller(getContext());
+ mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
return true;
}
- if (!canScroll()) {
- mIsBeingDragged = false;
- return false;
- }
-
final float x = ev.getX();
switch (action) {
return false;
}
- if (!canScroll()) {
- return false;
- }
-
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
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) {
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);
}
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;
--- /dev/null
+/*
+ * 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();
+ }
+}
package android.widget;
+import com.android.internal.R;
+
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.view.ViewParent;
import android.view.animation.AnimationUtils;
-import com.android.internal.R;
-
import java.util.List;
/**
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
private void initScrollView() {
- mScroller = new Scroller(getContext());
+ mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
return true;
}
- if (!canScroll()) {
- mIsBeingDragged = false;
- return false;
- }
-
final float y = ev.getY();
switch (action) {
// descendants.
return false;
}
-
- if (!canScroll()) {
- return false;
- }
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
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) {
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);
}
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;