Fix misc. bugs in AbsListView smooth scrolling.

Handle the edge cases a bit better. Deal with padding. If a smooth
scroll begins, the list is empty, but a data change is pending, try
again on the next looper pass.

Bug 6453837
Bug 6434713

Change-Id: I53f22ebacdcbc5d981a6c8055c4fd3fc1ef140f6
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 449870e..5fe0f5f 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -3985,7 +3985,7 @@
     }
 
     class PositionScroller implements Runnable {
-        private static final int SCROLL_DURATION = 400;
+        private static final int SCROLL_DURATION = 200;
 
         private static final int MOVE_DOWN_POS = 1;
         private static final int MOVE_UP_POS = 2;
@@ -4006,21 +4006,35 @@
             mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength();
         }
 
-        void start(int position) {
+        void start(final int position) {
             stop();
 
+            final int childCount = getChildCount();
+            if (childCount == 0) {
+                // Can't scroll without children.
+                if (mDataChanged) {
+                    // But we might have something in a minute.
+                    post(new Runnable() {
+                        @Override public void run() {
+                            start(position);
+                        }
+                    });
+                }
+                return;
+            }
+
             final int firstPos = mFirstPosition;
-            final int lastPos = firstPos + getChildCount() - 1;
+            final int lastPos = firstPos + childCount - 1;
 
             int viewTravelCount;
-            if (position <= firstPos) {
+            if (position < firstPos) {
                 viewTravelCount = firstPos - position + 1;
                 mMode = MOVE_UP_POS;
-            } else if (position >= lastPos) {
+            } else if (position > lastPos) {
                 viewTravelCount = position - lastPos + 1;
                 mMode = MOVE_DOWN_POS;
             } else {
-                // Already on screen, nothing to do
+                scrollToVisible(position, INVALID_POSITION, SCROLL_DURATION);
                 return;
             }
 
@@ -4036,7 +4050,7 @@
             postOnAnimation(this);
         }
 
-        void start(int position, int boundPosition) {
+        void start(final int position, final int boundPosition) {
             stop();
 
             if (boundPosition == INVALID_POSITION) {
@@ -4044,11 +4058,25 @@
                 return;
             }
 
+            final int childCount = getChildCount();
+            if (childCount == 0) {
+                // Can't scroll without children.
+                if (mDataChanged) {
+                    // But we might have something in a minute.
+                    post(new Runnable() {
+                        @Override public void run() {
+                            start(position, boundPosition);
+                        }
+                    });
+                }
+                return;
+            }
+
             final int firstPos = mFirstPosition;
-            final int lastPos = firstPos + getChildCount() - 1;
+            final int lastPos = firstPos + childCount - 1;
 
             int viewTravelCount;
-            if (position <= firstPos) {
+            if (position < firstPos) {
                 final int boundPosFromLast = lastPos - boundPosition;
                 if (boundPosFromLast < 1) {
                     // Moving would shift our bound position off the screen. Abort.
@@ -4064,7 +4092,7 @@
                     viewTravelCount = posTravel;
                     mMode = MOVE_UP_POS;
                 }
-            } else if (position >= lastPos) {
+            } else if (position > lastPos) {
                 final int boundPosFromFirst = boundPosition - firstPos;
                 if (boundPosFromFirst < 1) {
                     // Moving would shift our bound position off the screen. Abort.
@@ -4081,7 +4109,7 @@
                     mMode = MOVE_DOWN_POS;
                 }
             } else {
-                // Already on screen, nothing to do
+                scrollToVisible(position, boundPosition, SCROLL_DURATION);
                 return;
             }
 
@@ -4137,6 +4165,60 @@
             postOnAnimation(this);
         }
 
+        /**
+         * Scroll such that targetPos is in the visible padded region without scrolling
+         * boundPos out of view. Assumes targetPos is onscreen.
+         */
+        void scrollToVisible(int targetPos, int boundPos, int duration) {
+            final int firstPos = mFirstPosition;
+            final int childCount = getChildCount();
+            final int lastPos = firstPos + childCount - 1;
+            final int paddedTop = mListPadding.top;
+            final int paddedBottom = getHeight() - mListPadding.bottom;
+
+            if (targetPos < firstPos || targetPos > lastPos) {
+                Log.w(TAG, "scrollToVisible called with targetPos " + targetPos +
+                        " not visible [" + firstPos + ", " + lastPos + "]");
+            }
+            if (boundPos < firstPos || boundPos > lastPos) {
+                // boundPos doesn't matter, it's already offscreen.
+                boundPos = INVALID_POSITION;
+            }
+
+            final View targetChild = getChildAt(targetPos - firstPos);
+            final int targetTop = targetChild.getTop();
+            final int targetBottom = targetChild.getBottom();
+            int scrollBy = 0;
+
+            if (targetBottom > paddedBottom) {
+                scrollBy = targetBottom - paddedBottom;
+            }
+            if (targetTop < paddedTop) {
+                scrollBy = targetTop - paddedTop;
+            }
+
+            if (scrollBy == 0) {
+                return;
+            }
+
+            if (boundPos >= 0) {
+                final View boundChild = getChildAt(boundPos - firstPos);
+                final int boundTop = boundChild.getTop();
+                final int boundBottom = boundChild.getBottom();
+                final int absScroll = Math.abs(scrollBy);
+
+                if (scrollBy < 0 && boundBottom + absScroll > paddedBottom) {
+                    // Don't scroll the bound view off the bottom of the screen.
+                    scrollBy = Math.max(0, boundBottom - paddedBottom);
+                } else if (scrollBy > 0 && boundTop - absScroll < paddedTop) {
+                    // Don't scroll the bound view off the top of the screen.
+                    scrollBy = Math.min(0, boundTop - paddedTop);
+                }
+            }
+
+            smoothScrollBy(scrollBy, duration);
+        }
+
         void stop() {
             removeCallbacks(this);
         }