Overscrolling in AbsListView; visibility cleanup for FlingRunnable
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 3a4b92d..649167e 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.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.Canvas;
@@ -30,6 +32,7 @@
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
@@ -41,14 +44,12 @@
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import android.view.ContextMenu.ContextMenuInfo;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionWrapper;
import android.view.inputmethod.InputMethodManager;
-import android.view.ContextMenu.ContextMenuInfo;
-
-import com.android.internal.R;
import java.util.ArrayList;
import java.util.List;
@@ -120,13 +121,24 @@
* Indicates the touch gesture is a scroll
*/
static final int TOUCH_MODE_SCROLL = 3;
-
+
/**
* Indicates the view is in the process of being flung
*/
static final int TOUCH_MODE_FLING = 4;
/**
+ * Indicates the touch gesture is an overscroll - a scroll beyond the beginning or end.
+ */
+ static final int TOUCH_MODE_OVERSCROLL = 5;
+
+ /**
+ * Indicates the view is being flung outside of normal content bounds
+ * and will spring back.
+ */
+ static final int TOUCH_MODE_OVERFLING = 6;
+
+ /**
* Regular layout - usually an unsolicited layout from the view system
*/
static final int LAYOUT_NORMAL = 0;
@@ -1915,6 +1927,14 @@
mLayoutMode = LAYOUT_NORMAL;
layoutChildren();
}
+ } else {
+ int touchMode = mTouchMode;
+ if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) {
+ mScrollY = 0;
+ if (mFlingRunnable != null) {
+ mFlingRunnable.endFling();
+ }
+ }
}
}
@@ -1947,43 +1967,56 @@
switch (action) {
case MotionEvent.ACTION_DOWN: {
- int motionPosition = pointToPosition(x, y);
- if (!mDataChanged) {
- if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
- && (getAdapter().isEnabled(motionPosition))) {
- // User clicked on an actual view (and was not stopping a fling). It might be a
- // click or a scroll. Assume it is a click until proven otherwise
- mTouchMode = TOUCH_MODE_DOWN;
- // FIXME Debounce
- if (mPendingCheckForTap == null) {
- mPendingCheckForTap = new CheckForTap();
+ switch (mTouchMode) {
+ case TOUCH_MODE_OVERFLING: {
+ mFlingRunnable.endFling();
+ mTouchMode = TOUCH_MODE_OVERSCROLL;
+ mLastY = y;
+ mMotionCorrection = 0;
+ break;
+ }
+
+ default: {
+ int motionPosition = pointToPosition(x, y);
+ if (!mDataChanged) {
+ if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
+ && (getAdapter().isEnabled(motionPosition))) {
+ // User clicked on an actual view (and was not stopping a fling). It might be a
+ // click or a scroll. Assume it is a click until proven otherwise
+ mTouchMode = TOUCH_MODE_DOWN;
+ // FIXME Debounce
+ if (mPendingCheckForTap == null) {
+ mPendingCheckForTap = new CheckForTap();
+ }
+ postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
+ } else {
+ if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
+ // If we couldn't find a view to click on, but the down event was touching
+ // the edge, we will bail out and try again. This allows the edge correcting
+ // code in ViewRoot to try to find a nearby view to select
+ return false;
+ }
+ // User clicked on whitespace, or stopped a fling. It is a scroll.
+ createScrollingCache();
+ mTouchMode = TOUCH_MODE_SCROLL;
+ mMotionCorrection = 0;
+ motionPosition = findMotionRow(y);
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
- postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
- } else {
- if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
- // If we couldn't find a view to click on, but the down event was touching
- // the edge, we will bail out and try again. This allows the edge correcting
- // code in ViewRoot to try to find a nearby view to select
- return false;
- }
- // User clicked on whitespace, or stopped a fling. It is a scroll.
- createScrollingCache();
- mTouchMode = TOUCH_MODE_SCROLL;
- mMotionCorrection = 0;
- motionPosition = findMotionRow(y);
- reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
- }
- if (motionPosition >= 0) {
- // Remember where the motion event started
- v = getChildAt(motionPosition - mFirstPosition);
- mMotionViewOriginalTop = v.getTop();
- mMotionX = x;
- mMotionY = y;
- mMotionPosition = motionPosition;
+ if (motionPosition >= 0) {
+ // Remember where the motion event started
+ v = getChildAt(motionPosition - mFirstPosition);
+ mMotionViewOriginalTop = v.getTop();
+ mMotionX = x;
+ mMotionY = y;
+ mMotionPosition = motionPosition;
+ }
+ mLastY = Integer.MIN_VALUE;
+ break;
}
- mLastY = Integer.MIN_VALUE;
+ }
break;
}
@@ -2008,17 +2041,61 @@
if (y != mLastY) {
deltaY -= mMotionCorrection;
int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
+
+ int motionViewPrevTop = 0;
+ View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ motionViewPrevTop = motionView.getTop();
+ }
// No need to do all this work if we're not going to move anyway
if (incrementalDeltaY != 0) {
trackMotionScroll(deltaY, incrementalDeltaY);
}
// Check to see if we have bumped into the scroll limit
- View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
+ motionView = this.getChildAt(mMotionPosition - mFirstPosition);
if (motionView != null) {
// Check if the top of the motion view is where it is
// supposed to be
- if (motionView.getTop() != mMotionViewNewTop) {
+ final int motionViewRealTop = motionView.getTop();
+ final int motionViewNewTop = mMotionViewNewTop;
+ if (motionViewRealTop != motionViewNewTop) {
+ // Apply overscroll
+
+ mScrollY -= incrementalDeltaY - (motionViewRealTop - motionViewPrevTop);
+ mTouchMode = TOUCH_MODE_OVERSCROLL;
+ invalidate();
+ }
+ }
+ mLastY = y;
+ }
+ break;
+
+ case TOUCH_MODE_OVERSCROLL:
+ if (y != mLastY) {
+ deltaY -= mMotionCorrection;
+ int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
+
+ final int oldScroll = mScrollY;
+ final int newScroll = oldScroll - incrementalDeltaY;
+
+ if ((oldScroll >= 0 && newScroll <= 0) ||
+ (oldScroll <= 0 && newScroll >= 0)) {
+ // Coming back to 'real' list scrolling
+ incrementalDeltaY = -newScroll;
+ mScrollY = 0;
+
+ // No need to do all this work if we're not going to move anyway
+ if (incrementalDeltaY != 0) {
+ trackMotionScroll(incrementalDeltaY, incrementalDeltaY);
+ }
+
+ // Check to see if we are back in
+ View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ int topOffset = motionView.getTop() - mMotionViewNewTop;
+ mTouchMode = TOUCH_MODE_SCROLL;
+
// We did not scroll the full amount. Treat this essentially like the
// start of a new touch scroll
final int motionPosition = findMotionRow(y);
@@ -2029,6 +2106,9 @@
mMotionY = y;
mMotionPosition = motionPosition;
}
+ } else {
+ mScrollY -= incrementalDeltaY;
+ invalidate();
}
mLastY = y;
}
@@ -2120,6 +2200,7 @@
mFlingRunnable = new FlingRunnable();
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+
mFlingRunnable.start(-initialVelocity);
} else {
mTouchMode = TOUCH_MODE_REST;
@@ -2130,6 +2211,24 @@
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
+ break;
+
+ case TOUCH_MODE_OVERSCROLL:
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ final int initialVelocity = (int) velocityTracker.getYVelocity();
+
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+ if (Math.abs(initialVelocity) > mMinimumVelocity) {
+ mFlingRunnable.startOverfling(-initialVelocity);
+ } else {
+ mFlingRunnable.startSpringback();
+ }
+
+ break;
}
setPressed(false);
@@ -2157,25 +2256,38 @@
}
case MotionEvent.ACTION_CANCEL: {
- mTouchMode = TOUCH_MODE_REST;
- setPressed(false);
- View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
- if (motionView != null) {
- motionView.setPressed(false);
- }
- clearScrollingCache();
+ switch (mTouchMode) {
+ case TOUCH_MODE_OVERSCROLL:
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ mFlingRunnable.startSpringback();
+ break;
+
+ case TOUCH_MODE_OVERFLING:
+ // Do nothing - let it play out.
+ break;
+
+ default:
+ mTouchMode = TOUCH_MODE_REST;
+ setPressed(false);
+ View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ motionView.setPressed(false);
+ }
+ clearScrollingCache();
- final Handler handler = getHandler();
- if (handler != null) {
- handler.removeCallbacks(mPendingCheckForLongPress);
- }
+ final Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mPendingCheckForLongPress);
+ }
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
}
}
-
}
return true;
@@ -2205,8 +2317,13 @@
switch (action) {
case MotionEvent.ACTION_DOWN: {
+ int touchMode = mTouchMode;
+ if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
+ return true;
+ }
+
int motionPosition = findMotionRow(y);
- if (mTouchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
+ if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
// User clicked on an actual view (and was not stopping a fling).
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
@@ -2218,7 +2335,7 @@
clearScrollingCache();
}
mLastY = Integer.MIN_VALUE;
- if (mTouchMode == TOUCH_MODE_FLING) {
+ if (touchMode == TOUCH_MODE_FLING) {
return true;
}
break;
@@ -2293,18 +2410,18 @@
/**
* Tracks the decay of a fling scroll
*/
- private Scroller mScroller;
+ private OverScroller mScroller;
/**
* Y value reported by mScroller on the previous fling
*/
private int mLastFlingY;
- public FlingRunnable() {
- mScroller = new Scroller(getContext());
+ FlingRunnable() {
+ mScroller = new OverScroller(getContext());
}
- public void start(int initialVelocity) {
+ void start(int initialVelocity) {
int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
mLastFlingY = initialY;
mScroller.fling(0, initialY, 0, initialVelocity,
@@ -2319,77 +2436,117 @@
}
}
}
-
+
+ void startSpringback() {
+ if (mScroller.springback(0, mScrollY, 0, 0, 0, 0)) {
+ mTouchMode = TOUCH_MODE_OVERFLING;
+ invalidate();
+ post(this);
+ }
+ }
+
+ void startOverfling(int initialVelocity) {
+ mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0, 0, 0, 0, getHeight());
+ mTouchMode = TOUCH_MODE_OVERFLING;
+ invalidate();
+ post(this);
+ }
+
private void endFling() {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
clearScrollingCache();
+ removeCallbacks(this);
}
public void run() {
- if (mTouchMode != TOUCH_MODE_FLING) {
+ switch (mTouchMode) {
+ default:
return;
- }
-
- if (mItemCount == 0 || getChildCount() == 0) {
- endFling();
- return;
- }
-
- final Scroller scroller = mScroller;
- boolean more = scroller.computeScrollOffset();
- final int y = scroller.getCurrY();
-
- // Flip sign to convert finger direction to list items direction
- // (e.g. finger moving down means list is moving towards the top)
- int delta = mLastFlingY - y;
-
- // Pretend that each frame of a fling scroll is a touch scroll
- if (delta > 0) {
- // List is moving towards the top. Use first view as mMotionPosition
- mMotionPosition = mFirstPosition;
- final View firstView = getChildAt(0);
- mMotionViewOriginalTop = firstView.getTop();
-
- // Don't fling more than 1 screen
- delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta);
- } else {
- // List is moving towards the bottom. Use last view as mMotionPosition
- int offsetToLast = getChildCount() - 1;
- mMotionPosition = mFirstPosition + offsetToLast;
-
- final View lastView = getChildAt(offsetToLast);
- mMotionViewOriginalTop = lastView.getTop();
-
- // Don't fling more than 1 screen
- delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta);
- }
-
- trackMotionScroll(delta, delta);
-
- // Check to see if we have bumped into the scroll limit
- View motionView = getChildAt(mMotionPosition - mFirstPosition);
- if (motionView != null) {
- // Check if the top of the motion view is where it is
- // supposed to be
- if (motionView.getTop() != mMotionViewNewTop) {
- more = false;
+
+ case TOUCH_MODE_FLING: {
+ if (mItemCount == 0 || getChildCount() == 0) {
+ endFling();
+ return;
}
- }
- if (more) {
- invalidate();
- mLastFlingY = y;
- post(this);
- } else {
- endFling();
- if (PROFILE_FLINGING) {
- if (mFlingProfilingStarted) {
- Debug.stopMethodTracing();
- mFlingProfilingStarted = false;
+ final OverScroller scroller = mScroller;
+ boolean more = scroller.computeScrollOffset();
+ final int y = scroller.getCurrY();
+
+ // Flip sign to convert finger direction to list items direction
+ // (e.g. finger moving down means list is moving towards the top)
+ int delta = mLastFlingY - y;
+
+ // Pretend that each frame of a fling scroll is a touch scroll
+ if (delta > 0) {
+ // List is moving towards the top. Use first view as mMotionPosition
+ mMotionPosition = mFirstPosition;
+ final View firstView = getChildAt(0);
+ mMotionViewOriginalTop = firstView.getTop();
+
+ // Don't fling more than 1 screen
+ delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta);
+ } else {
+ // List is moving towards the bottom. Use last view as mMotionPosition
+ int offsetToLast = getChildCount() - 1;
+ mMotionPosition = mFirstPosition + offsetToLast;
+
+ final View lastView = getChildAt(offsetToLast);
+ mMotionViewOriginalTop = lastView.getTop();
+
+ // Don't fling more than 1 screen
+ delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta);
+ }
+
+ // Do something different on overscroll - offsetChildrenTopAndBottom()
+ trackMotionScroll(delta, delta);
+
+ // Check to see if we have bumped into the scroll limit
+ View motionView = getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ // Check if the top of the motion view is where it is
+ // supposed to be
+ if (motionView.getTop() != mMotionViewNewTop) {
+ float vel = scroller.getCurrVelocity();
+ if (delta > 0) {
+ vel = -vel;
+ }
+ startOverfling(Math.round(vel));
+ break;
}
}
+
+ if (more) {
+ invalidate();
+ mLastFlingY = y;
+ post(this);
+ } else {
+ endFling();
+
+ if (PROFILE_FLINGING) {
+ if (mFlingProfilingStarted) {
+ Debug.stopMethodTracing();
+ mFlingProfilingStarted = false;
+ }
+ }
+ }
+ break;
}
+
+ case TOUCH_MODE_OVERFLING: {
+ final OverScroller scroller = mScroller;
+ if (scroller.computeScrollOffset()) {
+ mScrollY = scroller.getCurrY();
+ invalidate();
+ post(this);
+ } else {
+ endFling();
+ }
+ break;
+ }
+ }
+
}
}
diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java
index b22ae3c..44d415e 100644
--- a/core/java/android/widget/OverScroller.java
+++ b/core/java/android/widget/OverScroller.java
@@ -383,4 +383,15 @@
public int getFinalY() {
return mCurrScroller.getFinalY();
}
+
+ /**
+ * @hide
+ * Returns the current velocity.
+ *
+ * @return The original velocity less the deceleration. Result may be
+ * negative.
+ */
+ public float getCurrVelocity() {
+ return mCurrScroller.getCurrVelocity();
+ }
}