Merge overscrolling from gingerbread.
Change-Id: I3eb4413261b38ddd8c609d6153bdfd4ae46c6a0f
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index e6eb46e..011ad77 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -1627,6 +1627,40 @@
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 @@
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 @@
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 @@
});
}
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 @@
}
/**
+ * 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 85981d2..bb85894 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -144,7 +144,7 @@
/**
* 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 @@
*/
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 @@
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 @@
mWindowTouchSlop = WINDOW_TOUCH_SLOP;
//noinspection deprecation
mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE;
+ mOverscrollDistance = OVERSCROLL_DISTANCE;
+ mOverflingDistance = OVERFLING_DISTANCE;
}
/**
@@ -216,6 +230,9 @@
// 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 @@
}
/**
+ * @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 0000000..53600f6
--- /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 98e6ab8..2e69d99 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -268,6 +268,8 @@
private AutoFillProfile mAutoFillProfile;
+ private boolean mUseWebViewBackgroundForOverscroll = true;
+
// private WebSettings, not accessible by the host activity
static private int mDoubleTapToastCount = 3;
@@ -631,6 +633,23 @@
}
/**
+ * 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 376b1d0..3cb9084 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.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.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.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 @@
// 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 @@
// 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 @@
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 @@
// 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 @@
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 @@
* @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 @@
}
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 @@
// 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 @@
// 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 @@
return false;
}
- @Override
- protected int computeHorizontalScrollRange() {
+ private int computeRealHorizontalScrollRange() {
if (mDrawHistory) {
return mHistoryWidth;
} else if (mHorizontalScrollBarMode == SCROLLBAR_ALWAYSOFF
@@ -2512,7 +2556,27 @@
}
@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 @@
}
@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 @@
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 @@
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 @@
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 @@
}
int saveCount = canvas.save();
+ if (mInOverScrollMode && !getSettings()
+ .getUseWebViewBackgroundForOverscrollBackground()) {
+ drawOverScrollBackground(canvas);
+ }
if (mTitleBar != null) {
canvas.translate(0, (int) mTitleBar.getHeight());
}
@@ -3466,6 +3625,10 @@
}
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 @@
@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 @@
}
// 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 @@
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 @@
}
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 @@
}
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 @@
mVelocityTracker.recycle();
mVelocityTracker = null;
}
+
+ // Release any pulled glows
+ if (mOverScrollGlow != null) {
+ mOverScrollGlow.releaseAll();
+ }
}
private void cancelTouch() {
@@ -5488,6 +5667,7 @@
mVelocityTracker.recycle();
mVelocityTracker = null;
}
+
if (mTouchMode == TOUCH_DRAG_MODE ||
mTouchMode == TOUCH_DRAG_LAYER_MODE) {
WebViewCore.resumePriority();
@@ -5799,12 +5979,20 @@
}
}
- 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 void flingScroll(int vx, int vy) {
mScroller.fling(mScrollX, mScrollY, vx, vy, 0, computeMaxScrollX(), 0,
- computeMaxScrollY());
+ computeMaxScrollY(), mOverflingDistance, mOverflingDistance);
invalidate();
}
@@ -5853,6 +6041,10 @@
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 @@
+ " 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 @@
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 70cfee9..eb8e2de 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -20,6 +20,7 @@
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.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.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 @@
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 @@
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 @@
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 @@
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 @@
}
/**
+ * @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 @@
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 @@
layoutChildren();
mInLayout = false;
+
+ mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
}
/**
@@ -2126,6 +2228,7 @@
mFlingRunnable.endFling();
if (mScrollY != 0) {
mScrollY = 0;
+ finishGlows();
invalidate();
}
}
@@ -2445,9 +2548,10 @@
// 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 @@
// 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 @@
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 @@
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 @@
}
// 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 @@
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 @@
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 @@
}
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 @@
}
@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 @@
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 @@
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 @@
/**
* 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 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 @@
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 @@
}
}
+ 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 @@
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 @@
final int firstPosition = mFirstPosition;
- if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
- // Don't need to move views down if the top of the first position
- // is already visible
- return true;
+ // 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 + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
+ if (firstPosition == 0 && firstTop >= listPadding.top && incrementalDeltaY >= 0) {
+ // Don't need to move views down if the top of the first position
+ // is already visible
+ return incrementalDeltaY != 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 @@
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 @@
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 0000000..416be86
--- /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 114ae81..4146460 100644
--- a/core/java/android/widget/GridView.java
+++ b/core/java/android/widget/GridView.java
@@ -1954,7 +1954,12 @@
// 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 e30d4c8..9fc91da 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 @@
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 @@
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 @@
private void initScrollView() {
- mScroller = new Scroller(getContext());
+ mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
@@ -199,6 +209,8 @@
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mOverscrollDistance = configuration.getScaledOverscrollDistance();
+ mOverflingDistance = configuration.getScaledOverflingDistance();
}
@Override
@@ -463,6 +475,9 @@
/* 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 @@
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 @@
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 @@
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 @@
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 @@
}
}
+ @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 @@
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 @@
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 @@
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 @@
}
}
+ @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 502cc38..fd4f950 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -104,6 +104,9 @@
Drawable mDivider;
int mDividerHeight;
+ Drawable mOverScrollHeader;
+ Drawable mOverScrollFooter;
+
private boolean mIsCacheColorOpaque;
private boolean mDividerIsOpaque;
@@ -152,6 +155,18 @@
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 @@
}
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 @@
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 @@
}
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 @@
invalidate();
}
+ /**
+ * Sets the drawable that will be drawn above all other list content.
+ * This area can become visible when the user overscrolls the list.
+ *
+ * @param header The drawable to use
+ */
+ public void setOverscrollHeader(Drawable header) {
+ mOverScrollHeader = header;
+ if (mScrollY < 0) {
+ invalidate();
+ }
+ }
+
+ /**
+ * @return The drawable that will be drawn above all other list content
+ */
+ public Drawable getOverscrollHeader() {
+ return mOverScrollHeader;
+ }
+
+ /**
+ * Sets the drawable that will be drawn below all other list content.
+ * This area can become visible when the user overscrolls the list,
+ * or when the list's content does not fully fill the container area.
+ *
+ * @param footer The drawable to use
+ */
+ public void setOverscrollFooter(Drawable footer) {
+ mOverScrollFooter = footer;
+ invalidate();
+ }
+
+ /**
+ * @return The drawable that will be drawn below all other list content
+ */
+ public Drawable getOverscrollFooter() {
+ return mOverScrollFooter;
+ }
+
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java
new file mode 100644
index 0000000..cd81e31
--- /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 eb527eb..3faec55 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -19,8 +19,11 @@
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 @@
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 @@
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 @@
private void initScrollView() {
- mScroller = new Scroller(getContext());
+ mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
@@ -195,6 +203,8 @@
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mOverscrollDistance = configuration.getScaledOverscrollDistance();
+ mOverflingDistance = configuration.getScaledOverflingDistance();
}
@Override
@@ -459,6 +469,9 @@
/* 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 @@
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 @@
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 @@
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 @@
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 @@
}
}
+ @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 @@
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 @@
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 @@
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 @@
}
}
+ @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 b1f50ba..f00640e 100644
--- a/core/java/android/widget/Scroller.java
+++ b/core/java/android/widget/Scroller.java
@@ -52,14 +52,10 @@
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 @@
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 @@
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 @@
/**
* 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 @@
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 @@
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 @@
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 @@
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 3982ed9..e3ba634 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 691ee8c..40b9e69 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 94dcb6a..80d22f6 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 651bfea..770adf5 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 f4d1df8..f2ab5cd 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" />