diff options
20 files changed, 3604 insertions, 256 deletions
diff --git a/api/current.xml b/api/current.xml index cf2fe45be615..e6ac50733f8e 100644 --- a/api/current.xml +++ b/api/current.xml @@ -6774,6 +6774,39 @@ visibility="public" > </field> +<field name="overScrollFooter" + type="int" + transient="false" + volatile="false" + value="16843459" + static="true" + final="true" + deprecated="not deprecated" + visibility="public" +> +</field> +<field name="overScrollHeader" + type="int" + transient="false" + volatile="false" + value="16843458" + static="true" + final="true" + deprecated="not deprecated" + visibility="public" +> +</field> +<field name="overScrollMode" + type="int" + transient="false" + volatile="false" + value="16843457" + static="true" + final="true" + deprecated="not deprecated" + visibility="public" +> +</field> <field name="padding" type="int" transient="false" @@ -205693,6 +205726,17 @@ visibility="public" > </method> +<method name="getOverScrollMode" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> <method name="getPaddingBottom" return="int" abstract="false" @@ -207024,6 +207068,25 @@ <parameter name="heightMeasureSpec" type="int"> </parameter> </method> +<method name="onOverScrolled" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="protected" +> +<parameter name="scrollX" type="int"> +</parameter> +<parameter name="scrollY" type="int"> +</parameter> +<parameter name="clampedX" type="boolean"> +</parameter> +<parameter name="clampedY" type="boolean"> +</parameter> +</method> <method name="onRestoreInstanceState" return="void" abstract="false" @@ -207177,6 +207240,35 @@ <parameter name="visibility" type="int"> </parameter> </method> +<method name="overScrollBy" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="protected" +> +<parameter name="deltaX" type="int"> +</parameter> +<parameter name="deltaY" type="int"> +</parameter> +<parameter name="scrollX" type="int"> +</parameter> +<parameter name="scrollY" type="int"> +</parameter> +<parameter name="scrollRangeX" type="int"> +</parameter> +<parameter name="scrollRangeY" type="int"> +</parameter> +<parameter name="maxOverScrollX" type="int"> +</parameter> +<parameter name="maxOverScrollY" type="int"> +</parameter> +<parameter name="isTouchEvent" type="boolean"> +</parameter> +</method> <method name="performClick" return="boolean" abstract="false" @@ -208094,6 +208186,19 @@ <parameter name="l" type="android.view.View.OnTouchListener"> </parameter> </method> +<method name="setOverScrollMode" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="overScrollMode" type="int"> +</parameter> +</method> <method name="setPadding" return="void" abstract="false" @@ -208928,6 +209033,39 @@ visibility="public" > </field> +<field name="OVER_SCROLL_ALWAYS" + type="int" + transient="false" + volatile="false" + value="0" + static="true" + final="true" + deprecated="not deprecated" + visibility="public" +> +</field> +<field name="OVER_SCROLL_IF_CONTENT_SCROLLS" + type="int" + transient="false" + volatile="false" + value="1" + static="true" + final="true" + deprecated="not deprecated" + visibility="public" +> +</field> +<field name="OVER_SCROLL_NEVER" + type="int" + transient="false" + volatile="false" + value="2" + static="true" + final="true" + deprecated="not deprecated" + visibility="public" +> +</field> <field name="PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET" type="int[]" transient="false" @@ -209808,6 +209946,28 @@ visibility="public" > </method> +<method name="getScaledOverflingDistance" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getScaledOverscrollDistance" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> <method name="getScaledPagingTouchSlop" return="int" abstract="false" @@ -224658,6 +224818,17 @@ visibility="public" > </method> +<method name="getUseWebViewBackgroundForOverscrollBackground" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> <method name="getUseWideViewPort" return="boolean" abstract="false" @@ -225263,6 +225434,19 @@ <parameter name="use" type="boolean"> </parameter> </method> +<method name="setUseWebViewBackgroundForOverscrollBackground" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="view" type="boolean"> +</parameter> +</method> <method name="setUseWideViewPort" return="void" abstract="false" @@ -237704,6 +237888,28 @@ visibility="public" > </method> +<method name="getOverscrollFooter" + return="android.graphics.drawable.Drawable" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getOverscrollHeader" + return="android.graphics.drawable.Drawable" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> <method name="removeFooterView" return="boolean" abstract="false" @@ -237795,6 +238001,32 @@ <parameter name="itemsCanFocus" type="boolean"> </parameter> </method> +<method name="setOverscrollFooter" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="footer" type="android.graphics.drawable.Drawable"> +</parameter> +</method> +<method name="setOverscrollHeader" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="header" type="android.graphics.drawable.Drawable"> +</parameter> +</method> <method name="setSelection" return="void" abstract="false" @@ -238346,6 +238578,334 @@ </parameter> </method> </interface> +<class name="OverScroller" + extends="java.lang.Object" + abstract="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<constructor name="OverScroller" + type="android.widget.OverScroller" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="context" type="android.content.Context"> +</parameter> +</constructor> +<constructor name="OverScroller" + type="android.widget.OverScroller" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="context" type="android.content.Context"> +</parameter> +<parameter name="interpolator" type="android.graphics.Interpolator"> +</parameter> +<parameter name="bounceCoefficientX" type="float"> +</parameter> +<parameter name="bounceCoefficientY" type="float"> +</parameter> +<parameter name="flywheel" type="boolean"> +</parameter> +</constructor> +<method name="abortAnimation" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="computeScrollOffset" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="fling" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="startX" type="int"> +</parameter> +<parameter name="startY" type="int"> +</parameter> +<parameter name="velocityX" type="int"> +</parameter> +<parameter name="velocityY" type="int"> +</parameter> +<parameter name="minX" type="int"> +</parameter> +<parameter name="maxX" type="int"> +</parameter> +<parameter name="minY" type="int"> +</parameter> +<parameter name="maxY" type="int"> +</parameter> +</method> +<method name="fling" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="startX" type="int"> +</parameter> +<parameter name="startY" type="int"> +</parameter> +<parameter name="velocityX" type="int"> +</parameter> +<parameter name="velocityY" type="int"> +</parameter> +<parameter name="minX" type="int"> +</parameter> +<parameter name="maxX" type="int"> +</parameter> +<parameter name="minY" type="int"> +</parameter> +<parameter name="maxY" type="int"> +</parameter> +<parameter name="overX" type="int"> +</parameter> +<parameter name="overY" type="int"> +</parameter> +</method> +<method name="forceFinished" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="true" + deprecated="not deprecated" + visibility="public" +> +<parameter name="finished" type="boolean"> +</parameter> +</method> +<method name="getCurrX" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="true" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getCurrY" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="true" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getFinalX" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="true" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getFinalY" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="true" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getStartX" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="true" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getStartY" + return="int" + abstract="false" + native="false" + synchronized="false" + static="false" + final="true" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="isFinished" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="true" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="isOverScrolled" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="notifyHorizontalEdgeReached" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="startX" type="int"> +</parameter> +<parameter name="finalX" type="int"> +</parameter> +<parameter name="overX" type="int"> +</parameter> +</method> +<method name="notifyVerticalEdgeReached" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="startY" type="int"> +</parameter> +<parameter name="finalY" type="int"> +</parameter> +<parameter name="overY" type="int"> +</parameter> +</method> +<method name="setFriction" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="true" + deprecated="not deprecated" + visibility="public" +> +<parameter name="friction" type="float"> +</parameter> +</method> +<method name="springBack" + return="boolean" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="startX" type="int"> +</parameter> +<parameter name="startY" type="int"> +</parameter> +<parameter name="minX" type="int"> +</parameter> +<parameter name="maxX" type="int"> +</parameter> +<parameter name="minY" type="int"> +</parameter> +<parameter name="maxY" type="int"> +</parameter> +</method> +<method name="startScroll" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="startX" type="int"> +</parameter> +<parameter name="startY" type="int"> +</parameter> +<parameter name="dx" type="int"> +</parameter> +<parameter name="dy" type="int"> +</parameter> +</method> +<method name="startScroll" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="startX" type="int"> +</parameter> +<parameter name="startY" type="int"> +</parameter> +<parameter name="dx" type="int"> +</parameter> +<parameter name="dy" type="int"> +</parameter> +<parameter name="duration" type="int"> +</parameter> +</method> +</class> <class name="PopupMenu" extends="java.lang.Object" abstract="false" diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index e6eb46e5a160..011ad773d0af 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -1627,6 +1627,40 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility static final int ACTIVATED = 0x40000000; /** + * Always allow a user to over-scroll this view, provided it is a + * view that can scroll. + * + * @see #getOverScrollMode() + * @see #setOverScrollMode(int) + */ + public static final int OVER_SCROLL_ALWAYS = 0; + + /** + * Allow a user to over-scroll 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 OVER_SCROLL_IF_CONTENT_SCROLLS = 1; + + /** + * Never allow a user to over-scroll this view. + * + * @see #getOverScrollMode() + * @see #setOverScrollMode(int) + */ + public static final int OVER_SCROLL_NEVER = 2; + + /** + * Controls the over-scroll mode for this view. + * See {@link #overScrollBy(int, int, int, int, int, int, int, int, boolean)}, + * {@link #OVER_SCROLL_ALWAYS}, {@link #OVER_SCROLL_IF_CONTENT_SCROLLS}, + * and {@link #OVER_SCROLL_NEVER}. + */ + private int mOverScrollMode; + + /** * The parent this view is attached to. * {@hide} * @@ -2057,6 +2091,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mResources = context != null ? context.getResources() : null; mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED; mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS); } /** @@ -2122,6 +2157,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); @@ -2327,9 +2363,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility }); } break; + case R.styleable.View_overScrollMode: + overScrollMode = a.getInt(attr, OVER_SCROLL_IF_CONTENT_SCROLLS); + break; } } + setOverScrollMode(overScrollMode); + if (background != null) { setBackgroundDrawable(background); } @@ -10131,6 +10172,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 over-scroll 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 over-scroll 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 == OVER_SCROLL_ALWAYS || + (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS || + (overScrollMode == OVER_SCROLL_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 over-scroll 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 over-scroll boundary + * @param clampedY True if scrollY was clamped to an over-scroll boundary + */ + protected void onOverScrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + // Intentionally empty. + } + + /** + * Returns the over-scroll mode for this view. The result will be + * one of {@link #OVER_SCROLL_ALWAYS} (default), {@link #OVER_SCROLL_IF_CONTENT_SCROLLS} + * (allow over-scrolling only if the view content is larger than the container), + * or {@link #OVER_SCROLL_NEVER}. + * + * @return This view's over-scroll mode. + */ + public int getOverScrollMode() { + return mOverScrollMode; + } + + /** + * Set the over-scroll mode for this view. Valid over-scroll modes are + * {@link #OVER_SCROLL_ALWAYS} (default), {@link #OVER_SCROLL_IF_CONTENT_SCROLLS} + * (allow over-scrolling only if the view content is larger than the container), + * or {@link #OVER_SCROLL_NEVER}. + * + * Setting the over-scroll mode of a view will have an effect only if the + * view is capable of scrolling. + * + * @param overScrollMode The new over-scroll mode for this view. + */ + public void setOverScrollMode(int overScrollMode) { + if (overScrollMode != OVER_SCROLL_ALWAYS && + overScrollMode != OVER_SCROLL_IF_CONTENT_SCROLLS && + overScrollMode != OVER_SCROLL_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 85981d2137dc..bb8589498244 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -144,7 +144,7 @@ public class ViewConfiguration { /** * Maximum velocity to initiate a fling, as measured in pixels per second */ - private static final int MAXIMUM_FLING_VELOCITY = 4000; + private static final int MAXIMUM_FLING_VELOCITY = 8000; /** * The maximum size of View's drawing cache, expressed in bytes. This size @@ -158,6 +158,16 @@ public class ViewConfiguration { */ private static float SCROLL_FRICTION = 0.015f; + /** + * Max distance to overscroll for edge effects + */ + private static final int OVERSCROLL_DISTANCE = 0; + + /** + * Max distance to overfling for edge effects + */ + private static final int OVERFLING_DISTANCE = 4; + private final int mEdgeSlop; private final int mFadingEdgeLength; private final int mMinimumFlingVelocity; @@ -168,6 +178,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); @@ -188,6 +200,8 @@ public class ViewConfiguration { mWindowTouchSlop = WINDOW_TOUCH_SLOP; //noinspection deprecation mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE; + mOverscrollDistance = OVERSCROLL_DISTANCE; + mOverflingDistance = OVERFLING_DISTANCE; } /** @@ -216,6 +230,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); } /** @@ -473,6 +490,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/OverScrollGlow.java b/core/java/android/webkit/OverScrollGlow.java new file mode 100644 index 000000000000..53600f6924e0 --- /dev/null +++ b/core/java/android/webkit/OverScrollGlow.java @@ -0,0 +1,223 @@ +/* + * 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.webkit; + +import com.android.internal.R; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.EdgeGlow; + +/** + * This class manages the edge glow effect when a WebView is flung or pulled beyond the edges. + * @hide + */ +public class OverScrollGlow { + private WebView mHostView; + + private EdgeGlow mEdgeGlowTop; + private EdgeGlow mEdgeGlowBottom; + private EdgeGlow mEdgeGlowLeft; + private EdgeGlow mEdgeGlowRight; + + private int mOverScrollDeltaX; + private int mOverScrollDeltaY; + + public OverScrollGlow(WebView host) { + mHostView = host; + final Resources res = host.getContext().getResources(); + final Drawable edge = res.getDrawable(R.drawable.overscroll_edge); + final Drawable glow = res.getDrawable(R.drawable.overscroll_glow); + mEdgeGlowTop = new EdgeGlow(edge, glow); + mEdgeGlowBottom = new EdgeGlow(edge, glow); + mEdgeGlowLeft = new EdgeGlow(edge, glow); + mEdgeGlowRight = new EdgeGlow(edge, glow); + } + + /** + * Pull leftover touch scroll distance into one of the edge glows as appropriate. + * + * @param x Current X scroll offset + * @param y Current Y scroll offset + * @param oldX Old X scroll offset + * @param oldY Old Y scroll offset + * @param maxX Maximum range for horizontal scrolling + * @param maxY Maximum range for vertical scrolling + */ + public void pullGlow(int x, int y, int oldX, int oldY, int maxX, int maxY) { + // Only show overscroll bars if there was no movement in any direction + // as a result of scrolling. + if (oldX == mHostView.getScrollX() && oldY == mHostView.getScrollY()) { + // Don't show left/right glows if we fit the whole content. + // Also don't show if there was vertical movement. + if (maxX > 0) { + final int pulledToX = oldX + mOverScrollDeltaX; + if (pulledToX < 0) { + mEdgeGlowLeft.onPull((float) mOverScrollDeltaX / mHostView.getWidth()); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onRelease(); + } + } else if (pulledToX > maxX) { + mEdgeGlowRight.onPull((float) mOverScrollDeltaX / mHostView.getWidth()); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } + mOverScrollDeltaX = 0; + } + + if (maxY > 0 || mHostView.getOverScrollMode() == View.OVER_SCROLL_ALWAYS) { + final int pulledToY = oldY + mOverScrollDeltaY; + if (pulledToY < 0) { + mEdgeGlowTop.onPull((float) mOverScrollDeltaY / mHostView.getHeight()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > maxY) { + mEdgeGlowBottom.onPull((float) mOverScrollDeltaY / mHostView.getHeight()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + mOverScrollDeltaY = 0; + } + } + } + + /** + * Set touch delta values indicating the current amount of overscroll. + * + * @param deltaX + * @param deltaY + */ + public void setOverScrollDeltas(int deltaX, int deltaY) { + mOverScrollDeltaX = deltaX; + mOverScrollDeltaY = deltaY; + } + + /** + * Absorb leftover fling velocity into one of the edge glows as appropriate. + * + * @param x Current X scroll offset + * @param y Current Y scroll offset + * @param oldX Old X scroll offset + * @param oldY Old Y scroll offset + * @param rangeX Maximum range for horizontal scrolling + * @param rangeY Maximum range for vertical scrolling + */ + public void absorbGlow(int x, int y, int oldX, int oldY, int rangeX, int rangeY) { + if (rangeY > 0 || mHostView.getOverScrollMode() == View.OVER_SCROLL_ALWAYS) { + if (y < 0 && oldY >= 0) { + mEdgeGlowTop.onAbsorb((int) mHostView.mScroller.getCurrVelocity()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (y > rangeY && oldY <= rangeY) { + mEdgeGlowBottom.onAbsorb((int) mHostView.mScroller.getCurrVelocity()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + } + + if (rangeX > 0) { + if (x < 0 && oldX >= 0) { + mEdgeGlowLeft.onAbsorb((int) mHostView.mScroller.getCurrVelocity()); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onRelease(); + } + } else if (x > rangeX && oldX <= rangeX) { + mEdgeGlowRight.onAbsorb((int) mHostView.mScroller.getCurrVelocity()); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } + } + } + + /** + * Draw the glow effect along the sides of the widget. mEdgeGlow* must be non-null. + * + * @param canvas Canvas to draw into, transformed into view coordinates. + * @return true if glow effects are still animating and the view should invalidate again. + */ + public boolean drawEdgeGlows(Canvas canvas) { + final int scrollX = mHostView.getScrollX(); + final int scrollY = mHostView.getScrollY(); + final int width = mHostView.getWidth(); + int height = mHostView.getHeight(); + + boolean invalidateForGlow = false; + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + + canvas.translate(-width / 2 + scrollX, Math.min(0, scrollY)); + mEdgeGlowTop.setSize(width * 2, height); + invalidateForGlow |= mEdgeGlowTop.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + + canvas.translate(-width / 2 + scrollX, + Math.max(mHostView.computeMaxScrollY(), scrollY) + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width * 2, height); + invalidateForGlow |= mEdgeGlowBottom.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowLeft.isFinished()) { + final int restoreCount = canvas.save(); + + canvas.rotate(270); + canvas.translate(-height * 1.5f - scrollY, Math.min(0, scrollX)); + mEdgeGlowLeft.setSize(height * 2, width); + invalidateForGlow |= mEdgeGlowLeft.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowRight.isFinished()) { + final int restoreCount = canvas.save(); + + canvas.rotate(90); + canvas.translate(-height / 2 + scrollY, + -(Math.max(mHostView.computeMaxScrollX(), scrollX) + width)); + mEdgeGlowRight.setSize(height * 2, width); + invalidateForGlow |= mEdgeGlowRight.draw(canvas); + canvas.restoreToCount(restoreCount); + } + return invalidateForGlow; + } + + /** + * @return True if any glow is still animating + */ + public boolean isAnimating() { + return (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished() || + !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished()); + } + + /** + * Release all glows from any touch pulls in progress. + */ + public void releaseAll() { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + mEdgeGlowLeft.onRelease(); + mEdgeGlowRight.onRelease(); + } +} diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 98e6ab88fd8f..2e69d995cd6d 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -268,6 +268,8 @@ public class WebSettings { private AutoFillProfile mAutoFillProfile; + private boolean mUseWebViewBackgroundForOverscroll = true; + // private WebSettings, not accessible by the host activity static private int mDoubleTapToastCount = 3; @@ -631,6 +633,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) { + mUseWebViewBackgroundForOverscroll = view; + } + + /** + * Returns true if this WebView uses WebView's background instead of + * internal pattern for over scroll background. + */ + public boolean getUseWebViewBackgroundForOverscrollBackground() { + return mUseWebViewBackgroundForOverscroll; + } + + /** * 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 376b1d099769..3cb9084fbca4 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -16,19 +16,24 @@ package android.webkit; +import com.android.internal.R; + import android.annotation.Widget; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; -import android.content.IntentFilter; import android.content.DialogInterface.OnCancelListener; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.Intent; +import android.content.res.Resources; import android.database.DataSetObserver; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.CornerPathEffect; @@ -40,6 +45,7 @@ import android.graphics.Point; import android.graphics.Rect; 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; @@ -81,13 +87,12 @@ import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.CheckedTextView; +import android.widget.EdgeGlow; import android.widget.LinearLayout; import android.widget.ListView; -import android.widget.Scroller; +import android.widget.OverScroller; import android.widget.Toast; -import junit.framework.Assert; - import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -102,6 +107,8 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import junit.framework.Assert; + /** * <p>A View that displays web pages. This class is the basis upon which you * can roll your own web browser or simply display some online content within your Activity. @@ -529,7 +536,13 @@ 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; + + // Used by OverScrollGlow + OverScroller mScroller; + + private boolean mInOverScrollMode = false; + private static Paint mOverScrollBackground; + private static Paint mOverScrollBorder; private boolean mWrapContent; private static final int MOTIONLESS_FALSE = 0; @@ -734,6 +747,20 @@ public class WebView extends AbsoluteLayout // variable to cache the above pattern in case accessibility is enabled. private Pattern mMatchAxsUrlParameterPattern; + /** + * Max distance to overscroll by in pixels. + * This how far content can be pulled beyond its normal bounds by the user. + */ + private int mOverscrollDistance; + + /** + * Max distance to overfling by in pixels. + * This is how far flinged content can move beyond the end of its normal bounds. + */ + private int mOverflingDistance; + + private OverScrollGlow mOverScrollGlow; + // Used to match key downs and key ups private boolean mGotKeyDown; @@ -909,7 +936,7 @@ public class WebView extends AbsoluteLayout L10nUtils.loadStrings(context); mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces); mDatabase = WebViewDatabase.getInstance(context); - mScroller = new Scroller(context); + mScroller = new OverScroller(context, null, 0, 0, false); //TODO Use OverScroller's flywheel mZoomManager = new ZoomManager(this, mCallbackProxy); /* The init method must follow the creation of certain member variables, @@ -1044,6 +1071,9 @@ public class WebView extends AbsoluteLayout // Compute the inverse of the density squared. DRAG_LAYER_INVERSE_DENSITY_SQUARED = 1 / (density * density); + + mOverscrollDistance = configuration.getScaledOverscrollDistance(); + mOverflingDistance = configuration.getScaledOverflingDistance(); } /** @@ -1066,6 +1096,18 @@ public class WebView extends AbsoluteLayout new TextToSpeech(getContext(), null)); } + @Override + public void setOverScrollMode(int mode) { + super.setOverScrollMode(mode); + if (mode != OVER_SCROLL_NEVER) { + if (mOverScrollGlow == null) { + mOverScrollGlow = new OverScrollGlow(this); + } + } else { + mOverScrollGlow = null; + } + } + /* package */void updateDefaultZoomDensity(int zoomDensity) { final float density = mContext.getResources().getDisplayMetrics().density * 100 / zoomDensity; @@ -1197,7 +1239,8 @@ public class WebView extends AbsoluteLayout * @hide */ public 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); } /* @@ -1946,7 +1989,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(); @@ -2171,13 +2214,15 @@ public class WebView extends AbsoluteLayout // Expects x in view coordinates int pinLocX(int x) { - return pinLoc(x, getViewWidth(), computeHorizontalScrollRange()); + if (mInOverScrollMode) return x; + return pinLoc(x, getViewWidth(), computeRealHorizontalScrollRange()); } // Expects y in view coordinates int pinLocY(int y) { + if (mInOverScrollMode) return y; return pinLoc(y, getViewHeightWithTitle(), - computeVerticalScrollRange() + getTitleHeight()); + computeRealVerticalScrollRange() + getTitleHeight()); } /** @@ -2412,7 +2457,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 @@ -2497,8 +2542,7 @@ public class WebView extends AbsoluteLayout return false; } - @Override - protected int computeHorizontalScrollRange() { + private int computeRealHorizontalScrollRange() { if (mDrawHistory) { return mHistoryWidth; } else if (mHorizontalScrollBarMode == SCROLLBAR_ALWAYSOFF @@ -2512,7 +2556,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 @@ -2526,6 +2590,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); } @@ -2540,10 +2620,39 @@ public class WebView extends AbsoluteLayout protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, int l, int t, int r, int b) { + if (mScrollY < 0) { + t -= mScrollY; + } scrollBar.setBounds(l, t + getVisibleTitleHeight(), r, b); scrollBar.draw(canvas); } + @Override + protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, + boolean clampedY) { + mInOverScrollMode = false; + int maxX = computeMaxScrollX(); + int maxY = computeMaxScrollY(); + 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 > maxY) { + mInOverScrollMode = true; + } + + int oldX = mScrollX; + int oldY = mScrollY; + + super.scrollTo(scrollX, scrollY); + + if (mOverScrollGlow != null) { + mOverScrollGlow.pullGlow(mScrollX, mScrollY, oldX, oldY, maxX, maxY); + } + } + /** * 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 @@ -2923,17 +3032,29 @@ public class WebView extends AbsoluteLayout if (mScroller.computeScrollOffset()) { int oldX = mScrollX; int oldY = mScrollY; - mScrollX = mScroller.getCurrX(); - mScrollY = mScroller.getCurrY(); - postInvalidate(); // So we draw again - if (oldX != mScrollX || oldY != mScrollY) { - onScrollChanged(mScrollX, mScrollY, oldX, oldY); - } else if (mScroller.getStartX() != mScrollX - || mScroller.getStartY() != mScrollY) { - abortAnimation(); - mPrivateHandler.removeMessages(RESUME_WEBCORE_PRIORITY); - WebViewCore.resumePriority(); - WebViewCore.resumeUpdatePicture(mWebViewCore); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + invalidate(); // So we draw again + + if (!mScroller.isFinished()) { + final int rangeX = computeMaxScrollX(); + final int rangeY = computeMaxScrollY(); + overScrollBy(x - oldX, y - oldY, oldX, oldY, + rangeX, rangeY, + mOverflingDistance, mOverflingDistance, false); + + if (mOverScrollGlow != null) { + mOverScrollGlow.absorbGlow(x, y, oldX, oldY, rangeX, rangeY); + } + } else { + mScrollX = x; + mScrollY = y; + if (mScroller.getStartX() != mScrollX || mScroller.getStartY() != mScrollY) { + abortAnimation(); + mPrivateHandler.removeMessages(RESUME_WEBCORE_PRIORITY); + WebViewCore.resumePriority(); + WebViewCore.resumeUpdatePicture(mWebViewCore); + } } } else { super.computeScroll(); @@ -3436,6 +3557,40 @@ public class WebView extends AbsoluteLayout drawCoreAndCursorRing(canvas, mBackgroundColor, mDrawCursorRing); } + /** + * Draw the background when beyond bounds + * @param canvas Canvas to draw into + */ + private void drawOverScrollBackground(Canvas canvas) { + 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 = 0; + 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); + } + @Override protected void onDraw(Canvas canvas) { // if mNativeClass is 0, the WebView has been destroyed. Do nothing. @@ -3452,6 +3607,10 @@ public class WebView extends AbsoluteLayout } int saveCount = canvas.save(); + if (mInOverScrollMode && !getSettings() + .getUseWebViewBackgroundForOverscrollBackground()) { + drawOverScrollBackground(canvas); + } if (mTitleBar != null) { canvas.translate(0, (int) mTitleBar.getHeight()); } @@ -3466,6 +3625,10 @@ public class WebView extends AbsoluteLayout } mWebViewCore.signalRepaintDone(); + if (mOverScrollGlow != null && mOverScrollGlow.drawEdgeGlows(canvas)) { + invalidate(); + } + // paint the highlight in the end if (!mTouchHighlightRegion.isEmpty()) { if (mTouchHightlightPaint == null) { @@ -4707,12 +4870,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(false); + 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(false); + } } } @@ -5167,20 +5332,6 @@ public class WebView extends AbsoluteLayout } // do pan - if (mTouchMode != TOUCH_DRAG_LAYER_MODE) { - 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) { @@ -5370,6 +5521,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; @@ -5390,6 +5547,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); @@ -5465,7 +5624,22 @@ public class WebView extends AbsoluteLayout } return; } - scrollBy(deltaX, deltaY); + + final int oldX = mScrollX; + final int oldY = mScrollY; + final int rangeX = computeMaxScrollX(); + final int rangeY = computeMaxScrollY(); + + if (mOverScrollGlow != null) { + mOverScrollGlow.setOverScrollDeltas(deltaX, deltaY); + } + + overScrollBy(deltaX, deltaY, oldX, oldY, + rangeX, rangeY, + mOverscrollDistance, mOverscrollDistance, true); + if (mOverScrollGlow != null && mOverScrollGlow.isAnimating()) { + invalidate(); + } } mZoomManager.keepZoomPickerVisible(); } @@ -5478,6 +5652,11 @@ public class WebView extends AbsoluteLayout mVelocityTracker.recycle(); mVelocityTracker = null; } + + // Release any pulled glows + if (mOverScrollGlow != null) { + mOverScrollGlow.releaseAll(); + } } private void cancelTouch() { @@ -5488,6 +5667,7 @@ public class WebView extends AbsoluteLayout mVelocityTracker.recycle(); mVelocityTracker = null; } + if (mTouchMode == TOUCH_DRAG_MODE || mTouchMode == TOUCH_DRAG_LAYER_MODE) { WebViewCore.resumePriority(); @@ -5799,12 +5979,20 @@ public class WebView extends AbsoluteLayout } } - private int computeMaxScrollX() { - return Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); + /** + * Compute the maximum horizontal scroll position. Used by {@link OverScrollGlow}. + * @return Maximum horizontal scroll position within real content + */ + int computeMaxScrollX() { + return Math.max(computeRealHorizontalScrollRange() - getViewWidth(), 0); } - private int computeMaxScrollY() { - return Math.max(computeVerticalScrollRange() + getTitleHeight() + /** + * Compute the maximum vertical scroll position. Used by {@link OverScrollGlow}. + * @return Maximum vertical scroll position within real content + */ + int computeMaxScrollY() { + return Math.max(computeRealVerticalScrollRange() + getTitleHeight() - getViewHeightWithTitle(), 0); } @@ -5823,7 +6011,7 @@ public class WebView extends AbsoluteLayout public void flingScroll(int vx, int vy) { mScroller.fling(mScrollX, mScrollY, vx, vy, 0, computeMaxScrollX(), 0, - computeMaxScrollY()); + computeMaxScrollY(), mOverflingDistance, mOverflingDistance); invalidate(); } @@ -5853,6 +6041,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(); @@ -5879,13 +6071,37 @@ public class WebView extends AbsoluteLayout + " maxX=" + maxX + " maxY=" + maxY + " mScrollX=" + mScrollX + " mScrollY=" + mScrollY); } + + // Allow sloppy flings without overscrolling at the edges. + if ((mScrollX == 0 || mScrollX == maxX) && Math.abs(vx) < Math.abs(vy)) { + vx = 0; + } + if ((mScrollY == 0 || mScrollY == maxY) && Math.abs(vy) < Math.abs(vx)) { + vy = 0; + } + + if (mOverscrollDistance < mOverflingDistance) { + if (mScrollX == -mOverscrollDistance || mScrollX == maxX + mOverscrollDistance) { + vx = 0; + } + if (mScrollY == -mOverscrollDistance || mScrollY == maxY + mOverscrollDistance) { + vy = 0; + } + } + mLastVelX = vx; mLastVelY = vy; mLastVelocity = velocity; - 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 : mOverflingDistance, mOverflingDistance); + // Duration is calculated based on velocity. With range boundaries and overscroll + // we may not know how long the final animation will take. (Hence the deprecation + // warning on the call below.) It's not a big deal for scroll bars but if webcore + // resumes during this effect we will take a performance hit. See computeScroll; + // we resume webcore there when the animation is finished. final int time = mScroller.getDuration(); - mPrivateHandler.sendEmptyMessageDelayed(RESUME_WEBCORE_PRIORITY, time); awakenScrollBars(time); invalidate(); } @@ -6684,6 +6900,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 70cfee96468c..eb8e2de8f92a 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -20,6 +20,7 @@ import com.android.internal.R; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; @@ -39,6 +40,7 @@ import android.util.LongSparseArray; import android.util.SparseBooleanArray; import android.util.StateSet; import android.view.ActionMode; +import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -52,7 +54,6 @@ import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.view.ContextMenu.ContextMenuInfo; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -138,6 +139,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; @@ -446,6 +458,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; @@ -548,6 +570,48 @@ 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; + + /** + * An estimate of how many pixels are between the top of the list and + * the top of the first position in the adapter, based on the last time + * we saw it. Used to hint where to draw edge glows. + */ + private int mFirstPositionDistanceGuess; + + /** + * An estimate of how many pixels are between the bottom of the list and + * the bottom of the last position in the adapter, based on the last time + * we saw it. Used to hint where to draw edge glows. + */ + private int mLastPositionDistanceGuess; + + /** + * Used for determining when to cancel out of overscroll. + */ + private int mDirection = 0; + + /** * Interface definition for a callback to be invoked when the list or grid * has been scrolled. */ @@ -690,9 +754,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 != OVER_SCROLL_NEVER) { + if (mEdgeGlowTop == null) { + final Resources res = getContext().getResources(); + final Drawable edge = res.getDrawable(R.drawable.overscroll_edge); + 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); + } + /** * {@inheritDoc} */ @@ -1003,6 +1087,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** + * @return true if all list content currently fits within the view boundaries + */ + private boolean contentFits() { + final int childCount = getChildCount(); + if (childCount != mItemCount) { + return false; + } + + return getChildAt(0).getTop() >= 0 && getChildAt(childCount - 1).getBottom() <= mBottom; + } + + /** * 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 * to implement {@link SectionIndexer} if it wishes to display alphabet preview and @@ -1540,6 +1636,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; } @@ -1612,6 +1712,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te layoutChildren(); mInLayout = false; + + mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; } /** @@ -2126,6 +2228,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mFlingRunnable.endFling(); if (mScrollY != 0) { mScrollY = 0; + finishGlows(); invalidate(); } } @@ -2445,9 +2548,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 @@ -2483,6 +2587,19 @@ 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; + finishGlows(); + invalidate(); + } + } } } @@ -2513,49 +2630,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); - mFlingRunnable.flywheelTouch(); + 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); + mFlingRunnable.flywheelTouch(); + } } } - } - 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; } @@ -2593,9 +2724,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) { @@ -2603,23 +2750,117 @@ 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(); + } + + final int overscrollMode = getOverScrollMode(); + if (overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && + !contentFits())) { + mDirection = 0; // Reset when entering overscroll. + mTouchMode = TOUCH_MODE_OVERSCROLL; + if (rawDeltaY > 0) { + mEdgeGlowTop.onPull((float) overscroll / getHeight()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (rawDeltaY < 0) { + mEdgeGlowBottom.onPull((float) overscroll / getHeight()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + } } 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; + int newDirection = y > mLastY ? 1 : -1; + + if (mDirection == 0) { + mDirection = newDirection; + } + + if (mDirection != newDirection) { + // 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); + final int overscrollMode = getOverScrollMode(); + if (overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && + !contentFits())) { + if (rawDeltaY > 0) { + mEdgeGlowTop.onPull((float) -incrementalDeltaY / getHeight()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (rawDeltaY < 0) { + mEdgeGlowBottom.onPull((float) -incrementalDeltaY / getHeight()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + invalidate(); + } + if (Math.abs(mOverscrollDistance) == Math.abs(mScrollY)) { + // Don't allow overfling if we're at the edge. + mVelocityTracker.clear(); + } + } + mLastY = y; + mDirection = newDirection; + } + break; } break; @@ -2693,19 +2934,30 @@ 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) * mVelocityScale); - - 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(); } @@ -2725,10 +2977,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(); @@ -2759,24 +3033,42 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } case MotionEvent.ACTION_CANCEL: { - mTouchMode = TOUCH_MODE_REST; - setPressed(false); - View motionView = 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; } @@ -2801,10 +3093,61 @@ 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, Math.min(0, scrollY + mFirstPositionDistanceGuess)); + 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, + Math.max(height, scrollY + mLastPositionDistanceGuess)); + 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); + } } } @@ -2823,6 +3166,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(); @@ -2887,6 +3234,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(); @@ -2942,7 +3290,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 @@ -2953,7 +3301,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public void run() { final int activeId = mActivePointerId; final VelocityTracker vt = mVelocityTracker; - final Scroller scroller = mScroller; + final OverScroller scroller = mScroller; if (vt == null || activeId == INVALID_POINTER) { return; } @@ -2975,7 +3323,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds FlingRunnable() { - mScroller = new Scroller(getContext()); + mScroller = new OverScroller(getContext()); } void start(int initialVelocity) { @@ -2998,6 +3346,42 @@ 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); + final int overscrollMode = getOverScrollMode(); + if (overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) { + mTouchMode = TOUCH_MODE_OVERFLING; + 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; @@ -3040,58 +3424,100 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return; } // Fall through - case TOUCH_MODE_FLING: + case TOUCH_MODE_FLING: { if (mItemCount == 0 || getChildCount() == 0) { endFling(); return; } - break; - } - final Scroller scroller = mScroller; - boolean more = scroller.computeScrollOffset(); - final int y = scroller.getCurrY(); - // Flip sign to convert finger direction to list items direction - // (e.g. finger moving down means list is moving towards the top) - int delta = mLastFlingY - y; + final OverScroller scroller = mScroller; + boolean more = scroller.computeScrollOffset(); + final int y = scroller.getCurrY(); - // Pretend that each frame of a fling scroll is a touch scroll - if (delta > 0) { - // List is moving towards the top. Use first view as mMotionPosition - mMotionPosition = mFirstPosition; - final View firstView = getChildAt(0); - mMotionViewOriginalTop = firstView.getTop(); + // Flip sign to convert finger direction to list items direction + // (e.g. finger moving down means list is moving towards the top) + int delta = mLastFlingY - y; - // Don't fling more than 1 screen - delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta); - } else { - // List is moving towards the bottom. Use last view as mMotionPosition - int offsetToLast = getChildCount() - 1; - mMotionPosition = mFirstPosition + offsetToLast; + // Pretend that each frame of a fling scroll is a touch scroll + if (delta > 0) { + // List is moving towards the top. Use first view as mMotionPosition + mMotionPosition = mFirstPosition; + final View firstView = getChildAt(0); + mMotionViewOriginalTop = firstView.getTop(); - final View lastView = getChildAt(offsetToLast); - mMotionViewOriginalTop = lastView.getTop(); + // Don't fling more than 1 screen + delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta); + } else { + // List is moving towards the bottom. Use last view as mMotionPosition + int offsetToLast = getChildCount() - 1; + mMotionPosition = mFirstPosition + offsetToLast; - // Don't fling more than 1 screen - delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta); - } + final View lastView = getChildAt(offsetToLast); + mMotionViewOriginalTop = lastView.getTop(); - // Don't stop just because delta is zero (it could have been rounded) - final boolean atEnd = trackMotionScroll(delta, delta) && (delta != 0); + // Don't fling more than 1 screen + delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta); + } - if (more && !atEnd) { - invalidate(); - mLastFlingY = y; - post(this); - } else { - endFling(); + // 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(); + } - if (PROFILE_FLINGING) { - if (mFlingProfilingStarted) { - Debug.stopMethodTracing(); - mFlingProfilingStarted = false; + // Don't stop just because delta is zero (it could have been rounded) + final boolean atEnd = trackMotionScroll(delta, delta) && (delta != 0); + 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(); + mLastFlingY = y; + post(this); + } else { + endFling(); + + if (PROFILE_FLINGING) { + if (mFlingProfilingStarted) { + Debug.stopMethodTracing(); + mFlingProfilingStarted = false; + } + + if (mFlingStrictSpan != null) { + mFlingStrictSpan.finish(); + mFlingStrictSpan = null; + } + } + } + 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; + } } } } @@ -3620,16 +4046,29 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final int firstPosition = mFirstPosition; - if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) { + // Update our guesses for where the first and last views are + if (firstPosition == 0) { + mFirstPositionDistanceGuess = firstTop - mListPadding.top; + } else { + mFirstPositionDistanceGuess += incrementalDeltaY; + } + if (firstPosition + childCount == mItemCount) { + mLastPositionDistanceGuess = lastBottom + mListPadding.bottom; + } else { + mLastPositionDistanceGuess += incrementalDeltaY; + } + + 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; @@ -3805,6 +4244,22 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te abstract int findMotionRow(int y); /** + * Find the row closest to y. This row will be used as the motion row when scrolling. + * + * @param y Where the user touched + * @return The position of the first (or only) item in the row closest to y + */ + int findClosestMotionRow(int y) { + final int childCount = getChildCount(); + if (childCount == 0) { + return INVALID_POSITION; + } + + final int motionRow = findMotionRow(y); + return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1; + } + + /** * Causes all the views to be rebuilt and redrawn. */ public void invalidateViews() { @@ -4577,6 +5032,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return result; } + private void finishGlows() { + if (mEdgeGlowTop != null) { + mEdgeGlowTop.finish(); + mEdgeGlowBottom.finish(); + } + } + /** * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService * through the specified intent. diff --git a/core/java/android/widget/EdgeGlow.java b/core/java/android/widget/EdgeGlow.java new file mode 100644 index 000000000000..416be86e5915 --- /dev/null +++ b/core/java/android/widget/EdgeGlow.java @@ -0,0 +1,327 @@ +/* + * 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.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"; + + // 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 = 167; + + // 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 MAX_ALPHA = 0.8f; + 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 MAX_GLOW_HEIGHT = 3.f; + + private static final float PULL_GLOW_BEGIN = 1.f; + private static final float PULL_EDGE_BEGIN = 0.6f; + + // Minimum velocity that will be absorbed + private static final int MIN_VELOCITY = 100; + + private static final float EPSILON = 0.001f; + + private final Drawable mEdge; + private final 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 float mDuration; + + private final 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; + + // How much dragging should effect the height of the edge image. + // Number determined by user testing. + private static final int PULL_DISTANCE_EDGE_FACTOR = 5; + + // How much dragging should effect the height of the glow image. + // Number determined by user testing. + private static final int PULL_DISTANCE_GLOW_FACTOR = 5; + private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f; + + private static final int VELOCITY_EDGE_FACTOR = 8; + private static final int VELOCITY_GLOW_FACTOR = 16; + + 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; + } + + public void finish() { + 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(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA)); + mEdgeScaleY = mEdgeScaleYStart = Math.max( + HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f)); + + mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, + mGlowAlpha + + (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); + + float glowChange = Math.abs(deltaDistance); + if (deltaDistance > 0 && mPullDistance < 0) { + glowChange = -glowChange; + } + if (mPullDistance == 0) { + mGlowScaleY = 0; + } + + // Do not allow glow to get larger than MAX_GLOW_HEIGHT. + mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max( + 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR)); + + mEdgeAlphaFinish = mEdgeAlpha; + mEdgeScaleYFinish = mEdgeScaleY; + mGlowAlphaFinish = mGlowAlpha; + mGlowScaleYFinish = mGlowScaleY; + } + + /** + * Call when the object is released after being pulled. + */ + public void onRelease() { + mPullDistance = 0; + + if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { + return; + } + + mState = STATE_RECEDE; + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + + 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; + velocity = Math.max(MIN_VELOCITY, Math.abs(velocity)); + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = 0.1f + (velocity * 0.03f); + + // The edge should always be at least partially visible, regardless + // of velocity. + mEdgeAlphaStart = 0.f; + mEdgeScaleY = mEdgeScaleYStart = 0.f; + // The glow depends more on the velocity, and therefore starts out + // nearly invisible. + mGlowAlphaStart = 0.5f; + mGlowScaleYStart = 0.f; + + // Factor the velocity by 8. Testing on device shows this works best to + // reflect the strength of the user's scrolling. + mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1)); + // Edge should never get larger than the size of its asset. + mEdgeScaleYFinish = Math.max( + HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f)); + + // Growth for the size of the glow should be quadratic to properly + // respond + // to a user's scrolling speed. The faster the scrolling speed, the more + // intense the effect should be for both the size and the saturation. + mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f); + // Alpha should change for the glow as well as size. + mGlowAlphaFinish = Math.max( + mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); + } + + + /** + * 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(); + + final float distScale = (float) mHeight / mWidth; + + mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255)); + // Width of the image should be 3 * the width of the screen. + // Should start off screen to the left. + mGlow.setBounds(-mWidth, 0, mWidth * 2, (int) Math.min( + glowHeight * mGlowScaleY * distScale * 0.6f, mHeight * MAX_GLOW_HEIGHT)); + 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); + + return mState != STATE_IDLE; + } + + private void update() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final float t = Math.min((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; + + // After absorb, the glow and edge should fade to nothing. + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL: + mState = STATE_PULL_DECAY; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = PULL_DECAY_TIME; + + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After pull, the glow and edge should fade to nothing. + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL_DECAY: + // When receding, we want edge to decrease more slowly + // than the glow. + float factor = mGlowScaleYFinish != 0 ? 1 + / (mGlowScaleYFinish * mGlowScaleYFinish) + : Float.MAX_VALUE; + mEdgeScaleY = mEdgeScaleYStart + + (mEdgeScaleYFinish - mEdgeScaleYStart) * + interp * factor; + break; + case STATE_RECEDE: + mState = STATE_IDLE; + break; + } + } + } +} diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 114ae81a05b9..414646023a12 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -1954,7 +1954,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 e30d4c8dc54a..9fc91daeba10 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; @@ -65,7 +70,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 @@ -119,6 +126,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. @@ -191,7 +201,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); @@ -199,6 +209,8 @@ public class HorizontalScrollView extends FrameLayout { mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mOverscrollDistance = configuration.getScaledOverscrollDistance(); + mOverflingDistance = configuration.getScaledOverflingDistance(); } @Override @@ -463,6 +475,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); @@ -495,9 +510,7 @@ public class HorizontalScrollView extends FrameLayout { switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); - if (!(mIsBeingDragged = inChild((int) x, (int) ev.getY()))) { - return false; - } + mIsBeingDragged = true; /* * If being flinged and user touches, stop the fling. isFinished @@ -520,7 +533,36 @@ 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 overscrollMode = getOverScrollMode(); + if (overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)) { + final int pulledToX = oldX + deltaX; + if (pulledToX < 0) { + mEdgeGlowLeft.onPull((float) deltaX / getWidth()); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onRelease(); + } + } else if (pulledToX > range) { + mEdgeGlowRight.onPull((float) deltaX / getWidth()); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } + if (mEdgeGlowLeft != null + && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) { + invalidate(); + } + } } break; case MotionEvent.ACTION_UP: @@ -529,8 +571,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; @@ -540,16 +589,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: @@ -576,12 +636,28 @@ 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) { View child = getChildAt(0); scrollRange = Math.max(0, - child.getWidth() - getWidth() - mPaddingLeft - mPaddingRight); + child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight)); } return scrollRange; } @@ -958,7 +1034,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 @@ -1019,14 +1104,20 @@ 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(); + final int overscrollMode = getOverScrollMode(); + if (overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)) { + if (x < 0 && oldX >= 0) { + mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (x > range && oldX <= range) { + mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); + } } } awakenScrollBars(); @@ -1263,7 +1354,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; @@ -1301,6 +1392,56 @@ public class HorizontalScrollView extends FrameLayout { } } + @Override + public void setOverScrollMode(int mode) { + if (mode != OVER_SCROLL_NEVER) { + if (mEdgeGlowLeft == null) { + final Resources res = getContext().getResources(); + final Drawable edge = res.getDrawable(R.drawable.overscroll_edge); + 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, Math.min(0, 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, -(Math.max(getScrollRange(), 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 502cc38bd758..fd4f950cde17 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -104,6 +104,9 @@ public class ListView extends AbsListView { Drawable mDivider; int mDividerHeight; + Drawable mOverScrollHeader; + Drawable mOverScrollFooter; + private boolean mIsCacheColorOpaque; private boolean mDividerIsOpaque; @@ -152,6 +155,18 @@ 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); @@ -2962,14 +2977,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; @@ -2998,34 +3051,67 @@ public class ListView extends AbsListView { final int listBottom = mBottom - mTop - mListPadding.bottom + mScrollY; if (!mStackFromBottom) { - int bottom; + 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 ((areAllItemsSelectable || - (adapter.isEnabled(first + i) && (i == count - 1 || - adapter.isEnabled(first + i + 1))))) { - bounds.top = bottom; - bounds.bottom = bottom + dividerHeight; - drawDivider(canvas, bounds, i); - } else if (fillForMissingDividers) { - bounds.top = bottom; - bounds.bottom = bottom + dividerHeight; - canvas.drawRect(bounds, paint); + + if (drawDividers && + (bottom < listBottom && !(drawOverscrollFooter && i == count - 1))) { + if ((areAllItemsSelectable || + (adapter.isEnabled(first + i) && (i == count - 1 || + adapter.isEnabled(first + i + 1))))) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + drawDivider(canvas, bounds, i); + } else if (fillForMissingDividers) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + canvas.drawRect(bounds, paint); + } } } } + + 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); @@ -3052,9 +3138,16 @@ public class ListView extends AbsListView { } if (count > 0 && scrollY > 0) { - bounds.top = listBottom; - bounds.bottom = listBottom + dividerHeight; - drawDivider(canvas, bounds, -1); + 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); + } } } } @@ -3150,6 +3243,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 000000000000..cd81e31ce869 --- /dev/null +++ b/core/java/android/widget/OverScroller.java @@ -0,0 +1,888 @@ +/* + * 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.graphics.Interpolator; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; + +/** + * 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 { + int mMode; + + private final MagneticOverScroller mScrollerX; + private final MagneticOverScroller mScrollerY; + + private float mDeceleration; + private final float mPpi; + private final boolean mFlywheel; + + private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9)); + private static float ALPHA = 800; // pixels / seconds + private static float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance) + private static float END_TENSION = 1.0f - START_TENSION; + private static final int NB_SAMPLES = 100; + private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; + private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; + + private static final int DEFAULT_DURATION = 250; + private static final int SCROLL_MODE = 0; + private static final int FLING_MODE = 1; + + static { + float x_min = 0.0f; + float y_min = 0.0f; + for (int i = 0; i < NB_SAMPLES; i++) { + final float alpha = (float) i / NB_SAMPLES; + { + float x_max = 1.0f; + float x, tx, coef; + while (true) { + x = x_min + (x_max - x_min) / 2.0f; + coef = 3.0f * x * (1.0f - x); + tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x; + if (Math.abs(tx - alpha) < 1E-5) break; + if (tx > alpha) x_max = x; + else x_min = x; + } + SPLINE_POSITION[i] = coef + x * x * x; + } + + { + float y_max = 1.0f; + float y, dy, coef; + while (true) { + y = y_min + (y_max - y_min) / 2.0f; + coef = 3.0f * y * (1.0f - y); + dy = coef + y * y * y; + if (Math.abs(dy - alpha) < 1E-5) break; + if (dy > alpha) y_max = y; + else y_min = y; + } + SPLINE_TIME[i] = coef * ((1.0f - y) * START_TENSION + y * END_TENSION) + y * y * y; + } + } + SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; + } + + public OverScroller(Context context) { + this(context, null, 0.f, 0.f, true); + } + + /** + * 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, boolean flywheel) { + mFlywheel = flywheel; + mPpi = context.getResources().getDisplayMetrics().density * 160.0f; + mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); + mScrollerX = new MagneticOverScroller(); + mScrollerY = new MagneticOverScroller(); + + mScrollerX.setBounceCoefficient(bounceCoefficientX); + mScrollerY.setBounceCoefficient(bounceCoefficientY); + } + + + /** + * The amount of friction applied to flings. The default value + * is {@link ViewConfiguration#getScrollFriction}. + * + * @param friction A scalar dimension-less value representing the coefficient of + * friction. + */ + public final void setFriction(float friction) { + mDeceleration = computeDeceleration(friction); + } + + private float computeDeceleration(float friction) { + return 9.81f // g (m/s^2) + * 39.37f // inch/meter + * mPpi // pixels per inch + * friction; + } + + /** + * + * 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 (float) Math.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. + */ + @Deprecated + 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. + */ + @Deprecated + 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. + */ + @Deprecated + 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. + */ + @Deprecated + 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; + + q = Scroller.viscousFluid(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) { + // Continue a scroll or fling in progress + if (mFlywheel && !isFinished()) { + float oldVelocityX = mScrollerX.mCurrVelocity; + float oldVelocityY = mScrollerY.mCurrVelocity; + if (Math.signum(velocityX) == Math.signum(oldVelocityX) && + Math.signum(velocityY) == Math.signum(oldVelocityY)) { + velocityX += oldVelocityX; + velocityY += oldVelocityY; + } + } + + 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. Absolute value - must be positive. + */ + 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); + } + + /** + * @hide + */ + public boolean isScrollingInDirection(float xvel, float yvel) { + final int dx = mScrollerX.mFinal - mScrollerX.mStart; + final int dy = mScrollerY.mFinal - mScrollerY.mStart; + return !isFinished() && Math.signum(xvel) == Math.signum(dx) && + Math.signum(yvel) == Math.signum(dy); + } + + 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; + + // Duration to complete spline component of animation + int mSplineDuration; + + // Distance to travel along spline animation + int mSplineDistance; + + // Whether the animation is currently in progress + boolean mFinished; + + 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; + + // 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; //Float.MAX_VALUE;//140.0f; + + // Proportion of the velocity that is preserved when the edge is reached. + private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.36f; + + 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. + */ + float getDeceleration(int velocity) { + return velocity > 0 ? -OverScroller.this.mDeceleration : OverScroller.this.mDeceleration; + } + + /* + * Modifies mDuration to the duration it takes to get from start to newFinal using the + * spline interpolation. The previous duration was needed to get to oldFinal. + */ + void adjustDuration(int start, int oldFinal, int newFinal) { + final int oldDistance = oldFinal - start; + final int newDistance = newFinal - start; + final float x = (float) Math.abs((float) newDistance / oldDistance); + final int index = (int) (NB_SAMPLES * x); + if (index < NB_SAMPLES) { + final float x_inf = (float) index / NB_SAMPLES; + final float x_sup = (float) (index + 1) / NB_SAMPLES; + final float t_inf = SPLINE_TIME[index]; + final float t_sup = SPLINE_TIME[index + 1]; + final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf); + + mDuration *= timeCoef; + } + } + + 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 finish() { + mCurrentPosition = mFinal; + // Not reset since WebView relies on this value for fast fling. + // TODO: restore when WebView uses the fast fling implemented in this class. + // 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 = mFinal = start; + mVelocity = 0; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = 0; + + if (start < min) { + startSpringback(start, min, 0); + } else if (start > max) { + startSpringback(start, max, 0); + } + + return !mFinished; + } + + private void startSpringback(int start, int end, int velocity) { + mFinished = false; + mState = TO_BOUNCE; + mStart = mFinal = end; + final float velocitySign = Math.signum(start - end); + mDeceleration = getDeceleration((int) velocitySign); + fitOnBounceCurve(start, end, velocity); + mDuration = - (int) (2000.0f * mVelocity / mDeceleration); + } + + void fling(int start, int velocity, int min, int max, int over) { + mOver = over; + mFinished = false; + mCurrVelocity = mVelocity = velocity; + mDuration = mSplineDuration = 0; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStart = start; + + if (start > max || start < min) { + startAfterEdge(start, min, max, velocity); + return; + } + + mState = TO_EDGE; + double totalDistance = 0.0; + + if (velocity != 0) { + final double l = Math.log(START_TENSION * Math.abs(velocity) / ALPHA); + // Duration are expressed in milliseconds + mDuration = mSplineDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0))); + totalDistance = (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)); + } + + mSplineDistance = (int) (totalDistance * Math.signum(velocity)); + mFinal = start + mSplineDistance; + + // Clamp to a valid final position + if (mFinal < min) { + adjustDuration(mStart, mFinal, min); + mFinal = min; + } + + if (mFinal > max) { + adjustDuration(mStart, mFinal, max); + mFinal = max; + } + } + + private void fitOnBounceCurve(int start, int end, int velocity) { + // Simulate a bounce that started from edge + final float durationToApex = - velocity / mDeceleration; + final float distanceToApex = velocity * velocity / 2.0f / Math.abs(mDeceleration); + final float distanceToEdge = Math.abs(end - start); + final float totalDuration = (float) Math.sqrt( + 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration)); + mStartTime -= (int) (1000.0f * (totalDuration - durationToApex)); + mStart = end; + mVelocity = (int) (- mDeceleration * totalDuration); + } + + private void startBounceAfterEdge(int start, int end, int velocity) { + mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity); + fitOnBounceCurve(start, end, velocity); + onEdgeReached(); + } + + private void startAfterEdge(int start, int min, int max, int velocity) { + if (start > min && start < max) { + mFinished = true; + return; + } + final boolean positive = start > max; + final int edge = positive ? max : min; + final int overDistance = start - edge; + boolean keepIncreasing = overDistance * velocity >= 0; + if (keepIncreasing) { + // Will result in a bounce or a to_boundary depending on velocity. + startBounceAfterEdge(start, edge, velocity); + } else { + final double l = Math.log(START_TENSION * Math.abs(velocity) / ALPHA); + final double totalDistance = + (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)); + if (totalDistance > Math.abs(overDistance)) { + fling(start, velocity, positive ? min : start, positive ? start : max, mOver); + } else { + startSpringback(start, edge, velocity); + } + } + } + + void notifyEdgeReached(int start, int end, int over) { + mOver = over; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + // We were in fling/scroll mode before: current velocity is such that distance to edge + // is increasing. Ensures that startAfterEdge will not start a new fling. + startAfterEdge(start, end, end, (int) mCurrVelocity); + } + + private void onEdgeReached() { + // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. + final float distance = - mVelocity * mVelocity / (2.0f * mDeceleration); + + if (Math.abs(distance) < mOver) { + // Spring force will bring us back to final position + mState = TO_BOUNCE; + mFinal = mStart; + mDuration = - (int) (2000.0f * mVelocity / mDeceleration); + } 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.0 * Math.PI * over / 2.0 / mVelocity); + } + } + + boolean continueWhenFinished() { + switch (mState) { + case TO_EDGE: + // Duration from start to null velocity + if (mDuration < mSplineDuration) { + // If the animation was clamped, we reached the edge + mStart = mFinal; + // Speed when edge was reached + mVelocity = (int) mCurrVelocity; + mDeceleration = getDeceleration(mVelocity); + 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), 0); + break; + case TO_BOUNCE: + mVelocity = (int) (mVelocity * mBounceCoefficient); + if (Math.abs(mVelocity) < MINIMUM_VELOCITY_FOR_BOUNCE) { + return false; + } + mStartTime += mDuration; + mDuration = - (int) (mVelocity / mDeceleration); + 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 currentTime = time - mStartTime; + + if (currentTime > mDuration) { + return false; + } + + double distance = 0.0; + switch (mState) { + case TO_EDGE: { + final float t = (float) currentTime / mSplineDuration; + final int index = (int) (NB_SAMPLES * t); + float distanceCoef = 1.f; + float velocityCoef = 0.f; + if (index < NB_SAMPLES) { + final float t_inf = (float) index / NB_SAMPLES; + final float t_sup = (float) (index + 1) / NB_SAMPLES; + final float d_inf = SPLINE_POSITION[index]; + final float d_sup = SPLINE_POSITION[index + 1]; + velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); + distanceCoef = d_inf + (t - t_inf) * velocityCoef; + } + + distance = distanceCoef * mSplineDistance; + mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000; + break; + } + + case TO_BOUNCE: { + final float t = currentTime / 1000.0f; + mCurrVelocity = mVelocity + mDeceleration * t; + distance = mVelocity * t + mDeceleration * t * t / 2.0f; + break; + } + + case TO_BOUNDARY: { + final float t = currentTime / 1000.0f; + final float d = t * Math.abs(mVelocity) / mOver; + mCurrVelocity = mVelocity * (float) Math.cos(d); + distance = (mVelocity > 0 ? mOver : -mOver) * Math.sin(d); + break; + } + } + + mCurrentPosition = mStart + (int) Math.round(distance); + return true; + } + } +} diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index eb527eb3d3e0..3faec5578d66 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; @@ -61,7 +64,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 @@ -115,6 +120,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. @@ -187,7 +195,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); @@ -195,6 +203,8 @@ public class ScrollView extends FrameLayout { mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mOverscrollDistance = configuration.getScaledOverscrollDistance(); + mOverflingDistance = configuration.getScaledOverflingDistance(); } @Override @@ -459,6 +469,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); @@ -491,9 +504,7 @@ public class ScrollView extends FrameLayout { switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { final float y = ev.getY(); - if (!(mIsBeingDragged = inChild((int) ev.getX(), (int) y))) { - return false; - } + mIsBeingDragged = true; /* * If being flinged and user touches, stop the fling. isFinished @@ -516,7 +527,36 @@ 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 overscrollMode = getOverScrollMode(); + if (overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)) { + final int pulledToY = oldY + deltaY; + if (pulledToY < 0) { + mEdgeGlowTop.onPull((float) deltaY / getHeight()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > range) { + mEdgeGlowBottom.onPull((float) deltaY / getHeight()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + if (mEdgeGlowTop != null + && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { + invalidate(); + } + } } break; case MotionEvent.ACTION_UP: @@ -525,8 +565,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; @@ -536,16 +583,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: @@ -572,6 +630,32 @@ 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) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop)); + } + return scrollRange; + } + /** * <p> * Finds the next focusable component that fits in this View's bounds @@ -948,7 +1032,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 @@ -1009,14 +1102,20 @@ 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(); + final int overscrollMode = getOverScrollMode(); + if (overscrollMode == OVER_SCROLL_ALWAYS || + (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)) { + if (y < 0 && oldY >= 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y > range && oldY <= range) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); + } } } awakenScrollBars(); @@ -1254,7 +1353,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; @@ -1292,6 +1391,55 @@ public class ScrollView extends FrameLayout { } } + @Override + public void setOverScrollMode(int mode) { + if (mode != OVER_SCROLL_NEVER) { + if (mEdgeGlowTop == null) { + final Resources res = getContext().getResources(); + final Drawable edge = res.getDrawable(R.drawable.overscroll_edge); + 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, Math.min(0, 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, Math.max(getScrollRange(), 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 b1f50ba04dc6..f00640eb5e45 100644 --- a/core/java/android/widget/Scroller.java +++ b/core/java/android/widget/Scroller.java @@ -52,14 +52,10 @@ public class Scroller { private float mDurationReciprocal; private float mDeltaX; private float mDeltaY; - private float mViscousFluidScale; - private float mViscousFluidNormalize; private boolean mFinished; private Interpolator mInterpolator; private boolean mFlywheel; - private float mCoeffX = 0.0f; - private float mCoeffY = 1.0f; private float mVelocity; private static final int DEFAULT_DURATION = 250; @@ -94,8 +90,17 @@ public class Scroller { SPLINE[i] = d; } SPLINE[NB_SAMPLES] = 1.0f; + + // 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); } + private static float sViscousFluidScale; + private static float sViscousFluidNormalize; + /** * Create a Scroller with the default duration and interpolator. */ @@ -103,6 +108,11 @@ public class Scroller { this(context, null); } + /** + * Create a Scroller with the specified interpolator. If the interpolator is + * null, the default (viscous) interpolator will be used. "Flywheel" behavior will + * be in effect for apps targeting Honeycomb or newer. + */ public Scroller(Context context, Interpolator interpolator) { this(context, interpolator, context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); @@ -110,7 +120,8 @@ public class Scroller { /** * Create a Scroller with the specified interpolator. If the interpolator is - * null, the default (viscous) interpolator will be used. + * null, the default (viscous) interpolator will be used. Specify whether or + * not to support progressive "flywheel" behavior in flinging. */ public Scroller(Context context, Interpolator interpolator, boolean flywheel) { mFinished = true; @@ -332,12 +343,7 @@ public class Scroller { mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; - mDurationReciprocal = 1.0f / 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); + mDurationReciprocal = 1.0f / (float) mDuration; } /** @@ -393,8 +399,8 @@ public class Scroller { mStartX = startX; mStartY = startY; - mCoeffX = velocity == 0 ? 1.0f : velocityX / velocity; - mCoeffY = velocity == 0 ? 1.0f : velocityY / velocity; + float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; + float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; int totalDistance = (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)); @@ -404,22 +410,20 @@ public class Scroller { mMinY = minY; mMaxY = maxY; - mFinalX = startX + Math.round(totalDistance * mCoeffX); + mFinalX = startX + Math.round(totalDistance * coeffX); // Pin to mMinX <= mFinalX <= mMaxX mFinalX = Math.min(mFinalX, mMaxX); mFinalX = Math.max(mFinalX, mMinX); - mFinalY = startY + Math.round(totalDistance * mCoeffY); + mFinalY = startY + Math.round(totalDistance * coeffY); // Pin to mMinY <= mFinalY <= mMaxY mFinalY = Math.min(mFinalY, mMaxY); 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 { @@ -427,7 +431,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/layout/alert_dialog.xml b/core/res/res/layout/alert_dialog.xml index 3982ed9c159b..e3ba63484f30 100644 --- a/core/res/res/layout/alert_dialog.xml +++ b/core/res/res/layout/alert_dialog.xml @@ -81,7 +81,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 691ee8c8d339..40b9e69a83d6 100644 --- a/core/res/res/layout/preference_dialog_edittext.xml +++ b/core/res/res/layout/preference_dialog_edittext.xml @@ -19,7 +19,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="48dp" - android:layout_marginBottom="48dp"> + android:layout_marginBottom="48dp" + 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 94dcb6a349b3..80d22f61d3fb 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:attr/listDividerAlertDialog" - 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 651bfea4f292..770adf5a81e0 100755 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -1632,6 +1632,20 @@ <code>public void sayHello(View v)</code> method of your context (typically, your Activity). --> <attr name="onClick" format="string" /> + + <!-- Defines over-scrolling behavior. This property is used only if the + View is scrollable. Over-scrolling is the ability for the user to + receive feedback when attempting to scroll beyond meaningful content. --> + <attr name="overScrollMode"> + <!-- Always show over-scroll effects, even if the content fits entirely + within the available space. --> + <enum name="always" value="0" /> + <!-- Only show over-scroll effects if the content is large + enough to meaningfully scroll. --> + <enum name="ifContentScrolls" value="1" /> + <!-- Never show over-scroll effects. --> + <enum name="never" value="2" /> + </attr> </declare-styleable> <!-- Attributes that can be used with a {@link android.view.ViewGroup} or any @@ -2116,6 +2130,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 f4d1df8ae730..f2ab5cdce58e 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1253,6 +1253,9 @@ <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 type="attr" name="filterTouchesWhenObscured" id="0x010102c4" /> <public type="attr" name="textSelectHandleLeft" id="0x010102c5" /> <public type="attr" name="textSelectHandleRight" id="0x010102c6" /> diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml index 3ad199e09377..18e8273614d0 100644 --- a/packages/SystemUI/res/layout/status_bar_expanded.xml +++ b/packages/SystemUI/res/layout/status_bar_expanded.xml @@ -71,6 +71,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fadingEdge="none" + android:overScrollMode="ifContentScrolls" > <LinearLayout android:id="@+id/notificationLinearLayout" |