diff options
author | Adam Powell <adamp@google.com> | 2010-08-25 14:37:03 -0700 |
---|---|---|
committer | Adam Powell <adamp@google.com> | 2010-08-30 19:14:07 -0700 |
commit | 0a77ce277c6ed2aa25bbea5f8cd5687c0720cb68 (patch) | |
tree | ebc9590d88ae375498aac04a168c49984138749a /core | |
parent | 4c72ad75cfb413f54cb59d413a232e77c7260ef2 (diff) | |
download | frameworks_base-0a77ce277c6ed2aa25bbea5f8cd5687c0720cb68.zip frameworks_base-0a77ce277c6ed2aa25bbea5f8cd5687c0720cb68.tar.gz frameworks_base-0a77ce277c6ed2aa25bbea5f8cd5687c0720cb68.tar.bz2 |
New edge effects for scrolling widgets (overscroll)
TODO: Currently disabled for WebView. Assets used for the glow effect
need to be themable/styleable. Overscroll effect should take place
even when the user did not grab the widget within actual content.
Change-Id: I68277d14d37dc5bcdb9254eaddf6e4998b3f2bf4
Diffstat (limited to 'core')
19 files changed, 2401 insertions, 183 deletions
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c9662ff..7332c16 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -1521,6 +1521,40 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private static final int AWAKEN_SCROLL_BARS_ON_ATTACH = 0x08000000; /** + * Always allow a user to overscroll this view, provided it is a + * view that can scroll. + * + * @see #getOverscrollMode() + * @see #setOverscrollMode(int) + */ + public static final int OVERSCROLL_ALWAYS = 0; + + /** + * Allow a user to overscroll this view only if the content is large + * enough to meaningfully scroll, provided it is a view that can scroll. + * + * @see #getOverscrollMode() + * @see #setOverscrollMode(int) + */ + public static final int OVERSCROLL_IF_CONTENT_SCROLLS = 1; + + /** + * Never allow a user to overscroll this view. + * + * @see #getOverscrollMode() + * @see #setOverscrollMode(int) + */ + public static final int OVERSCROLL_NEVER = 2; + + /** + * Controls the overscroll mode for this view. + * See {@link #overscrollBy(int, int, int, int, int, int, int, int, boolean)}, + * {@link #OVERSCROLL_ALWAYS}, {@link #OVERSCROLL_IF_CONTENT_SCROLLS}, + * and {@link #OVERSCROLL_NEVER}. + */ + private int mOverscrollMode = OVERSCROLL_ALWAYS; + + /** * The parent this view is attached to. * {@hide} * @@ -1877,6 +1911,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility int scrollbarStyle = SCROLLBARS_INSIDE_OVERLAY; + int overscrollMode = mOverscrollMode; final int N = a.getIndexCount(); for (int i = 0; i < N; i++) { int attr = a.getIndex(i); @@ -2076,9 +2111,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility }); } break; + case R.styleable.View_overscrollMode: + overscrollMode = a.getInt(attr, OVERSCROLL_ALWAYS); + break; } } + setOverscrollMode(overscrollMode); + if (background != null) { setBackgroundDrawable(background); } @@ -8681,6 +8721,128 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Scroll the view with standard behavior for scrolling beyond the normal + * content boundaries. Views that call this method should override + * {@link #onOverscrolled(int, int, boolean, boolean)} to respond to the + * results of an overscroll operation. + * + * Views can use this method to handle any touch or fling-based scrolling. + * + * @param deltaX Change in X in pixels + * @param deltaY Change in Y in pixels + * @param scrollX Current X scroll value in pixels before applying deltaX + * @param scrollY Current Y scroll value in pixels before applying deltaY + * @param scrollRangeX Maximum content scroll range along the X axis + * @param scrollRangeY Maximum content scroll range along the Y axis + * @param maxOverscrollX Number of pixels to overscroll by in either direction + * along the X axis. + * @param maxOverscrollY Number of pixels to overscroll by in either direction + * along the Y axis. + * @param isTouchEvent true if this scroll operation is the result of a touch event. + * @return true if scrolling was clamped to an overscroll boundary along either + * axis, false otherwise. + */ + protected boolean overscrollBy(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverscrollX, int maxOverscrollY, + boolean isTouchEvent) { + final int overscrollMode = mOverscrollMode; + final boolean canScrollHorizontal = + computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + final boolean canScrollVertical = + computeVerticalScrollRange() > computeVerticalScrollExtent(); + final boolean overscrollHorizontal = overscrollMode == OVERSCROLL_ALWAYS || + (overscrollMode == OVERSCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overscrollVertical = overscrollMode == OVERSCROLL_ALWAYS || + (overscrollMode == OVERSCROLL_IF_CONTENT_SCROLLS && canScrollVertical); + + int newScrollX = scrollX + deltaX; + if (!overscrollHorizontal) { + maxOverscrollX = 0; + } + + int newScrollY = scrollY + deltaY; + if (!overscrollVertical) { + maxOverscrollY = 0; + } + + // Clamp values if at the limits and record + final int left = -maxOverscrollX; + final int right = maxOverscrollX + scrollRangeX; + final int top = -maxOverscrollY; + final int bottom = maxOverscrollY + scrollRangeY; + + boolean clampedX = false; + if (newScrollX > right) { + newScrollX = right; + clampedX = true; + } else if (newScrollX < left) { + newScrollX = left; + clampedX = true; + } + + boolean clampedY = false; + if (newScrollY > bottom) { + newScrollY = bottom; + clampedY = true; + } else if (newScrollY < top) { + newScrollY = top; + clampedY = true; + } + + onOverscrolled(newScrollX, newScrollY, clampedX, clampedY); + + return clampedX || clampedY; + } + + /** + * Called by {@link #overscrollBy(int, int, int, int, int, int, int, int, boolean)} to + * respond to the results of an overscroll operation. + * + * @param scrollX New X scroll value in pixels + * @param scrollY New Y scroll value in pixels + * @param clampedX True if scrollX was clamped to an overscroll boundary + * @param clampedY True if scrollY was clamped to an overscroll boundary + */ + protected void onOverscrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + // Intentionally empty. + } + + /** + * Returns the overscroll mode for this view. The result will be + * one of {@link #OVERSCROLL_ALWAYS} (default), {@link #OVERSCROLL_IF_CONTENT_SCROLLS} + * (allow overscrolling only if the view content is larger than the container), + * or {@link #OVERSCROLL_NEVER}. + * + * @return This view's overscroll mode. + */ + public int getOverscrollMode() { + return mOverscrollMode; + } + + /** + * Set the overscroll mode for this view. Valid overscroll modes are + * {@link #OVERSCROLL_ALWAYS} (default), {@link #OVERSCROLL_IF_CONTENT_SCROLLS} + * (allow overscrolling only if the view content is larger than the container), + * or {@link #OVERSCROLL_NEVER}. + * + * Setting the overscroll mode of a view will have an effect only if the + * view is capable of scrolling. + * + * @param overscrollMode The new overscroll mode for this view. + */ + public void setOverscrollMode(int overscrollMode) { + if (overscrollMode != OVERSCROLL_ALWAYS && + overscrollMode != OVERSCROLL_IF_CONTENT_SCROLLS && + overscrollMode != OVERSCROLL_NEVER) { + throw new IllegalArgumentException("Invalid overscroll mode " + overscrollMode); + } + mOverscrollMode = overscrollMode; + } + + /** * A MeasureSpec encapsulates the layout requirements passed from parent to child. * Each MeasureSpec represents a requirement for either the width or the height. * A MeasureSpec is comprised of a size and a mode. There are three possible diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index acdfc28..aa9fe76 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -140,6 +140,16 @@ public class ViewConfiguration { */ private static float SCROLL_FRICTION = 0.015f; + /** + * Max distance to overscroll for edge effects + */ + private static final int OVERSCROLL_DISTANCE = 4; + + /** + * Max distance to overfling for edge effects + */ + private static final int OVERFLING_DISTANCE = 8; + private final int mEdgeSlop; private final int mFadingEdgeLength; private final int mMinimumFlingVelocity; @@ -150,6 +160,8 @@ public class ViewConfiguration { private final int mDoubleTapSlop; private final int mWindowTouchSlop; private final int mMaximumDrawingCacheSize; + private final int mOverscrollDistance; + private final int mOverflingDistance; private static final SparseArray<ViewConfiguration> sConfigurations = new SparseArray<ViewConfiguration>(2); @@ -170,6 +182,8 @@ public class ViewConfiguration { mWindowTouchSlop = WINDOW_TOUCH_SLOP; //noinspection deprecation mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE; + mOverscrollDistance = OVERSCROLL_DISTANCE; + mOverflingDistance = OVERFLING_DISTANCE; } /** @@ -198,6 +212,9 @@ public class ViewConfiguration { // Size of the screen in bytes, in ARGB_8888 format mMaximumDrawingCacheSize = 4 * metrics.widthPixels * metrics.heightPixels; + + mOverscrollDistance = (int) (density * OVERSCROLL_DISTANCE + 0.5f); + mOverflingDistance = (int) (density * OVERFLING_DISTANCE + 0.5f); } /** @@ -455,6 +472,20 @@ public class ViewConfiguration { } /** + * @return The maximum distance a View should overscroll by when showing edge effects. + */ + public int getScaledOverscrollDistance() { + return mOverscrollDistance; + } + + /** + * @return The maximum distance a View should overfling by when showing edge effects. + */ + public int getScaledOverflingDistance() { + return mOverflingDistance; + } + + /** * The amount of time that the zoom controls should be * displayed on the screen expressed in milliseconds. * diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index b767f11..1b801d4 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -207,6 +207,7 @@ public class WebSettings { private boolean mBuiltInZoomControls = false; private boolean mAllowFileAccess = true; private boolean mLoadWithOverviewMode = false; + private boolean mUseWebViewBackgroundOverscrollBackground = true; // private WebSettings, not accessible by the host activity static private int mDoubleTapToastCount = 3; @@ -485,6 +486,23 @@ public class WebSettings { } /** + * Set whether the WebView uses its background for over scroll background. + * If true, it will use the WebView's background. If false, it will use an + * internal pattern. Default is true. + */ + public void setUseWebViewBackgroundForOverscrollBackground(boolean view) { + mUseWebViewBackgroundOverscrollBackground = view; + } + + /** + * Returns true if this WebView uses WebView's background instead of + * internal pattern for over scroll background. + */ + public boolean getUseWebViewBackgroundForOverscrollBackground() { + return mUseWebViewBackgroundOverscrollBackground; + } + + /** * Store whether the WebView is saving form data. */ public void setSaveFormData(boolean save) { diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 052de97..0deb45a 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -39,8 +39,8 @@ import android.graphics.RectF; import android.graphics.Region; import android.graphics.Shader; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.net.http.SslCertificate; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -79,7 +79,7 @@ import android.widget.CheckedTextView; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ListView; -import android.widget.Scroller; +import android.widget.OverScroller; import android.widget.Toast; import android.widget.ZoomButtonsController; import android.widget.ZoomControls; @@ -92,8 +92,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; +import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -557,7 +557,10 @@ public class WebView extends AbsoluteLayout // time for the longest scroll animation private static final int MAX_DURATION = 750; // milliseconds private static final int SLIDE_TITLE_DURATION = 500; // milliseconds - private Scroller mScroller; + private OverScroller mScroller; + private boolean mInOverScrollMode = false; + private static Paint mOverScrollBackground; + private static Paint mOverScrollBorder; private boolean mWrapContent; private static final int MOTIONLESS_FALSE = 0; @@ -943,7 +946,7 @@ public class WebView extends AbsoluteLayout mViewManager = new ViewManager(this); mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces); mDatabase = WebViewDatabase.getInstance(context); - mScroller = new Scroller(context); + mScroller = new OverScroller(context); updateMultiTouchSupport(context); } @@ -983,6 +986,7 @@ public class WebView extends AbsoluteLayout setFocusableInTouchMode(true); setClickable(true); setLongClickable(true); + setOverscrollMode(OVERSCROLL_NEVER); final ViewConfiguration configuration = ViewConfiguration.get(getContext()); int slop = configuration.getScaledTouchSlop(); @@ -1146,7 +1150,8 @@ public class WebView extends AbsoluteLayout * Return the amount of the titlebarview (if any) that is visible */ private int getVisibleTitleHeight() { - return Math.max(getTitleHeight() - mScrollY, 0); + // need to restrict mScrollY due to over scroll + return Math.max(getTitleHeight() - Math.max(0, mScrollY), 0); } /* @@ -1786,7 +1791,7 @@ public class WebView extends AbsoluteLayout } nativeClearCursor(); // start next trackball movement from page edge if (bottom) { - return pinScrollTo(mScrollX, computeVerticalScrollRange(), true, 0); + return pinScrollTo(mScrollX, computeRealVerticalScrollRange(), true, 0); } // Page down. int h = getHeight(); @@ -2016,13 +2021,15 @@ public class WebView extends AbsoluteLayout // Expects x in view coordinates private int pinLocX(int x) { - return pinLoc(x, getViewWidth(), computeHorizontalScrollRange()); + if (mInOverScrollMode) return x; + return pinLoc(x, getViewWidth(), computeRealHorizontalScrollRange()); } // Expects y in view coordinates private int pinLocY(int y) { + if (mInOverScrollMode) return y; return pinLoc(y, getViewHeightWithTitle(), - computeVerticalScrollRange() + getTitleHeight()); + computeRealVerticalScrollRange() + getTitleHeight()); } /** @@ -2334,7 +2341,7 @@ public class WebView extends AbsoluteLayout // Sets r to be our visible rectangle in content coordinates private void calcOurContentVisibleRect(Rect r) { calcOurVisibleRect(r); - // pin the rect to the bounds of the content + // since we might overscroll, pin the rect to the bounds of the content r.left = Math.max(viewToContentX(r.left), 0); // viewToContentY will remove the total height of the title bar. Add // the visible height back in to account for the fact that if the title @@ -2415,8 +2422,7 @@ public class WebView extends AbsoluteLayout return false; } - @Override - protected int computeHorizontalScrollRange() { + private int computeRealHorizontalScrollRange() { if (mDrawHistory) { return mHistoryWidth; } else if (mHorizontalScrollBarMode == SCROLLBAR_ALWAYSOFF @@ -2430,7 +2436,27 @@ public class WebView extends AbsoluteLayout } @Override - protected int computeVerticalScrollRange() { + protected int computeHorizontalScrollRange() { + int range = computeRealHorizontalScrollRange(); + + // Adjust reported range if overscrolled to compress the scroll bars + final int scrollX = mScrollX; + final int overscrollRight = computeMaxScrollX(); + if (scrollX < 0) { + range -= scrollX; + } else if (scrollX > overscrollRight) { + range += scrollX - overscrollRight; + } + + return range; + } + + @Override + protected int computeHorizontalScrollOffset() { + return Math.max(mScrollX, 0); + } + + private int computeRealVerticalScrollRange() { if (mDrawHistory) { return mHistoryHeight; } else if (mVerticalScrollBarMode == SCROLLBAR_ALWAYSOFF @@ -2444,6 +2470,22 @@ public class WebView extends AbsoluteLayout } @Override + protected int computeVerticalScrollRange() { + int range = computeRealVerticalScrollRange(); + + // Adjust reported range if overscrolled to compress the scroll bars + final int scrollY = mScrollY; + final int overscrollBottom = computeMaxScrollY(); + if (scrollY < 0) { + range -= scrollY; + } else if (scrollY > overscrollBottom) { + range += scrollY - overscrollBottom; + } + + return range; + } + + @Override protected int computeVerticalScrollOffset() { return Math.max(mScrollY - getTitleHeight(), 0); } @@ -2462,6 +2504,23 @@ public class WebView extends AbsoluteLayout scrollBar.draw(canvas); } + @Override + protected void onOverscrolled(int scrollX, int scrollY, boolean clampedX, + boolean clampedY) { + mInOverScrollMode = false; + int maxX = computeMaxScrollX(); + if (maxX == 0) { + // do not over scroll x if the page just fits the screen + scrollX = pinLocX(scrollX); + } else if (scrollX < 0 || scrollX > maxX) { + mInOverScrollMode = true; + } + if (scrollY < 0 || scrollY > computeMaxScrollY()) { + mInOverScrollMode = true; + } + super.scrollTo(scrollX, scrollY); + } + /** * Get the url for the current page. This is not always the same as the url * passed to WebViewClient.onPageStarted because although the load for @@ -2815,11 +2874,14 @@ public class WebView extends AbsoluteLayout if (mScroller.computeScrollOffset()) { int oldX = mScrollX; int oldY = mScrollY; - mScrollX = mScroller.getCurrX(); - mScrollY = mScroller.getCurrY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); postInvalidate(); // So we draw again - if (oldX != mScrollX || oldY != mScrollY) { - onScrollChanged(mScrollX, mScrollY, oldX, oldY); + + if (oldX != x || oldY != y) { + overscrollBy(x - oldX, y - oldY, oldX, oldY, + computeMaxScrollX(), computeMaxScrollY(), + getViewWidth() / 3, getViewHeight() / 3, false); } } else { super.computeScroll(); @@ -3230,8 +3292,13 @@ public class WebView extends AbsoluteLayout protected boolean drawChild(Canvas canvas, View child, long drawingTime) { if (child == mTitleBar) { // When drawing the title bar, move it horizontally to always show - // at the top of the WebView. + // at the top of the WebView. While overscroll, stick the title bar + // on the top otherwise we may have two during loading, one is drawn + // here, another is drawn by the Browser. mTitleBar.offsetLeftAndRight(mScrollX - mTitleBar.getLeft()); + if (mScrollY <= 0) { + mTitleBar.offsetTopAndBottom(mScrollY - mTitleBar.getTop()); + } } return super.drawChild(canvas, child, drawingTime); } @@ -3267,6 +3334,36 @@ public class WebView extends AbsoluteLayout } int saveCount = canvas.save(); + if (mInOverScrollMode && !getSettings() + .getUseWebViewBackgroundForOverscrollBackground()) { + if (mOverScrollBackground == null) { + mOverScrollBackground = new Paint(); + Bitmap bm = BitmapFactory.decodeResource( + mContext.getResources(), + com.android.internal.R.drawable.status_bar_background); + mOverScrollBackground.setShader(new BitmapShader(bm, + Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); + mOverScrollBorder = new Paint(); + mOverScrollBorder.setStyle(Paint.Style.STROKE); + mOverScrollBorder.setStrokeWidth(0); + mOverScrollBorder.setColor(0xffbbbbbb); + } + + int top = getTitleHeight(); + int right = computeRealHorizontalScrollRange(); + int bottom = top + computeRealVerticalScrollRange(); + // first draw the background and anchor to the top of the view + canvas.save(); + canvas.translate(mScrollX, mScrollY); + canvas.clipRect(-mScrollX, top - mScrollY, right - mScrollX, bottom + - mScrollY, Region.Op.DIFFERENCE); + canvas.drawPaint(mOverScrollBackground); + canvas.restore(); + // then draw the border + canvas.drawRect(-1, top - 1, right, bottom, mOverScrollBorder); + // next clip the region for the content + canvas.clipRect(0, top, right, bottom); + } if (mTitleBar != null) { canvas.translate(0, (int) mTitleBar.getHeight()); } @@ -4405,12 +4502,14 @@ public class WebView extends AbsoluteLayout @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); - sendOurVisibleRect(); - // update WebKit if visible title bar height changed. The logic is same - // as getVisibleTitleHeight. - int titleHeight = getTitleHeight(); - if (Math.max(titleHeight - t, 0) != Math.max(titleHeight - oldt, 0)) { - sendViewSizeZoom(); + if (!mInOverScrollMode) { + sendOurVisibleRect(); + // update WebKit if visible title bar height changed. The logic is same + // as getVisibleTitleHeight. + int titleHeight = getTitleHeight(); + if (Math.max(titleHeight - t, 0) != Math.max(titleHeight - oldt, 0)) { + sendViewSizeZoom(); + } } } @@ -4480,7 +4579,7 @@ public class WebView extends AbsoluteLayout public DragTrackerHandler(float x, float y, DragTracker proxy) { mProxy = proxy; - int docBottom = computeVerticalScrollRange() + getTitleHeight(); + int docBottom = computeRealVerticalScrollRange() + getTitleHeight(); int viewTop = getScrollY(); int viewBottom = viewTop + getHeight(); @@ -4493,7 +4592,7 @@ public class WebView extends AbsoluteLayout " up/down= " + mMinDY + " " + mMaxDY); } - int docRight = computeHorizontalScrollRange(); + int docRight = computeRealHorizontalScrollRange(); int viewLeft = getScrollX(); int viewRight = viewLeft + getWidth(); mStartX = x; @@ -5001,18 +5100,6 @@ public class WebView extends AbsoluteLayout } // do pan - int newScrollX = pinLocX(mScrollX + deltaX); - int newDeltaX = newScrollX - mScrollX; - if (deltaX != newDeltaX) { - deltaX = newDeltaX; - fDeltaX = (float) newDeltaX; - } - int newScrollY = pinLocY(mScrollY + deltaY); - int newDeltaY = newScrollY - mScrollY; - if (deltaY != newDeltaY) { - deltaY = newDeltaY; - fDeltaY = (float) newDeltaY; - } boolean done = false; boolean keepScrollBarsVisible = false; if (Math.abs(fDeltaX) < 1.0f && Math.abs(fDeltaY) < 1.0f) { @@ -5196,6 +5283,12 @@ public class WebView extends AbsoluteLayout mHeldMotionless = MOTIONLESS_IGNORE; doFling(); break; + } else { + if (mScroller.springback(mScrollX, mScrollY, 0, + computeMaxScrollX(), 0, + computeMaxScrollY())) { + invalidate(); + } } // redraw in high-quality, as we're done dragging mHeldMotionless = MOTIONLESS_TRUE; @@ -5215,6 +5308,8 @@ public class WebView extends AbsoluteLayout } case MotionEvent.ACTION_CANCEL: { if (mTouchMode == TOUCH_DRAG_MODE) { + mScroller.springback(mScrollX, mScrollY, 0, + computeMaxScrollX(), 0, computeMaxScrollY()); invalidate(); } cancelWebCoreTouchEvent(contentX, contentY, false); @@ -5278,7 +5373,9 @@ public class WebView extends AbsoluteLayout private void doDrag(int deltaX, int deltaY) { if ((deltaX | deltaY) != 0) { - scrollBy(deltaX, deltaY); + overscrollBy(deltaX, deltaY, mScrollX, mScrollY, + computeMaxScrollX(), computeMaxScrollY(), + getViewWidth() / 3, getViewHeight() / 3, true); } if (!getSettings().getBuiltInZoomControls()) { boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; @@ -5626,17 +5723,17 @@ public class WebView extends AbsoluteLayout } private int computeMaxScrollX() { - return Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); + return Math.max(computeRealHorizontalScrollRange() - getViewWidth(), 0); } private int computeMaxScrollY() { - return Math.max(computeVerticalScrollRange() + getTitleHeight() + return Math.max(computeRealVerticalScrollRange() + getTitleHeight() - getViewHeightWithTitle(), 0); } public void flingScroll(int vx, int vy) { mScroller.fling(mScrollX, mScrollY, vx, vy, 0, computeMaxScrollX(), 0, - computeMaxScrollY()); + computeMaxScrollY(), getViewWidth() / 3, getViewHeight() / 3); invalidate(); } @@ -5666,6 +5763,10 @@ public class WebView extends AbsoluteLayout if ((maxX == 0 && vy == 0) || (maxY == 0 && vx == 0)) { WebViewCore.resumePriority(); WebViewCore.resumeUpdatePicture(mWebViewCore); + if (mScroller.springback(mScrollX, mScrollY, 0, computeMaxScrollX(), + 0, computeMaxScrollY())) { + invalidate(); + } return; } float currentVelocity = mScroller.getCurrVelocity(); @@ -5693,7 +5794,9 @@ public class WebView extends AbsoluteLayout mLastVelY = vy; mLastVelocity = (float) Math.hypot(vx, vy); - mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY); + // no horizontal overscroll if the content just fits + mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY, + maxX == 0 ? 0 : getViewWidth() / 3, getViewHeight() / 3); // TODO: duration is calculated based on velocity, if the range is // small, the animation will stop before duration is up. We may // want to calculate how long the animation is going to run to precisely @@ -6747,6 +6850,10 @@ public class WebView extends AbsoluteLayout case MotionEvent.ACTION_CANCEL: if (mDeferTouchMode == TOUCH_DRAG_MODE) { // no fling in defer process + mScroller.springback(mScrollX, mScrollY, 0, + computeMaxScrollX(), 0, + computeMaxScrollY()); + invalidate(); WebViewCore.resumePriority(); WebViewCore.resumeUpdatePicture(mWebViewCore); } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index c970ae6..db50ca1 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -19,6 +19,7 @@ package android.widget; import com.android.internal.R; import android.content.Context; +import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; @@ -33,6 +34,7 @@ import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; +import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -44,7 +46,7 @@ import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.view.ContextMenu.ContextMenuInfo; +import android.view.animation.AnimationUtils; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -128,6 +130,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te 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; @@ -369,6 +382,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private ContextMenuInfo mContextMenuInfo = null; /** + * Maximum distance to record overscroll + */ + int mOverscrollMax; + + /** + * Content height divided by this is the overscroll limit. + */ + static final int OVERSCROLL_LIMIT_DIVISOR = 3; + + /** * Used to request a layout when we changed touch mode */ private static final int TOUCH_MODE_UNKNOWN = -1; @@ -461,6 +484,29 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private static final int INVALID_POINTER = -1; /** + * Maximum distance to overscroll by during edge effects + */ + int mOverscrollDistance; + + /** + * Maximum distance to overfling during edge effects + */ + int mOverflingDistance; + + // These two EdgeGlows are always set and used together. + // Checking one for null is as good as checking both. + + /** + * Tracks the state of the top edge glow. + */ + private EdgeGlow mEdgeGlowTop; + + /** + * Tracks the state of the bottom edge glow. + */ + private EdgeGlow mEdgeGlowBottom; + + /** * Interface definition for a callback to be invoked when the list or grid * has been scrolled. */ @@ -575,9 +621,29 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mOverscrollDistance = configuration.getScaledOverscrollDistance(); + mOverflingDistance = configuration.getScaledOverflingDistance(); + mDensityScale = getContext().getResources().getDisplayMetrics().density; } + @Override + public void setOverscrollMode(int mode) { + if (mode != OVERSCROLL_NEVER) { + if (mEdgeGlowTop == null) { + final Resources res = getContext().getResources(); + final Drawable edge = res.getDrawable(R.drawable.edge_light); + final Drawable glow = res.getDrawable(R.drawable.overscroll_glow); + mEdgeGlowTop = new EdgeGlow(edge, glow); + mEdgeGlowBottom = new EdgeGlow(edge, glow); + } + } else { + mEdgeGlowTop = null; + mEdgeGlowBottom = null; + } + super.setOverscrollMode(mode); + } + /** * Enables fast scrolling by letting the user quickly scroll through lists by * dragging the fast scroll thumb. The adapter attached to the list may want @@ -1074,6 +1140,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te int result; if (mSmoothScrollbarEnabled) { result = Math.max(mItemCount * 100, 0); + if (mScrollY != 0) { + // Compensate for overscroll + result += Math.abs((int) ((float) mScrollY / getHeight() * mItemCount * 100)); + } } else { result = mItemCount; } @@ -1146,6 +1216,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te layoutChildren(); mInLayout = false; + + mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; } /** @@ -1921,9 +1993,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // Check if we have moved far enough that it looks more like a // scroll than a tap final int distance = Math.abs(deltaY); - if (distance > mTouchSlop) { + final boolean overscroll = mScrollY != 0; + if (overscroll || distance > mTouchSlop) { createScrollingCache(); - mTouchMode = TOUCH_MODE_SCROLL; + mTouchMode = overscroll ? TOUCH_MODE_OVERSCROLL : TOUCH_MODE_SCROLL; mMotionCorrection = deltaY; final Handler handler = getHandler(); // Handler should not be null unless the AbsListView is not attached to a @@ -1959,6 +2032,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // touch mode). Force an initial layout to get rid of the selection. layoutChildren(); } + } else { + int touchMode = mTouchMode; + if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) { + if (mFlingRunnable != null) { + mFlingRunnable.endFling(); + } + + if (mScrollY != 0) { + mScrollY = 0; + invalidate(); + } + } } } @@ -1989,49 +2074,63 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { - mActivePointerId = ev.getPointerId(0); - final int x = (int) ev.getX(); - final int y = (int) ev.getY(); - 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; - } + switch (mTouchMode) { + case TOUCH_MODE_OVERFLING: { + mFlingRunnable.endFling(); + mTouchMode = TOUCH_MODE_OVERSCROLL; + mMotionY = mLastY = (int) ev.getY(); + mMotionCorrection = 0; + mActivePointerId = ev.getPointerId(0); + break; + } - if (mTouchMode == TOUCH_MODE_FLING) { - // Stopped a fling. It is a scroll. - createScrollingCache(); - mTouchMode = TOUCH_MODE_SCROLL; - mMotionCorrection = 0; - motionPosition = findMotionRow(y); - reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); + default: { + mActivePointerId = ev.getPointerId(0); + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + 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; + } + + if (mTouchMode == TOUCH_MODE_FLING) { + // 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(); + 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; + } } - mMotionX = x; - mMotionY = y; - mMotionPosition = motionPosition; - mLastY = Integer.MIN_VALUE; break; } @@ -2064,9 +2163,25 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te requestDisallowInterceptTouchEvent(true); } + final int rawDeltaY = deltaY; deltaY -= mMotionCorrection; int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY; + final int motionIndex; + if (mMotionPosition >= 0) { + motionIndex = mMotionPosition - mFirstPosition; + } else { + // If we don't have a motion position that we can reliably track, + // pick something in the middle to make a best guess at things below. + motionIndex = getChildCount() / 2; + } + + int motionViewPrevTop = 0; + View motionView = this.getChildAt(motionIndex); + if (motionView != null) { + motionViewPrevTop = motionView.getTop(); + } + // No need to do all this work if we're not going to move anyway boolean atEdge = false; if (incrementalDeltaY != 0) { @@ -2074,23 +2189,92 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } // Check to see if we have bumped into the scroll limit - if (atEdge && getChildCount() > 0) { - // Treat this like we're starting a new scroll from the current - // position. This will let the user start scrolling back into - // content immediately rather than needing to scroll back to the - // point where they hit the limit first. - int motionPosition = findMotionRow(y); - if (motionPosition >= 0) { - final View motionView = getChildAt(motionPosition - mFirstPosition); - mMotionViewOriginalTop = motionView.getTop(); + motionView = this.getChildAt(motionIndex); + if (motionView != null) { + // Check if the top of the motion view is where it is + // supposed to be + final int motionViewRealTop = motionView.getTop(); + if (atEdge) { + // Apply overscroll + + int overscroll = -incrementalDeltaY - + (motionViewRealTop - motionViewPrevTop); + overscrollBy(0, overscroll, 0, mScrollY, 0, 0, + 0, mOverscrollDistance, true); + if (Math.abs(mOverscrollDistance) == Math.abs(mScrollY)) { + // Don't allow overfling if we're at the edge. + mVelocityTracker.clear(); + } + mTouchMode = TOUCH_MODE_OVERSCROLL; + if (mEdgeGlowTop != null) { + if (rawDeltaY > 0) { + mEdgeGlowTop.onPull((float) overscroll / getHeight()); + } else if (rawDeltaY < 0) { + mEdgeGlowBottom.onPull((float) overscroll / getHeight()); + } + } } mMotionY = y; - mMotionPosition = motionPosition; invalidate(); } mLastY = y; } break; + + case TOUCH_MODE_OVERSCROLL: + if (y != mLastY) { + final int rawDeltaY = deltaY; + 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) { + 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 = findClosestMotionRow(y); + + mMotionCorrection = 0; + motionView = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = motionView.getTop(); + mMotionY = y; + mMotionPosition = motionPosition; + } + } else { + overscrollBy(0, -incrementalDeltaY, 0, mScrollY, 0, 0, + 0, mOverscrollDistance, true); + if (mEdgeGlowTop != null) { + if (rawDeltaY > 0) { + mEdgeGlowTop.onPull((float) -incrementalDeltaY / getHeight()); + } else if (rawDeltaY < 0) { + mEdgeGlowBottom.onPull((float) -incrementalDeltaY / getHeight()); + } + invalidate(); + } + if (Math.abs(mOverscrollDistance) == Math.abs(mScrollY)) { + // Don't allow overfling if we're at the edge. + mVelocityTracker.clear(); + } + } + mLastY = y; + } + break; } break; @@ -2162,18 +2346,29 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te case TOUCH_MODE_SCROLL: final int childCount = getChildCount(); if (childCount > 0) { - if (mFirstPosition == 0 && getChildAt(0).getTop() >= mListPadding.top && + final int firstChildTop = getChildAt(0).getTop(); + final int lastChildBottom = getChildAt(childCount - 1).getBottom(); + final int contentTop = mListPadding.top; + final int contentBottom = getHeight() - mListPadding.bottom; + if (mFirstPosition == 0 && firstChildTop >= contentTop && mFirstPosition + childCount < mItemCount && - getChildAt(childCount - 1).getBottom() <= - getHeight() - mListPadding.bottom) { + lastChildBottom <= getHeight() - contentBottom) { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); - - if (Math.abs(initialVelocity) > mMinimumVelocity) { + + // Fling if we have enough velocity and we aren't at a boundary. + // Since we can potentially overfling more than we can overscroll, don't + // allow the weird behavior where you can scroll to a boundary then + // fling further. + if (Math.abs(initialVelocity) > mMinimumVelocity && + !((mFirstPosition == 0 && + firstChildTop == contentTop - mOverscrollDistance) || + (mFirstPosition + childCount == mItemCount && + lastChildBottom == contentBottom + mOverscrollDistance))) { if (mFlingRunnable == null) { mFlingRunnable = new FlingRunnable(); } @@ -2190,10 +2385,32 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te 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(mActivePointerId); + + reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + if (Math.abs(initialVelocity) > mMinimumVelocity) { + mFlingRunnable.startOverfling(-initialVelocity); + } else { + mFlingRunnable.startSpringback(); + } + + break; } setPressed(false); + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } + // Need to redraw since we probably aren't drawing the selector anymore invalidate(); @@ -2219,24 +2436,42 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } 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; - final Handler handler = getHandler(); - if (handler != null) { - handler.removeCallbacks(mPendingCheckForLongPress); - } + case TOUCH_MODE_OVERFLING: + // Do nothing - let it play out. + break; - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; + 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); + } + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } } + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } mActivePointerId = INVALID_POINTER; break; } @@ -2261,10 +2496,60 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } @Override + protected void onOverscrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + mScrollY = scrollY; + + if (clampedY) { + // Velocity is broken by hitting the limit; don't start a fling off of this. + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + awakenScrollBars(); + } + + @Override public void draw(Canvas canvas) { super.draw(canvas); + if (mEdgeGlowTop != null) { + final int scrollY = mScrollY; + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + + canvas.translate(-width / 2, scrollY); + mEdgeGlowTop.setSize(width * 2, getHeight()); + if (mEdgeGlowTop.draw(canvas)) { + invalidate(); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight(); + + canvas.translate(-width / 2, scrollY + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width * 2, height); + if (mEdgeGlowBottom.draw(canvas)) { + invalidate(); + } + canvas.restoreToCount(restoreCount); + } + } if (mFastScroller != null) { - mFastScroller.draw(canvas); + final int scrollY = mScrollY; + if (scrollY != 0) { + // Pin to the top/bottom during overscroll + int restoreCount = canvas.save(); + canvas.translate(0, (float) scrollY); + mFastScroller.draw(canvas); + canvas.restoreToCount(restoreCount); + } else { + mFastScroller.draw(canvas); + } } } @@ -2283,6 +2568,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { int touchMode = mTouchMode; + if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) { + mMotionCorrection = 0; + return true; + } final int x = (int) ev.getX(); final int y = (int) ev.getY(); @@ -2347,6 +2636,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mMotionX = (int) ev.getX(newPointerIndex); mMotionY = (int) ev.getY(newPointerIndex); + mMotionCorrection = 0; mActivePointerId = ev.getPointerId(newPointerIndex); if (mVelocityTracker != null) { mVelocityTracker.clear(); @@ -2402,7 +2692,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te /** * Tracks the decay of a fling scroll */ - private final Scroller mScroller; + private final OverScroller mScroller; /** * Y value reported by mScroller on the previous fling @@ -2410,7 +2700,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private int mLastFlingY; FlingRunnable() { - mScroller = new Scroller(getContext()); + mScroller = new OverScroller(getContext()); } void start(int initialVelocity) { @@ -2429,6 +2719,40 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } + void startSpringback() { + if (mScroller.springback(0, mScrollY, 0, 0, 0, 0)) { + mTouchMode = TOUCH_MODE_OVERFLING; + invalidate(); + post(this); + } else { + mTouchMode = TOUCH_MODE_REST; + } + } + + void startOverfling(int initialVelocity) { + final int min = mScrollY > 0 ? Integer.MIN_VALUE : 0; + final int max = mScrollY > 0 ? 0 : Integer.MAX_VALUE; + mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0, min, max, 0, getHeight()); + mTouchMode = TOUCH_MODE_OVERFLING; + invalidate(); + post(this); + } + + void edgeReached(int delta) { + mScroller.notifyVerticalEdgeReached(mScrollY, 0, mOverflingDistance); + mTouchMode = TOUCH_MODE_OVERFLING; + if (mEdgeGlowTop != null) { + final int vel = (int) mScroller.getCurrVelocity(); + if (delta > 0) { + mEdgeGlowTop.onAbsorb(vel); + } else { + mEdgeGlowBottom.onAbsorb(vel); + } + } + invalidate(); + post(this); + } + void startScroll(int distance, int duration) { int initialY = distance < 0 ? Integer.MAX_VALUE : 0; mLastFlingY = initialY; @@ -2461,7 +2785,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return; } - final Scroller scroller = mScroller; + final OverScroller scroller = mScroller; boolean more = scroller.computeScrollOffset(); final int y = scroller.getCurrY(); @@ -2490,7 +2814,24 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta); } + // Check to see if we have bumped into the scroll limit + View motionView = getChildAt(mMotionPosition - mFirstPosition); + int oldTop = 0; + if (motionView != null) { + oldTop = motionView.getTop(); + } + final boolean atEnd = trackMotionScroll(delta, delta); + if (atEnd) { + if (motionView != null) { + // Tweak the scroll for how far we overshot + int overshoot = -(delta - (motionView.getTop() - oldTop)); + overscrollBy(0, overshoot, 0, mScrollY, 0, 0, + 0, mOverflingDistance, false); + } + edgeReached(delta); + break; + } if (more && !atEnd) { invalidate(); @@ -2508,6 +2849,24 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } break; } + + case TOUCH_MODE_OVERFLING: { + final OverScroller scroller = mScroller; + if (scroller.computeScrollOffset()) { + final int scrollY = mScrollY; + final int deltaY = scroller.getCurrY() - scrollY; + if (overscrollBy(0, deltaY, 0, scrollY, 0, 0, + 0, mOverflingDistance, false)) { + startSpringback(); + } else { + invalidate(); + post(this); + } + } else { + endFling(); + } + break; + } } } @@ -2864,16 +3223,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final int firstPosition = mFirstPosition; - if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) { + if (firstPosition == 0 && firstTop >= listPadding.top && incrementalDeltaY >= 0) { // Don't need to move views down if the top of the first position // is already visible - return true; + return incrementalDeltaY != 0; } - if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) { + if (firstPosition + childCount == mItemCount && lastBottom <= end && + incrementalDeltaY <= 0) { // Don't need to move views up if the bottom of the last position // is already visible - return true; + return incrementalDeltaY != 0; } final boolean down = incrementalDeltaY < 0; diff --git a/core/java/android/widget/EdgeGlow.java b/core/java/android/widget/EdgeGlow.java new file mode 100644 index 0000000..e2c7bca --- /dev/null +++ b/core/java/android/widget/EdgeGlow.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2010 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.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +/** + * This class performs the glow effect used at the edges of scrollable widgets. + * @hide + */ +public class EdgeGlow { + private static final String TAG = "EdgeGlow"; + + private static final boolean DEBUG = false; + + // Time it will take the effect to fully recede in ms + private static final int RECEDE_TIME = 1000; + + // Time it will take before a pulled glow begins receding + private static final int PULL_TIME = 250; + + // Time it will take for a pulled glow to decay to partial strength before release + private static final int PULL_DECAY_TIME = 1000; + + private static final float HELD_EDGE_ALPHA = 0.7f; + private static final float HELD_EDGE_SCALE_Y = 0.5f; + private static final float HELD_GLOW_ALPHA = 0.5f; + private static final float HELD_GLOW_SCALE_Y = 0.5f; + + private static final float PULL_GLOW_BEGIN = 0.5f; + + // Minimum velocity that will be absorbed + private static final int MIN_VELOCITY = 750; + + private static final float EPSILON = 0.001f; + + private Drawable mEdge; + private Drawable mGlow; + private int mWidth; + private int mHeight; + + private float mEdgeAlpha; + private float mEdgeScaleY; + private float mGlowAlpha; + private float mGlowScaleY; + + private float mEdgeAlphaStart; + private float mEdgeAlphaFinish; + private float mEdgeScaleYStart; + private float mEdgeScaleYFinish; + private float mGlowAlphaStart; + private float mGlowAlphaFinish; + private float mGlowScaleYStart; + private float mGlowScaleYFinish; + + private long mStartTime; + private int mDuration; + + private Interpolator mInterpolator; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RECEDE = 3; + private static final int STATE_PULL_DECAY = 4; + + private int mState = STATE_IDLE; + + private float mPullDistance; + + public EdgeGlow(Drawable edge, Drawable glow) { + mEdge = edge; + mGlow = glow; + + mInterpolator = new DecelerateInterpolator(); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + public boolean isFinished() { + return mState == STATE_IDLE; + } + + /** + * Call when the object is pulled by the user. + * @param deltaDistance Change in distance since the last call + */ + public void onPull(float deltaDistance) { + final long now = AnimationUtils.currentAnimationTimeMillis(); + if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { + return; + } + if (mState != STATE_PULL) { + mGlowScaleY = PULL_GLOW_BEGIN; + } + mState = STATE_PULL; + + mStartTime = now; + mDuration = PULL_TIME; + + mPullDistance += deltaDistance; + float distance = Math.abs(mPullDistance); + + mEdgeAlpha = mEdgeAlphaStart = Math.max(HELD_EDGE_ALPHA, Math.min(distance, 1.f)); + mEdgeScaleY = mEdgeScaleYStart = Math.max(HELD_EDGE_SCALE_Y, Math.min(distance, 2.f)); + + mGlowAlpha = mGlowAlphaStart = Math.max(0.5f, + Math.min(mGlowAlpha + Math.abs(deltaDistance), 1.f)); + + float glowChange = Math.abs(deltaDistance); + if (deltaDistance > 0 && mPullDistance < 0) { + glowChange = -glowChange; + } + if (mPullDistance == 0) { + mGlowScaleY = 0; + } + mGlowScaleY = mGlowScaleYStart = Math.max(0, mGlowScaleY + glowChange * 2); + + mEdgeAlphaFinish = mEdgeAlpha; + mEdgeScaleYFinish = mEdgeScaleY; + mGlowAlphaFinish = mGlowAlpha; + mGlowScaleYFinish = mGlowScaleY; + + if (DEBUG) Log.d(TAG, "onPull(" + distance + ", " + deltaDistance + ")"); + } + + /** + * Call when the object is released after being pulled. + */ + public void onRelease() { + mPullDistance = 0; + + if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { + return; + } + if (DEBUG) Log.d(TAG, "onRelease"); + + mState = STATE_RECEDE; + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.1f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.1f; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * @param velocity Velocity at impact in pixels per second. + */ + public void onAbsorb(int velocity) { + mState = STATE_ABSORB; + if (DEBUG) Log.d(TAG, "onAbsorb uncooked velocity: " + velocity); + velocity = Math.max(MIN_VELOCITY, Math.abs(velocity)); + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = (int) (velocity * 0.03f); + + mEdgeAlphaStart = 0.5f; + mEdgeScaleYStart = 0.2f; + mGlowAlphaStart = 0.5f; + mGlowScaleYStart = 0.f; + + mEdgeAlphaFinish = Math.max(0, Math.min(velocity * 0.01f, 1)); + mEdgeScaleYFinish = 1.f; + mGlowAlphaFinish = 1.f; + mGlowScaleYFinish = Math.min(velocity * 0.001f, 1); + + if (DEBUG) Log.d(TAG, "onAbsorb(" + velocity + "): duration " + mDuration); + } + + /** + * Draw into the provided canvas. + * Assumes that the canvas has been rotated accordingly and the size has been set. + * The effect will be drawn the full width of X=0 to X=width, emitting from Y=0 and extending + * to some factor < 1.f of height. + * + * @param canvas Canvas to draw into + * @return true if drawing should continue beyond this frame to continue the animation + */ + public boolean draw(Canvas canvas) { + update(); + + final int edgeHeight = mEdge.getIntrinsicHeight(); + final int glowHeight = mGlow.getIntrinsicHeight(); + + mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255)); + mGlow.setBounds(0, 0, mWidth, (int) (glowHeight * mGlowScaleY)); + mGlow.draw(canvas); + + mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255)); + mEdge.setBounds(0, + 0, + mWidth, + (int) (edgeHeight * mEdgeScaleY)); + mEdge.draw(canvas); + if (DEBUG) Log.d(TAG, "draw() glow(" + mGlowAlpha + ", " + mGlowScaleY + ") edge(" + mEdgeAlpha + + ", " + mEdgeScaleY + ")"); + + return mState != STATE_IDLE; + } + + private void update() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final float t = Math.min((float) (time - mStartTime) / mDuration, 1.f); + + final float interp = mInterpolator.getInterpolation(t); + + mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp; + mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp; + mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; + mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; + + if (t >= 1.f - EPSILON) { + switch (mState) { + case STATE_ABSORB: + mState = STATE_RECEDE; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.1f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = mGlowScaleY; + if (DEBUG) Log.d(TAG, "STATE_ABSORB => STATE_RECEDE"); + break; + case STATE_PULL: + mState = STATE_PULL_DECAY; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = PULL_DECAY_TIME; + + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mEdgeAlphaFinish = Math.min(mEdgeAlphaStart, HELD_EDGE_ALPHA); + mEdgeScaleYFinish = Math.min(mEdgeScaleYStart, HELD_EDGE_SCALE_Y); + mGlowAlphaFinish = Math.min(mGlowAlphaStart, HELD_GLOW_ALPHA); + mGlowScaleYFinish = Math.min(mGlowScaleY, HELD_GLOW_SCALE_Y); + if (DEBUG) Log.d(TAG, "STATE_PULL => STATE_PULL_DECAY"); + break; + case STATE_PULL_DECAY: + // Do nothing; wait for release + break; + case STATE_RECEDE: + mState = STATE_IDLE; + if (DEBUG) Log.d(TAG, "STATE_RECEDE => STATE_IDLE"); + break; + } + } + } +} diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 2f86d75..a7300c2 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -1876,7 +1876,12 @@ public class GridView extends AbsListView { // TODO: Account for vertical spacing too final int numColumns = mNumColumns; final int rowCount = (mItemCount + numColumns - 1) / numColumns; - return Math.max(rowCount * 100, 0); + int result = Math.max(rowCount * 100, 0); + if (mScrollY != 0) { + // Compensate for overscroll + result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100)); + } + return result; } } diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 32a9146..3493f49 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -16,19 +16,24 @@ package android.widget; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Rect; +import com.android.internal.R; + import android.util.AttributeSet; -import android.view.FocusFinder; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.VelocityTracker; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.view.View; +import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.KeyEvent; +import android.view.FocusFinder; +import android.view.MotionEvent; import android.view.ViewParent; import android.view.animation.AnimationUtils; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; import java.util.List; @@ -63,7 +68,9 @@ public class HorizontalScrollView extends FrameLayout { private long mLastScroll; private final Rect mTempRect = new Rect(); - private Scroller mScroller; + private OverScroller mScroller; + private EdgeGlow mEdgeGlowLeft; + private EdgeGlow mEdgeGlowRight; /** * Flag to indicate that we are moving focus ourselves. This is so the @@ -117,6 +124,9 @@ public class HorizontalScrollView extends FrameLayout { private int mMinimumVelocity; private int mMaximumVelocity; + private int mOverscrollDistance; + private int mOverflingDistance; + /** * ID of the active pointer. This is used to retain consistency during * drags/flings if multiple pointers are used. @@ -189,7 +199,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); @@ -197,6 +207,8 @@ public class HorizontalScrollView extends FrameLayout { mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mOverscrollDistance = configuration.getScaledOverscrollDistance(); + mOverflingDistance = configuration.getScaledOverflingDistance(); } @Override @@ -456,6 +468,9 @@ public class HorizontalScrollView extends FrameLayout { /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; + if (mScroller.springback(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) { + invalidate(); + } break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); @@ -513,7 +528,22 @@ public class HorizontalScrollView extends FrameLayout { final int deltaX = (int) (mLastMotionX - x); mLastMotionX = x; - scrollBy(deltaX, 0); + final int oldX = mScrollX; + final int oldY = mScrollY; + final int range = getScrollRange(); + if (overscrollBy(deltaX, 0, mScrollX, 0, range, 0, + mOverscrollDistance, 0, true)) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + onScrollChanged(mScrollX, mScrollY, oldX, oldY); + + final int pulledToX = oldX + deltaX; + if (pulledToX < 0) { + mEdgeGlowLeft.onPull((float) deltaX / getWidth()); + } else if (pulledToX > range) { + mEdgeGlowRight.onPull((float) deltaX / getWidth()); + } } break; case MotionEvent.ACTION_UP: @@ -522,8 +552,15 @@ public class HorizontalScrollView extends FrameLayout { velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); - if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) { - fling(-initialVelocity); + if (getChildCount() > 0) { + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + fling(-initialVelocity); + } else { + final int right = getScrollRange(); + if (mScroller.springback(mScrollX, mScrollY, 0, right, 0, 0)) { + invalidate(); + } + } } mActivePointerId = INVALID_POINTER; @@ -533,16 +570,27 @@ public class HorizontalScrollView extends FrameLayout { mVelocityTracker.recycle(); mVelocityTracker = null; } + if (mEdgeGlowLeft != null) { + mEdgeGlowLeft.onRelease(); + mEdgeGlowRight.onRelease(); + } } break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged && getChildCount() > 0) { + if (mScroller.springback(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) { + invalidate(); + } mActivePointerId = INVALID_POINTER; mIsBeingDragged = false; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } + if (mEdgeGlowLeft != null) { + mEdgeGlowLeft.onRelease(); + mEdgeGlowRight.onRelease(); + } } break; case MotionEvent.ACTION_POINTER_UP: @@ -569,6 +617,22 @@ public class HorizontalScrollView extends FrameLayout { } } + @Override + protected void onOverscrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + // Treat animating scrolls differently; see #computeScroll() for why. + if (!mScroller.isFinished()) { + mScrollX = scrollX; + mScrollY = scrollY; + if (clampedX) { + mScroller.springback(mScrollX, mScrollY, 0, getScrollRange(), 0, 0); + } + } else { + super.scrollTo(scrollX, scrollY); + } + awakenScrollBars(); + } + private int getScrollRange() { int scrollRange = 0; if (getChildCount() > 0) { @@ -951,7 +1015,16 @@ public class HorizontalScrollView extends FrameLayout { return contentWidth; } - return getChildAt(0).getRight(); + int scrollRange = getChildAt(0).getRight(); + final int scrollX = mScrollX; + final int overscrollRight = Math.max(0, scrollRange - contentWidth); + if (scrollX < 0) { + scrollRange -= scrollX; + } else if (scrollX > overscrollRight) { + scrollRange += scrollX - overscrollRight; + } + + return scrollRange; } @Override @@ -1012,14 +1085,16 @@ public class HorizontalScrollView extends FrameLayout { int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); - if (getChildCount() > 0) { - View child = getChildAt(0); - x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth()); - y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight()); - if (x != oldX || y != oldY) { - mScrollX = x; - mScrollY = y; - onScrollChanged(x, y, oldX, oldY); + if (oldX != x || oldY != y) { + overscrollBy(x - oldX, y - oldY, oldX, oldY, getScrollRange(), 0, + mOverflingDistance, 0, false); + onScrollChanged(mScrollX, mScrollY, oldX, oldY); + + final int range = getScrollRange(); + if (x < 0 && oldX >= 0) { + mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (x > range && oldX <= range) { + mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); } } awakenScrollBars(); @@ -1256,7 +1331,7 @@ public class HorizontalScrollView extends FrameLayout { int right = getChildAt(0).getWidth(); mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0, - Math.max(0, right - width), 0, 0); + Math.max(0, right - width), 0, 0, width/2, 0); final boolean movingRight = velocityX > 0; @@ -1294,6 +1369,56 @@ public class HorizontalScrollView extends FrameLayout { } } + @Override + public void setOverscrollMode(int mode) { + if (mode != OVERSCROLL_NEVER) { + if (mEdgeGlowLeft == null) { + final Resources res = getContext().getResources(); + final Drawable edge = res.getDrawable(R.drawable.edge_light); + final Drawable glow = res.getDrawable(R.drawable.overscroll_glow); + mEdgeGlowLeft = new EdgeGlow(edge, glow); + mEdgeGlowRight = new EdgeGlow(edge, glow); + } + } else { + mEdgeGlowLeft = null; + mEdgeGlowRight = null; + } + super.setOverscrollMode(mode); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mEdgeGlowLeft != null) { + final int scrollX = mScrollX; + if (!mEdgeGlowLeft.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight(); + + canvas.rotate(270); + canvas.translate(-height * 1.5f, scrollX); + mEdgeGlowLeft.setSize(getHeight() * 2, getWidth()); + if (mEdgeGlowLeft.draw(canvas)) { + invalidate(); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowRight.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight(); + + canvas.rotate(90); + canvas.translate(-height / 2, -scrollX - width); + mEdgeGlowRight.setSize(height * 2, width); + if (mEdgeGlowRight.draw(canvas)) { + invalidate(); + } + canvas.restoreToCount(restoreCount); + } + } + } + private int clamp(int n, int my, int child) { if (my >= child || n < 0) { return 0; diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index a3f8624..46cd45a 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -121,6 +121,9 @@ public class ListView extends AbsListView { Drawable mDivider; int mDividerHeight; + Drawable mOverscrollHeader; + Drawable mOverscrollFooter; + private boolean mIsCacheColorOpaque; private boolean mDividerIsOpaque; private boolean mClipDivider; @@ -175,6 +178,16 @@ public class ListView extends AbsListView { setDivider(d); } + final Drawable osHeader = a.getDrawable(com.android.internal.R.styleable.ListView_overscrollHeader); + if (osHeader != null) { + setOverscrollHeader(osHeader); + } + + final Drawable osFooter = a.getDrawable(com.android.internal.R.styleable.ListView_overscrollFooter); + if (osFooter != null) { + setOverscrollFooter(osFooter); + } + // Use the height specified, zero being the default final int dividerHeight = a.getDimensionPixelSize( com.android.internal.R.styleable.ListView_dividerHeight, 0); @@ -2945,14 +2958,52 @@ public class ListView extends AbsListView { } super.setCacheColorHint(color); } - + + void drawOverscrollHeader(Canvas canvas, Drawable drawable, Rect bounds) { + final int height = drawable.getMinimumHeight(); + + canvas.save(); + canvas.clipRect(bounds); + + final int span = bounds.bottom - bounds.top; + if (span < height) { + bounds.top = bounds.bottom - height; + } + + drawable.setBounds(bounds); + drawable.draw(canvas); + + canvas.restore(); + } + + void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) { + final int height = drawable.getMinimumHeight(); + + canvas.save(); + canvas.clipRect(bounds); + + final int span = bounds.bottom - bounds.top; + if (span < height) { + bounds.bottom = bounds.top + height; + } + + drawable.setBounds(bounds); + drawable.draw(canvas); + + canvas.restore(); + } + @Override protected void dispatchDraw(Canvas canvas) { // Draw the dividers final int dividerHeight = mDividerHeight; + final Drawable overscrollHeader = mOverscrollHeader; + final Drawable overscrollFooter = mOverscrollFooter; + final boolean drawOverscrollHeader = overscrollHeader != null; + final boolean drawOverscrollFooter = overscrollFooter != null; final boolean drawDividers = dividerHeight > 0 && mDivider != null; - if (drawDividers) { + if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) { // Only modify the top and bottom in the loop, we set the left and right here final Rect bounds = mTempRect; bounds.left = mPaddingLeft; @@ -2983,14 +3034,28 @@ public class ListView extends AbsListView { if (!mStackFromBottom) { int bottom = 0; + // Draw top divider or header for overscroll final int scrollY = mScrollY; + if (count > 0 && scrollY < 0) { + if (drawOverscrollHeader) { + bounds.bottom = 0; + bounds.top = scrollY; + drawOverscrollHeader(canvas, overscrollHeader, bounds); + } else if (drawDividers) { + bounds.bottom = 0; + bounds.top = -dividerHeight; + drawDivider(canvas, bounds, -1); + } + } + for (int i = 0; i < count; i++) { if ((headerDividers || first + i >= headerCount) && (footerDividers || first + i < footerLimit)) { View child = getChildAt(i); bottom = child.getBottom(); // Don't draw dividers next to items that are not enabled - if (drawDividers) { + if (drawDividers && + (bottom < listBottom && !(drawOverscrollFooter && i == count - 1))) { if ((areAllItemsSelectable || (adapter.isEnabled(first + i) && (i == count - 1 || adapter.isEnabled(first + i + 1))))) { @@ -3005,13 +3070,28 @@ public class ListView extends AbsListView { } } } + + final int overFooterBottom = mBottom + mScrollY; + if (drawOverscrollFooter && first + count == itemCount && + overFooterBottom > bottom) { + bounds.top = bottom; + bounds.bottom = overFooterBottom; + drawOverscrollFooter(canvas, overscrollFooter, bounds); + } } else { int top; int listTop = mListPadding.top; final int scrollY = mScrollY; - for (int i = 0; i < count; i++) { + if (count > 0 && drawOverscrollHeader) { + bounds.top = scrollY; + bounds.bottom = getChildAt(0).getTop(); + drawOverscrollHeader(canvas, overscrollHeader, bounds); + } + + final int start = drawOverscrollHeader ? 1 : 0; + for (int i = start; i < count; i++) { if ((headerDividers || first + i >= headerCount) && (footerDividers || first + i < footerLimit)) { View child = getChildAt(i); @@ -3037,10 +3117,17 @@ public class ListView extends AbsListView { } } - if (count > 0 && scrollY > 0 && drawDividers) { - bounds.top = listBottom; - bounds.bottom = listBottom + dividerHeight; - drawDivider(canvas, bounds, -1); + if (count > 0 && scrollY > 0) { + if (drawOverscrollFooter) { + final int absListBottom = mBottom; + bounds.top = absListBottom; + bounds.bottom = absListBottom + scrollY; + drawOverscrollFooter(canvas, overscrollFooter, bounds); + } else if (drawDividers) { + bounds.top = listBottom; + bounds.bottom = listBottom + dividerHeight; + drawDivider(canvas, bounds, -1); + } } } } @@ -3149,6 +3236,45 @@ public class ListView extends AbsListView { invalidate(); } + /** + * Sets the drawable that will be drawn above all other list content. + * This area can become visible when the user overscrolls the list. + * + * @param header The drawable to use + */ + public void setOverscrollHeader(Drawable header) { + mOverscrollHeader = header; + if (mScrollY < 0) { + invalidate(); + } + } + + /** + * @return The drawable that will be drawn above all other list content + */ + public Drawable getOverscrollHeader() { + return mOverscrollHeader; + } + + /** + * Sets the drawable that will be drawn below all other list content. + * This area can become visible when the user overscrolls the list, + * or when the list's content does not fully fill the container area. + * + * @param footer The drawable to use + */ + public void setOverscrollFooter(Drawable footer) { + mOverscrollFooter = footer; + invalidate(); + } + + /** + * @return The drawable that will be drawn below all other list content + */ + public Drawable getOverscrollFooter() { + return mOverscrollFooter; + } + @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java new file mode 100644 index 0000000..93900a0 --- /dev/null +++ b/core/java/android/widget/OverScroller.java @@ -0,0 +1,849 @@ +/* + * Copyright (C) 2010 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.hardware.SensorManager; +import android.util.FloatMath; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +/** + * This class encapsulates scrolling with the ability to overshoot the bounds + * of a scrolling operation. This class is a drop-in replacement for + * {@link android.widget.Scroller} in most cases. + */ +public class OverScroller { + private int mMode; + + private MagneticOverScroller mScrollerX; + private MagneticOverScroller mScrollerY; + + private final Interpolator mInterpolator; + + private static final int DEFAULT_DURATION = 250; + private static final int SCROLL_MODE = 0; + private static final int FLING_MODE = 1; + + /** + * Creates an OverScroller with a viscous fluid scroll interpolator. + * @param context + */ + public OverScroller(Context context) { + this(context, null); + } + + /** + * Creates an OverScroller with default edge bounce coefficients. + * @param context The context of this application. + * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will + * be used. + */ + public OverScroller(Context context, Interpolator interpolator) { + this(context, interpolator, MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT, + MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT); + } + + /** + * Creates an OverScroller. + * @param context The context of this application. + * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will + * be used. + * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the + * velocity which is preserved in the bounce when the horizontal edge is reached. A null value + * means no bounce. + * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. + */ + public OverScroller(Context context, Interpolator interpolator, + float bounceCoefficientX, float bounceCoefficientY) { + mInterpolator = interpolator; + mScrollerX = new MagneticOverScroller(); + mScrollerY = new MagneticOverScroller(); + MagneticOverScroller.initializeFromContext(context); + + mScrollerX.setBounceCoefficient(bounceCoefficientX); + mScrollerY.setBounceCoefficient(bounceCoefficientY); + } + + /** + * + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + public final boolean isFinished() { + return mScrollerX.mFinished && mScrollerY.mFinished; + } + + /** + * Force the finished field to a particular value. Contrary to + * {@link #abortAnimation()}, forcing the animation to finished + * does NOT cause the scroller to move to the final x and y + * position. + * + * @param finished The new finished value. + */ + public final void forceFinished(boolean finished) { + mScrollerX.mFinished = mScrollerY.mFinished = finished; + } + + /** + * 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 mScrollerX.mCurrentPosition; + } + + /** + * 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 mScrollerY.mCurrentPosition; + } + + /** + * @hide + * Returns the current velocity. + * + * @return The original velocity less the deceleration, norm of the X and Y velocity vector. + */ + public float getCurrVelocity() { + float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity; + squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity; + return FloatMath.sqrt(squaredNorm); + } + + /** + * Returns the start X offset in the scroll. + * + * @return The start X offset as an absolute distance from the origin. + */ + public final int getStartX() { + return mScrollerX.mStart; + } + + /** + * Returns the start Y offset in the scroll. + * + * @return The start Y offset as an absolute distance from the origin. + */ + public final int getStartY() { + return mScrollerY.mStart; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final X offset as an absolute distance from the origin. + */ + public final int getFinalX() { + return mScrollerX.mFinal; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final Y offset as an absolute distance from the origin. + */ + public final int getFinalY() { + return mScrollerY.mFinal; + } + + /** + * Returns how long the scroll event will take, in milliseconds. + * + * @return The duration of the scroll in milliseconds. + * + * @hide Pending removal once nothing depends on it + * @deprecated OverScrollers don't necessarily have a fixed duration. + * This function will lie to the best of its ability. + */ + public final int getDuration() { + return Math.max(mScrollerX.mDuration, mScrollerY.mDuration); + } + + /** + * Extend the scroll animation. This allows a running animation to scroll + * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. + * + * @param extend Additional time to scroll in milliseconds. + * @see #setFinalX(int) + * @see #setFinalY(int) + * + * @hide Pending removal once nothing depends on it + * @deprecated OverScrollers don't necessarily have a fixed duration. + * Instead of setting a new final position and extending + * the duration of an existing scroll, use startScroll + * to begin a new animation. + */ + public void extendDuration(int extend) { + mScrollerX.extendDuration(extend); + mScrollerY.extendDuration(extend); + } + + /** + * Sets the final position (X) for this scroller. + * + * @param newX The new X offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalY(int) + * + * @hide Pending removal once nothing depends on it + * @deprecated OverScroller's final position may change during an animation. + * Instead of setting a new final position and extending + * the duration of an existing scroll, use startScroll + * to begin a new animation. + */ + public void setFinalX(int newX) { + mScrollerX.setFinalPosition(newX); + } + + /** + * Sets the final position (Y) for this scroller. + * + * @param newY The new Y offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalX(int) + * + * @hide Pending removal once nothing depends on it + * @deprecated OverScroller's final position may change during an animation. + * Instead of setting a new final position and extending + * the duration of an existing scroll, use startScroll + * to begin a new animation. + */ + public void setFinalY(int newY) { + mScrollerY.setFinalPosition(newY); + } + + /** + * Call this when you want to know the new location. If it returns true, the + * animation is not yet finished. + */ + public boolean computeScrollOffset() { + if (isFinished()) { + return false; + } + + switch (mMode) { + case SCROLL_MODE: + long time = AnimationUtils.currentAnimationTimeMillis(); + // Any scroller can be used for time, since they were started + // together in scroll mode. We use X here. + final long elapsedTime = time - mScrollerX.mStartTime; + + final int duration = mScrollerX.mDuration; + if (elapsedTime < duration) { + float q = (float) (elapsedTime) / duration; + + if (mInterpolator == null) + q = Scroller.viscousFluid(q); + else + q = mInterpolator.getInterpolation(q); + + mScrollerX.updateScroll(q); + mScrollerY.updateScroll(q); + } else { + abortAnimation(); + } + break; + + case FLING_MODE: + if (!mScrollerX.mFinished) { + if (!mScrollerX.update()) { + if (!mScrollerX.continueWhenFinished()) { + mScrollerX.finish(); + } + } + } + + if (!mScrollerY.mFinished) { + if (!mScrollerY.update()) { + if (!mScrollerY.continueWhenFinished()) { + mScrollerY.finish(); + } + } + } + + break; + } + + return true; + } + + /** + * 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) { + startScroll(startX, startY, dx, dy, DEFAULT_DURATION); + } + + /** + * 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) { + mMode = SCROLL_MODE; + mScrollerX.startScroll(startX, dx, duration); + mScrollerY.startScroll(startY, dy, duration); + } + + /** + * 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 and startY were + * already within the valid range. + */ + public boolean springback(int startX, int startY, int minX, int maxX, int minY, int maxY) { + mMode = FLING_MODE; + + // Make sure both methods are called. + final boolean spingbackX = mScrollerX.springback(startX, minX, maxX); + final boolean spingbackY = mScrollerY.springback(startY, minY, maxY); + return spingbackX || spingbackY; + } + + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); + } + + /** + * Start scrolling based on a fling gesture. The distance traveled 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) { + mMode = FLING_MODE; + mScrollerX.fling(startX, velocityX, minX, maxX, overX); + mScrollerY.fling(startY, velocityY, minY, maxY, overY); + } + + /** + * Notify the scroller that we've reached a horizontal boundary. + * Normally the information to handle this will already be known + * when the animation is started, such as in a call to one of the + * fling functions. However there are cases where this cannot be known + * in advance. This function will transition the current motion and + * animate from startX to finalX as appropriate. + * + * @param startX Starting/current X position + * @param finalX Desired final X position + * @param overX Magnitude of overscroll allowed. This should be the maximum + * desired distance from finalX. Absolute value - must be positive. + */ + public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { + mScrollerX.notifyEdgeReached(startX, finalX, overX); + } + + /** + * Notify the scroller that we've reached a vertical boundary. + * Normally the information to handle this will already be known + * when the animation is started, such as in a call to one of the + * fling functions. However there are cases where this cannot be known + * in advance. This function will animate a parabolic motion from + * startY to finalY. + * + * @param startY Starting/current Y position + * @param finalY Desired final Y position + * @param overY Magnitude of overscroll allowed. This should be the maximum + * desired distance from finalY. + */ + public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { + mScrollerY.notifyEdgeReached(startY, finalY, overY); + } + + /** + * Returns whether the current Scroller is currently returning to a valid position. + * Valid bounds were provided by the + * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. + * + * One should check this value before calling + * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress + * to restore a valid position will then be stopped. The caller has to take into account + * the fact that the started scroll will start from an overscrolled position. + * + * @return true when the current position is overscrolled and in the process of + * interpolating back to a valid value. + */ + public boolean isOverscrolled() { + return ((!mScrollerX.mFinished && + mScrollerX.mState != MagneticOverScroller.TO_EDGE) || + (!mScrollerY.mFinished && + mScrollerY.mState != MagneticOverScroller.TO_EDGE)); + } + + /** + * Stops the animation. Contrary to {@link #forceFinished(boolean)}, + * aborting the animating causes the scroller to move to the final x and y + * positions. + * + * @see #forceFinished(boolean) + */ + public void abortAnimation() { + mScrollerX.finish(); + mScrollerY.finish(); + } + + /** + * Returns the time elapsed since the beginning of the scrolling. + * + * @return The elapsed time in milliseconds. + * + * @hide + */ + public int timePassed() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime); + return (int) (time - startTime); + } + + static class MagneticOverScroller { + // Initial position + int mStart; + + // Current position + int mCurrentPosition; + + // Final position + int mFinal; + + // Initial velocity + int mVelocity; + + // Current velocity + float mCurrVelocity; + + // Constant current deceleration + float mDeceleration; + + // Animation starting time, in system milliseconds + long mStartTime; + + // Animation duration, in milliseconds + int mDuration; + + // Whether the animation is currently in progress + boolean mFinished; + + // Constant gravity value, used to scale deceleration + static float GRAVITY; + + static void initializeFromContext(Context context) { + final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; + GRAVITY = SensorManager.GRAVITY_EARTH // g (m/s^2) + * 39.37f // inch/meter + * ppi // pixels per inch + * ViewConfiguration.getScrollFriction(); + } + + private static final int TO_EDGE = 0; + private static final int TO_BOUNDARY = 1; + private static final int TO_BOUNCE = 2; + + private int mState = TO_EDGE; + + // The allowed overshot distance before boundary is reached. + private int mOver; + + // Duration in milliseconds to go back from edge to edge. Springback is half of it. + private static final int OVERSCROLL_SPRINGBACK_DURATION = 200; + + // Oscillation period + private static final float TIME_COEF = + 1000.0f * (float) Math.PI / OVERSCROLL_SPRINGBACK_DURATION; + + // If the velocity is smaller than this value, no bounce is triggered + // when the edge limits are reached (would result in a zero pixels + // displacement anyway). + private static final float MINIMUM_VELOCITY_FOR_BOUNCE = 140.0f; + + // Proportion of the velocity that is preserved when the edge is reached. + private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.16f; + + private float mBounceCoefficient = DEFAULT_BOUNCE_COEFFICIENT; + + MagneticOverScroller() { + mFinished = true; + } + + void updateScroll(float q) { + mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); + } + + /* + * Get a signed deceleration that will reduce the velocity. + */ + static float getDeceleration(int velocity) { + return velocity > 0 ? -GRAVITY : GRAVITY; + } + + /* + * Returns the time (in milliseconds) it will take to go from start to end. + */ + static int computeDuration(int start, int end, float initialVelocity, float deceleration) { + final int distance = start - end; + final float discriminant = initialVelocity * initialVelocity - 2.0f * deceleration + * distance; + if (discriminant >= 0.0f) { + float delta = (float) Math.sqrt(discriminant); + if (deceleration < 0.0f) { + delta = -delta; + } + return (int) (1000.0f * (-initialVelocity - delta) / deceleration); + } + + // End position can not be reached + return 0; + } + + void startScroll(int start, int distance, int duration) { + mFinished = false; + + mStart = start; + mFinal = start + distance; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = duration; + + // Unused + mDeceleration = 0.0f; + mVelocity = 0; + } + + void fling(int start, int velocity, int min, int max) { + mFinished = false; + + mStart = start; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + + mVelocity = velocity; + + mDeceleration = getDeceleration(velocity); + + // A start from an invalid position immediately brings back to a valid position + if (mStart < min) { + mDuration = 0; + mFinal = min; + return; + } + + if (mStart > max) { + mDuration = 0; + mFinal = max; + return; + } + + // Duration are expressed in milliseconds + mDuration = (int) (-1000.0f * velocity / mDeceleration); + + mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration)); + + // Clamp to a valid final position + if (mFinal < min) { + mFinal = min; + mDuration = computeDuration(mStart, min, mVelocity, mDeceleration); + } + + if (mFinal > max) { + mFinal = max; + mDuration = computeDuration(mStart, max, mVelocity, mDeceleration); + } + } + + void finish() { + mCurrentPosition = mFinal; + // Not reset since WebView relies on this value for fast fling. + // mCurrVelocity = 0.0f; + mFinished = true; + } + + void setFinalPosition(int position) { + mFinal = position; + mFinished = false; + } + + void extendDuration(int extend) { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final int elapsedTime = (int) (time - mStartTime); + mDuration = elapsedTime + extend; + mFinished = false; + } + + void setBounceCoefficient(float coefficient) { + mBounceCoefficient = coefficient; + } + + boolean springback(int start, int min, int max) { + mFinished = true; + + mStart = start; + mVelocity = 0; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = 0; + + if (start < min) { + startSpringback(start, min, false); + } else if (start > max) { + startSpringback(start, max, true); + } + + return !mFinished; + } + + private void startSpringback(int start, int end, boolean positive) { + mFinished = false; + mState = TO_BOUNCE; + mStart = mFinal = end; + mDuration = OVERSCROLL_SPRINGBACK_DURATION; + mStartTime -= OVERSCROLL_SPRINGBACK_DURATION / 2; + mVelocity = (int) (Math.abs(end - start) * TIME_COEF * (positive ? 1.0 : -1.0f)); + } + + void fling(int start, int velocity, int min, int max, int over) { + mState = TO_EDGE; + mOver = over; + + mFinished = false; + + mStart = start; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + + mVelocity = velocity; + + mDeceleration = getDeceleration(velocity); + + // Duration are expressed in milliseconds + mDuration = (int) (-1000.0f * velocity / mDeceleration); + + mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration)); + + // Clamp to a valid final position + if (mFinal < min) { + mFinal = min; + mDuration = computeDuration(mStart, min, mVelocity, mDeceleration); + } + + if (mFinal > max) { + mFinal = max; + mDuration = computeDuration(mStart, max, mVelocity, mDeceleration); + } + + if (start > max) { + if (start >= max + over) { + springback(max + over, min, max); + } else { + if (velocity <= 0) { + springback(start, min, max); + } else { + long time = AnimationUtils.currentAnimationTimeMillis(); + final double durationSinceEdge = + Math.atan((start-max) * TIME_COEF / velocity) / TIME_COEF; + mStartTime = (int) (time - 1000.0f * durationSinceEdge); + + // Simulate a bounce that started from edge + mStart = max; + + mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF)); + + onEdgeReached(); + } + } + } else { + if (start < min) { + if (start <= min - over) { + springback(min - over, min, max); + } else { + if (velocity >= 0) { + springback(start, min, max); + } else { + long time = AnimationUtils.currentAnimationTimeMillis(); + final double durationSinceEdge = + Math.atan((start-min) * TIME_COEF / velocity) / TIME_COEF; + mStartTime = (int) (time - 1000.0f * durationSinceEdge); + + // Simulate a bounce that started from edge + mStart = min; + + mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF)); + + onEdgeReached(); + } + + } + } + } + } + + void notifyEdgeReached(int start, int end, int over) { + mDeceleration = getDeceleration(mVelocity); + + // Local time, used to compute edge crossing time. + float timeCurrent = mCurrVelocity / mDeceleration; + final int distance = end - start; + float timeEdge = -(float) Math.sqrt((2.0f * distance / mDeceleration) + + (timeCurrent * timeCurrent)); + + mVelocity = (int) (mDeceleration * timeEdge); + + // Simulate a symmetric bounce that started from edge + mStart = end; + + mOver = over; + + long time = AnimationUtils.currentAnimationTimeMillis(); + mStartTime = (int) (time - 1000.0f * (timeCurrent - timeEdge)); + + onEdgeReached(); + } + + private void onEdgeReached() { + // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. + final float distance = mVelocity / TIME_COEF; + + if (Math.abs(distance) < mOver) { + // Spring force will bring us back to final position + mState = TO_BOUNCE; + mFinal = mStart; + mDuration = OVERSCROLL_SPRINGBACK_DURATION; + } else { + // Velocity is too high, we will hit the boundary limit + mState = TO_BOUNDARY; + int over = mVelocity > 0 ? mOver : -mOver; + mFinal = mStart + over; + mDuration = (int) (1000.0f * Math.asin(over / distance) / TIME_COEF); + } + } + + boolean continueWhenFinished() { + switch (mState) { + case TO_EDGE: + // Duration from start to null velocity + int duration = (int) (-1000.0f * mVelocity / mDeceleration); + if (mDuration < duration) { + // If the animation was clamped, we reached the edge + mStart = mFinal; + // Speed when edge was reached + mVelocity = (int) (mVelocity + mDeceleration * mDuration / 1000.0f); + mStartTime += mDuration; + onEdgeReached(); + } else { + // Normal stop, no need to continue + return false; + } + break; + case TO_BOUNDARY: + mStartTime += mDuration; + startSpringback(mFinal, mFinal - (mVelocity > 0 ? mOver:-mOver), mVelocity > 0); + break; + case TO_BOUNCE: + //mVelocity = (int) (mVelocity * BOUNCE_COEFFICIENT); + mVelocity = (int) (mVelocity * mBounceCoefficient); + if (Math.abs(mVelocity) < MINIMUM_VELOCITY_FOR_BOUNCE) { + return false; + } + mStartTime += mDuration; + break; + } + + update(); + return true; + } + + /* + * Update the current position and velocity for current time. Returns + * true if update has been done and false if animation duration has been + * reached. + */ + boolean update() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final long duration = time - mStartTime; + + if (duration > mDuration) { + return false; + } + + double distance; + final float t = duration / 1000.0f; + if (mState == TO_EDGE) { + mCurrVelocity = mVelocity + mDeceleration * t; + distance = mVelocity * t + mDeceleration * t * t / 2.0f; + } else { + final float d = t * TIME_COEF; + mCurrVelocity = mVelocity * (float)Math.cos(d); + distance = mVelocity / TIME_COEF * Math.sin(d); + } + + mCurrentPosition = mStart + (int) distance; + return true; + } + } +} diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 959e982..9d971f6 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -19,8 +19,11 @@ package android.widget; import com.android.internal.R; import android.content.Context; +import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Canvas; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.FocusFinder; import android.view.KeyEvent; @@ -59,7 +62,9 @@ public class ScrollView extends FrameLayout { private long mLastScroll; private final Rect mTempRect = new Rect(); - private Scroller mScroller; + private OverScroller mScroller; + private EdgeGlow mEdgeGlowTop; + private EdgeGlow mEdgeGlowBottom; /** * Flag to indicate that we are moving focus ourselves. This is so the @@ -113,6 +118,9 @@ public class ScrollView extends FrameLayout { private int mMinimumVelocity; private int mMaximumVelocity; + private int mOverscrollDistance; + private int mOverflingDistance; + /** * ID of the active pointer. This is used to retain consistency during * drags/flings if multiple pointers are used. @@ -185,7 +193,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); @@ -193,6 +201,8 @@ public class ScrollView extends FrameLayout { mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mOverscrollDistance = configuration.getScaledOverscrollDistance(); + mOverflingDistance = configuration.getScaledOverflingDistance(); } @Override @@ -453,6 +463,9 @@ public class ScrollView extends FrameLayout { /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; + if (mScroller.springback(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { + invalidate(); + } break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); @@ -510,7 +523,22 @@ public class ScrollView extends FrameLayout { final int deltaY = (int) (mLastMotionY - y); mLastMotionY = y; - scrollBy(0, deltaY); + final int oldX = mScrollX; + final int oldY = mScrollY; + final int range = getScrollRange(); + if (overscrollBy(0, deltaY, 0, mScrollY, 0, range, + 0, mOverscrollDistance, true)) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + onScrollChanged(mScrollX, mScrollY, oldX, oldY); + + final int pulledToY = oldY + deltaY; + if (pulledToY < 0) { + mEdgeGlowTop.onPull((float) deltaY / getHeight()); + } else if (pulledToY > range) { + mEdgeGlowBottom.onPull((float) deltaY / getHeight()); + } } break; case MotionEvent.ACTION_UP: @@ -519,8 +547,15 @@ public class ScrollView extends FrameLayout { velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); - if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) { - fling(-initialVelocity); + if (getChildCount() > 0) { + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + fling(-initialVelocity); + } else { + final int bottom = getScrollRange(); + if (mScroller.springback(mScrollX, mScrollY, 0, 0, 0, bottom)) { + invalidate(); + } + } } mActivePointerId = INVALID_POINTER; @@ -530,16 +565,27 @@ public class ScrollView extends FrameLayout { mVelocityTracker.recycle(); mVelocityTracker = null; } + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } } break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged && getChildCount() > 0) { + if (mScroller.springback(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { + invalidate(); + } mActivePointerId = INVALID_POINTER; mIsBeingDragged = false; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } } break; case MotionEvent.ACTION_POINTER_UP: @@ -566,6 +612,22 @@ public class ScrollView extends FrameLayout { } } + @Override + protected void onOverscrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + // Treat animating scrolls differently; see #computeScroll() for why. + if (!mScroller.isFinished()) { + mScrollX = scrollX; + mScrollY = scrollY; + if (clampedY) { + mScroller.springback(mScrollX, mScrollY, 0, 0, 0, getScrollRange()); + } + } else { + super.scrollTo(scrollX, scrollY); + } + awakenScrollBars(); + } + private int getScrollRange() { int scrollRange = 0; if (getChildCount() > 0) { @@ -952,7 +1014,16 @@ public class ScrollView extends FrameLayout { return contentHeight; } - return getChildAt(0).getBottom(); + int scrollRange = getChildAt(0).getBottom(); + final int scrollY = mScrollY; + final int overscrollBottom = Math.max(0, scrollRange - contentHeight); + if (scrollY < 0) { + scrollRange -= scrollY; + } else if (scrollY > overscrollBottom) { + scrollRange += scrollY - overscrollBottom; + } + + return scrollRange; } @Override @@ -1013,14 +1084,16 @@ public class ScrollView extends FrameLayout { int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); - if (getChildCount() > 0) { - View child = getChildAt(0); - x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth()); - y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight()); - if (x != oldX || y != oldY) { - mScrollX = x; - mScrollY = y; - onScrollChanged(x, y, oldX, oldY); + if (oldX != x || oldY != y) { + overscrollBy(x - oldX, y - oldY, oldX, oldY, 0, getScrollRange(), + 0, mOverflingDistance, false); + onScrollChanged(mScrollX, mScrollY, oldX, oldY); + + final int range = getScrollRange(); + if (y < 0 && oldY >= 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y > range && oldY <= range) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); } } awakenScrollBars(); @@ -1258,7 +1331,7 @@ public class ScrollView extends FrameLayout { int bottom = getChildAt(0).getHeight(); mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, - Math.max(0, bottom - height)); + Math.max(0, bottom - height), 0, height/2); final boolean movingDown = velocityY > 0; @@ -1296,6 +1369,55 @@ public class ScrollView extends FrameLayout { } } + @Override + public void setOverscrollMode(int mode) { + if (mode != OVERSCROLL_NEVER) { + if (mEdgeGlowTop == null) { + final Resources res = getContext().getResources(); + final Drawable edge = res.getDrawable(R.drawable.edge_light); + final Drawable glow = res.getDrawable(R.drawable.overscroll_glow); + mEdgeGlowTop = new EdgeGlow(edge, glow); + mEdgeGlowBottom = new EdgeGlow(edge, glow); + } + } else { + mEdgeGlowTop = null; + mEdgeGlowBottom = null; + } + super.setOverscrollMode(mode); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mEdgeGlowTop != null) { + final int scrollY = mScrollY; + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + + canvas.translate(-width / 2, scrollY); + mEdgeGlowTop.setSize(width * 2, getHeight()); + if (mEdgeGlowTop.draw(canvas)) { + invalidate(); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight(); + + canvas.translate(-width / 2, scrollY + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width * 2, height); + if (mEdgeGlowBottom.draw(canvas)) { + invalidate(); + } + canvas.restoreToCount(restoreCount); + } + } + } + private int clamp(int n, int my, int child) { if (my >= child || n < 0) { /* my >= child is this case: diff --git a/core/java/android/widget/Scroller.java b/core/java/android/widget/Scroller.java index 4cb0839..23f72b6 100644 --- a/core/java/android/widget/Scroller.java +++ b/core/java/android/widget/Scroller.java @@ -50,8 +50,6 @@ public class Scroller { private float mDurationReciprocal; private float mDeltaX; private float mDeltaY; - private float mViscousFluidScale; - private float mViscousFluidNormalize; private boolean mFinished; private Interpolator mInterpolator; @@ -65,6 +63,17 @@ public class Scroller { private final float mDeceleration; + private static float sViscousFluidScale; + private static float sViscousFluidNormalize; + + static { + // This controls the viscous fluid effect (how much of it) + sViscousFluidScale = 8.0f; + // must be set to 1.0 (used in viscousFluid()) + sViscousFluidNormalize = 1.0f; + sViscousFluidNormalize = 1.0f / viscousFluid(1.0f); + } + /** * Create a Scroller with the default duration and interpolator. */ @@ -277,11 +286,6 @@ public class Scroller { mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; - // This controls the viscous fluid effect (how much of it) - mViscousFluidScale = 8.0f; - // must be set to 1.0 (used in viscousFluid()) - mViscousFluidNormalize = 1.0f; - mViscousFluidNormalize = 1.0f / viscousFluid(1.0f); } /** @@ -339,11 +343,9 @@ public class Scroller { mFinalY = Math.max(mFinalY, mMinY); } - - - private float viscousFluid(float x) + static float viscousFluid(float x) { - x *= mViscousFluidScale; + x *= sViscousFluidScale; if (x < 1.0f) { x -= (1.0f - (float)Math.exp(-x)); } else { @@ -351,7 +353,7 @@ public class Scroller { x = 1.0f - (float)Math.exp(1.0f - x); x = start + x * (1.0f - start); } - x *= mViscousFluidNormalize; + x *= sViscousFluidNormalize; return x; } diff --git a/core/res/res/drawable/edge_light.png b/core/res/res/drawable/edge_light.png Binary files differnew file mode 100644 index 0000000..b026880 --- /dev/null +++ b/core/res/res/drawable/edge_light.png diff --git a/core/res/res/drawable/overscroll_glow.png b/core/res/res/drawable/overscroll_glow.png Binary files differnew file mode 100644 index 0000000..7f1831e --- /dev/null +++ b/core/res/res/drawable/overscroll_glow.png diff --git a/core/res/res/layout/alert_dialog.xml b/core/res/res/layout/alert_dialog.xml index 7ae68f9..25a41f8 100644 --- a/core/res/res/layout/alert_dialog.xml +++ b/core/res/res/layout/alert_dialog.xml @@ -80,7 +80,8 @@ android:paddingTop="2dip" android:paddingBottom="12dip" android:paddingLeft="14dip" - android:paddingRight="10dip"> + android:paddingRight="10dip" + android:overscrollMode="ifContentScrolls"> <TextView android:id="@+id/message" style="?android:attr/textAppearanceMedium" android:layout_width="match_parent" diff --git a/core/res/res/layout/preference_dialog_edittext.xml b/core/res/res/layout/preference_dialog_edittext.xml index 5be5773..b41e774 100644 --- a/core/res/res/layout/preference_dialog_edittext.xml +++ b/core/res/res/layout/preference_dialog_edittext.xml @@ -17,7 +17,8 @@ <!-- Layout used as the dialog's content View for EditTextPreference. --> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:overscrollMode="ifContentScrolls"> <LinearLayout android:id="@+android:id/edittext_container" diff --git a/core/res/res/layout/select_dialog.xml b/core/res/res/layout/select_dialog.xml index c665f7a..6e4e5e1 100644 --- a/core/res/res/layout/select_dialog.xml +++ b/core/res/res/layout/select_dialog.xml @@ -31,4 +31,5 @@ android:layout_marginTop="5px" android:cacheColorHint="@null" android:divider="@android:drawable/divider_horizontal_bright" - android:scrollbars="vertical" /> + android:scrollbars="vertical" + android:overscrollMode="ifContentScrolls" /> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index d16b91c..1130b69 100755 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -1326,6 +1326,19 @@ <code>public void sayHello(View v)</code> method of your context (typically, your Activity). --> <attr name="onClick" format="string" /> + + <!-- Defines overscrolling behavior. This property is used only if the + View is scrollable. Overscrolling is the ability for the user to + scroll a View beyond its content boundaries into empty space. --> + <attr name="overscrollMode"> + <!-- Always allow the user to overscroll the content. --> + <enum name="always" value="0" /> + <!-- Only allow the user to overscroll content if the content is large + enough to meaningfully scroll. --> + <enum name="ifContentScrolls" value="1" /> + <!-- Never overscroll. --> + <enum name="never" value="2" /> + </attr> </declare-styleable> <!-- Attributes that can be used with a {@link android.view.ViewGroup} or any @@ -1759,6 +1772,10 @@ <!-- When set to false, the ListView will not draw the divider before each footer view. The default value is true. --> <attr name="footerDividersEnabled" format="boolean" /> + <!-- Drawable to draw above list content. --> + <attr name="overscrollHeader" format="reference|color" /> + <!-- Drawable to draw below list content. --> + <attr name="overscrollFooter" format="reference|color" /> </declare-styleable> <declare-styleable name="MenuView"> <!-- Default appearance of menu item text. --> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 3af12b7..de419be 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1251,6 +1251,10 @@ <public type="attr" name="logo" id="0x010102be" /> <public type="attr" name="xlargeScreens" id="0x010102bf" /> <public type="attr" name="immersive" id="0x010102c0" /> + <public type="attr" name="overscrollMode" id="0x010102c1" /> + <public type="attr" name="overscrollHeader" id="0x010102c2" /> + <public type="attr" name="overscrollFooter" id="0x010102c3" /> + <public-padding type="attr" name="kraken_resource_pad" end="0x01010300" /> <public-padding type="id" name="kraken_resource_pad" end="0x01020040" /> @@ -1262,7 +1266,8 @@ <public type="drawable" name="presence_video_online" id="0x010800ae" /> <public type="drawable" name="presence_audio_away" id="0x010800af" /> <public type="drawable" name="presence_audio_busy" id="0x010800b0" /> - <public type="drawable" name="presence_audio_online" id="0x010800b1" /> + <public type="drawable" name="presence_audio_online" id="0x010800b1" /> + <public-padding type="drawable" name="kraken_resource_pad" end="0x01080100" /> <public-padding type="style" name="kraken_resource_pad" end="0x01030090" /> |