Overscrolling modifications. Overscroll will not allow the user to
scroll content out of view. Scrolling will slow down halfway to the
barrier point. API added in View. AbsListView, ScrollView,
HorizontalScrollView all use this API. Overscrolling uses haptic
feedback. Added scroll barrier pattern to config.xml.
diff --git a/api/current.xml b/api/current.xml
index 08c956e..7f63a78 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -164726,6 +164726,17 @@
  visibility="public"
 >
 </field>
+<field name="SCROLL_BARRIER"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="2"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
 <field name="VIRTUAL_KEY"
  type="int"
  transient="false"
@@ -173140,6 +173151,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"
@@ -173293,6 +173323,33 @@
 <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>
+</method>
 <method name="performClick"
  return="boolean"
  abstract="false"
diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java
index e1f2823..ccbd8d4 100644
--- a/core/java/android/view/HapticFeedbackConstants.java
+++ b/core/java/android/view/HapticFeedbackConstants.java
@@ -36,6 +36,11 @@
     public static final int VIRTUAL_KEY = 1;
     
     /**
+     * The user has hit the barrier point while scrolling a view.
+     */
+    public static final int SCROLL_BARRIER = 2;
+    
+    /**
      * This is a private constant.  Feel free to renumber as desired.
      * @hide
      */
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index fb19dcf..c1537ca 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -8547,7 +8547,119 @@
         LayoutInflater factory = LayoutInflater.from(context);
         return factory.inflate(resource, root);
     }
+    
+    /**
+     * Scroll the view with standard behavior for scrolling beyond the normal
+     * content boundaries. Views that call this method should override
+     * {@link #onOverscrolled()} 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.
+     * @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) {
+        // Scale the scroll amount if we're in the dropoff zone
+        final int dropoffX = maxOverscrollX / 2;
+        final int dropoffLeft = -dropoffX;
+        final int dropoffRight = dropoffX + scrollRangeX;
+        int newScrollX;
+        if ((scrollX < dropoffLeft && deltaX < 0) ||
+                (scrollX > dropoffRight && deltaX > 0)) {
+            newScrollX = scrollX + deltaX / 2;
+        } else {
+            newScrollX = scrollX + deltaX;
+            if (newScrollX > dropoffRight && deltaX > 0) {
+                int extra = newScrollX - dropoffRight;
+                newScrollX = dropoffRight + extra / 2;
+            } else if (newScrollX < dropoffLeft && deltaX < 0) {
+                int extra = newScrollX - dropoffLeft;
+                newScrollX = dropoffLeft + extra / 2;
+            }
+        }
+        
+        final int dropoffY = maxOverscrollY / 2;
+        final int dropoffTop = -dropoffY;
+        final int dropoffBottom = dropoffY + scrollRangeY;
+        int newScrollY;
+        if ((scrollY < dropoffTop && deltaY < 0) ||
+                (scrollY > dropoffBottom && deltaY > 0)) {
+            newScrollY = scrollY + deltaY / 2;
+        } else {
+            newScrollY = scrollY + deltaY;
+            if (newScrollY > dropoffBottom && deltaY > 0) {
+                int extra = newScrollY - dropoffBottom;
+                newScrollY = dropoffBottom + extra / 2;
+            } else if (newScrollY < dropoffTop && deltaY < 0) {
+                int extra = newScrollY - dropoffTop;
+                newScrollY = dropoffTop + extra / 2;
+            }
+        }
 
+        // 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;
+        }
+        
+        // Bump the device with some haptic feedback if we're at the edge
+        // and didn't start there.
+        if ((clampedX && scrollX != left && scrollX != right) ||
+                (clampedY && scrollY != top && scrollY != bottom)) {
+            performHapticFeedback(HapticFeedbackConstants.SCROLL_BARRIER);
+        }
+
+        onOverscrolled(newScrollX, newScrollY, clampedX, clampedY);
+        
+        return clampedX || clampedY;
+    }
+    
+    /**
+     * Called by {@link #overscrollBy(int, int, int, int, int, int, int, int)} 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.
+    }
+    
     /**
      * A MeasureSpec encapsulates the layout requirements passed from parent to child.
      * Each MeasureSpec represents a requirement for either the width or the height.
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 2f96aef..4c77bdc 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -377,6 +377,16 @@
     int mResurrectToPosition = INVALID_POSITION;
 
     private ContextMenuInfo mContextMenuInfo = null;
+    
+    /**
+     * Maximum distance to overscroll by
+     */
+    private int mOverscrollMax;
+    
+    /**
+     * Content height divided by this is the overscroll limit.
+     */
+    private static final int OVERSCROLL_LIMIT_DIVISOR = 3;
 
     /**
      * Used to request a layout when we changed touch mode
@@ -1048,7 +1058,8 @@
                 final int top = view.getTop();
                 int height = view.getHeight();
                 if (height > 0) {
-                    return Math.max(firstPosition * 100 - (top * 100) / height, 0);
+                    return Math.max(firstPosition * 100 - (top * 100) / height +
+                            (int)((float)mScrollY / getHeight() * mItemCount * 100), 0);
                 }
             } else {
                 int index;
@@ -1068,7 +1079,17 @@
 
     @Override
     protected int computeVerticalScrollRange() {
-        return mSmoothScrollbarEnabled ? Math.max(mItemCount * 100, 0) : mItemCount;
+        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;
+        }
+        return result;
     }
 
     @Override
@@ -1129,6 +1150,8 @@
         mInLayout = true;
         layoutChildren();
         mInLayout = false;
+        
+        mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
     }
 
     /**
@@ -2078,11 +2101,13 @@
                         // Check if the top of the motion view is where it is
                         // supposed to be
                         final int motionViewRealTop = motionView.getTop();
-                        final int motionViewNewTop = mMotionViewNewTop;
                         if (atEdge) {
                             // Apply overscroll
                             
-                            mScrollY -= incrementalDeltaY - (motionViewRealTop - motionViewPrevTop);
+                            int overscroll = -incrementalDeltaY - 
+                                    (motionViewRealTop - motionViewPrevTop);
+                            overscrollBy(0, overscroll, 0, mScrollY, 0, 0,
+                                    0, getOverscrollMax());
                             mTouchMode = TOUCH_MODE_OVERSCROLL;
                             invalidate();
                         }
@@ -2126,7 +2151,8 @@
                             mMotionPosition = motionPosition;
                         }
                     } else {
-                        mScrollY -= incrementalDeltaY;
+                        overscrollBy(0, -incrementalDeltaY, 0, mScrollY, 0, 0,
+                                0, getOverscrollMax());
                         invalidate();
                     }
                     mLastY = y;
@@ -2311,6 +2337,28 @@
 
         return true;
     }
+    
+    @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();
+            }
+        }
+    }
+    
+    private int getOverscrollMax() {
+        final int childCount = getChildCount();
+        if (childCount > 0) {
+            return Math.min(mOverscrollMax,
+                    getChildAt(childCount - 1).getBottom() / OVERSCROLL_LIMIT_DIVISOR);
+        } else {
+            return mOverscrollMax;
+        }
+    }
 
     @Override
     public void draw(Canvas canvas) {
@@ -2532,22 +2580,23 @@
                     delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta);
                 }
 
-                // Do something different on overscroll - offsetChildrenTopAndBottom()
-                trackMotionScroll(delta, delta);
-
                 // Check to see if we have bumped into the scroll limit
                 View motionView = getChildAt(mMotionPosition - mFirstPosition);
+                int oldTop = 0;
                 if (motionView != null) {
-                    // Check if the top of the motion view is where it is
-                    // supposed to be
-                    if (motionView.getTop() != mMotionViewNewTop) {
-                       float vel = scroller.getCurrVelocity();
-                       if (delta > 0) {
-                           vel = -vel;
-                       }
-                       startOverfling(Math.round(vel));
-                       break;
+                    oldTop = motionView.getTop();
+                }
+                if (trackMotionScroll(delta, delta)) {
+                    if (motionView != null) {
+                        // Tweak the scroll for how far we overshot
+                        mScrollY -= delta - (motionView.getTop() - oldTop);
                     }
+                    float vel = scroller.getCurrVelocity();
+                    if (delta > 0) {
+                        vel = -vel;
+                    }
+                    startOverfling(Math.round(vel));
+                    break;
                 }
 
                 if (more) {
@@ -2570,9 +2619,14 @@
             case TOUCH_MODE_OVERFLING: {
                 final OverScroller scroller = mScroller;
                 if (scroller.computeScrollOffset()) {
-                    mScrollY = scroller.getCurrY();
-                    invalidate();
-                    post(this);
+                    final int scrollY = mScrollY;
+                    final int deltaY = scroller.getCurrY() - scrollY;
+                    if (overscrollBy(0, deltaY, 0, scrollY, 0, 0, 0, getOverscrollMax())) {
+                        startSpringback();
+                    } else {
+                        invalidate();
+                        post(this);
+                    }
                 } else {
                     endFling();
                 }
@@ -2702,6 +2756,10 @@
             case MOVE_DOWN_POS: {
                 final int lastViewIndex = getChildCount() - 1;
                 final int lastPos = firstPos + lastViewIndex;
+                
+                if (lastViewIndex < 0) {
+                    return;
+                }
 
                 if (lastPos == mLastSeenPos) {
                     // No new views, let things keep going.
@@ -2764,6 +2822,9 @@
                 }
 
                 final View firstView = getChildAt(0);
+                if (firstView == null) {
+                    return;
+                }
                 final int firstViewTop = firstView.getTop();
 
                 smoothScrollBy(firstViewTop - mExtraScroll, mScrollDuration);
diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java
index 0078fec..c62724c 100644
--- a/core/java/android/widget/HorizontalScrollView.java
+++ b/core/java/android/widget/HorizontalScrollView.java
@@ -461,7 +461,8 @@
                 final int deltaX = (int) (mLastMotionX - x);
                 mLastMotionX = x;
 
-                super.scrollTo(mScrollX + deltaX, mScrollY);
+                overscrollBy(deltaX, 0, mScrollX, 0, getScrollRange(), 0,
+                        getOverscrollMax(), 0);
                 break;
             case MotionEvent.ACTION_UP:
                 final VelocityTracker velocityTracker = mVelocityTracker;
@@ -472,8 +473,7 @@
                     if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                         fling(-initialVelocity);
                     } else {
-                        final int right = Math.max(0, getChildAt(0).getHeight() - 
-                                (getHeight() - mPaddingRight - mPaddingLeft));
+                        final int right = getScrollRange();
                         if (mScroller.springback(mScrollX, mScrollY, 0, 0, right, 0)) {
                             invalidate();
                         }
@@ -487,6 +487,41 @@
         }
         return true;
     }
+    
+    @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);
+        }
+    }
+    
+    private int getOverscrollMax() {
+        int childCount = getChildCount();
+        int containerOverscroll = (getWidth() - mPaddingLeft - mPaddingRight) / 3;
+        if (childCount > 0) {
+            return Math.min(containerOverscroll, getChildAt(0).getWidth() / 3);
+        } else {
+            return containerOverscroll;
+        }
+    }
+    
+    private int getScrollRange() {
+        int scrollRange = 0;
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            scrollRange = Math.max(0,
+                    child.getWidth() - getWidth() - mPaddingLeft - mPaddingRight);
+        }
+        return scrollRange;
+    }
 
     /**
      * <p>
@@ -856,9 +891,26 @@
     @Override
     protected int computeHorizontalScrollRange() {
         int count = getChildCount();
-        return count == 0 ? getWidth() : getChildAt(0).getRight();
+        if (count == 0) {
+            return getWidth();
+        }
+        
+        int scrollRange = getChildAt(0).getRight();
+        int scrollX = mScrollX;
+        int overscrollRight = scrollRange - getWidth() - mPaddingLeft - mPaddingRight;
+        if (scrollX < 0) {
+            scrollRange -= scrollX;
+        } else if (scrollX > overscrollRight) {
+            scrollRange += scrollX - overscrollRight;
+        }
+        
+        return scrollRange;
     }
-
+    
+    @Override
+    protected int computeHorizontalScrollOffset() {
+        return Math.max(0, super.computeHorizontalScrollOffset());
+    }
 
     @Override
     protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
@@ -913,10 +965,9 @@
             int x = mScroller.getCurrX();
             int y = mScroller.getCurrY();
 
-            mScrollX = x;
-            mScrollY = y;
-
-            if (oldX != mScrollX || oldY != mScrollY) {
+            if (oldX != x || oldY != y) {
+                overscrollBy(x - oldX, y - oldY, oldX, oldY, getScrollRange(), 0,
+                        getOverscrollMax(), 0);
                 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
             }
 
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index 401e7ff4..c428dc0 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -2916,7 +2916,14 @@
 
             if (!mStackFromBottom) {
                 int bottom;
-                int listBottom = mBottom - mTop - mListPadding.bottom;
+                int listBottom = mBottom - mTop - mListPadding.bottom + mScrollY;
+                
+                // Draw top divider for overscroll
+                if (count > 0 && mScrollY < 0) {
+                    bounds.bottom = 0;
+                    bounds.top = -dividerHeight;
+                    drawDivider(canvas, bounds, -1);
+                }
 
                 for (int i = 0; i < count; i++) {
                     if ((headerDividers || first + i >= headerCount) &&
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index 4a1d871..2ee7ad5 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -459,7 +459,8 @@
                 final int deltaY = (int) (mLastMotionY - y);
                 mLastMotionY = y;
 
-                super.scrollTo(mScrollX, mScrollY + deltaY);
+                overscrollBy(0, deltaY, 0, mScrollY, 0, getScrollRange(),
+                        0, getOverscrollMax());
                 break;
             case MotionEvent.ACTION_UP:
                 final VelocityTracker velocityTracker = mVelocityTracker;
@@ -470,8 +471,7 @@
                     if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                         fling(-initialVelocity);
                     } else {
-                        final int bottom = Math.max(0, getChildAt(0).getHeight() - 
-                                (getHeight() - mPaddingBottom - mPaddingTop));
+                        final int bottom = getScrollRange();
                         if (mScroller.springback(mScrollX, mScrollY, 0, 0, 0, bottom)) {
                             invalidate();
                         }
@@ -485,6 +485,41 @@
         }
         return true;
     }
+    
+    @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);
+        }
+    }
+    
+    private int getOverscrollMax() {
+        int childCount = getChildCount();
+        int containerOverscroll = (getHeight() - mPaddingBottom - mPaddingTop) / 3;
+        if (childCount > 0) {
+            return Math.min(containerOverscroll, getChildAt(0).getHeight() / 3);
+        } else {
+            return containerOverscroll;
+        }
+    }
+    
+    private int getScrollRange() {
+        int scrollRange = 0;
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            scrollRange = Math.max(0,
+                    child.getHeight() - getHeight() - mPaddingBottom - mPaddingTop);
+        }
+        return scrollRange;
+    }
 
     /**
      * <p>
@@ -858,9 +893,26 @@
     @Override
     protected int computeVerticalScrollRange() {
         int count = getChildCount();
-        return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
+        if (count == 0) {
+            return getHeight();
+        }
+        
+        int scrollRange = getChildAt(0).getBottom();
+        int scrollY = mScrollY;
+        int overscrollBottom = scrollRange - getHeight() - mPaddingBottom - mPaddingTop;
+        if (scrollY < 0) {
+            scrollRange -= scrollY;
+        } else if (scrollY > overscrollBottom) {
+            scrollRange += scrollY - overscrollBottom;
+        }
+        
+        return scrollRange;
     }
 
+    @Override
+    protected int computeVerticalScrollOffset() {
+        return Math.max(0, super.computeVerticalScrollOffset());
+    }
 
     @Override
     protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
@@ -915,10 +967,9 @@
             int x = mScroller.getCurrX();
             int y = mScroller.getCurrY();
 
-            mScrollX = x;
-            mScrollY = y;
-
-            if (oldX != mScrollX || oldY != mScrollY) {
+            if (oldX != x || oldY != y) {
+                overscrollBy(x - oldX, y - oldY, oldX, oldY, 0, getScrollRange(),
+                        0, getOverscrollMax());
                 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
             }
             
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2b8ddc4..7bd07a5 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -156,6 +156,14 @@
         <item>600</item>
     </integer-array>
 
+    <!-- Vibrator pattern for feedback about hitting a scroll barrier -->
+    <integer-array name="config_scrollBarrierVibePattern">
+        <item>0</item>
+        <item>15</item>
+        <item>10</item>
+        <item>10</item>
+    </integer-array>
+
     <bool name="config_use_strict_phone_number_comparation">false</bool>
 
     <!-- Display low battery warning when battery level dips to this value -->