am 0a77ce27: New edge effects for scrolling widgets (overscroll)

Merge commit '0a77ce277c6ed2aa25bbea5f8cd5687c0720cb68' into gingerbread-plus-aosp

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