Update smoothScrollToPosition to move faster for large offsets

Adds a method to AbsListView for translating from position to row
and vice-versa.

BUG: 3434554
Change-Id: I08459a6545cd02ac4eb5007c59bda1f3fece9e9f
diff --git a/api/current.txt b/api/current.txt
index 1495277..cb6f2c3 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -31256,10 +31256,12 @@
     method public int getCheckedItemPosition();
     method public android.util.SparseBooleanArray getCheckedItemPositions();
     method public int getChoiceMode();
+    method public int getFirstPositionForRow(int);
     method public int getListPaddingBottom();
     method public int getListPaddingLeft();
     method public int getListPaddingRight();
     method public int getListPaddingTop();
+    method public int getRowForPosition(int);
     method public android.view.View getSelectedView();
     method public android.graphics.drawable.Drawable getSelector();
     method public java.lang.CharSequence getTextFilter();
@@ -31305,6 +31307,7 @@
     method public void setRemoteViewsAdapter(android.content.Intent);
     method public void setScrollIndicators(android.view.View, android.view.View);
     method public void setScrollingCacheEnabled(boolean);
+    method public void setSelectionFromTop(int, int);
     method public void setSelector(int);
     method public void setSelector(android.graphics.drawable.Drawable);
     method public void setSmoothScrollbarEnabled(boolean);
@@ -32405,7 +32408,6 @@
     method public void setOverscrollHeader(android.graphics.drawable.Drawable);
     method public void setSelection(int);
     method public void setSelectionAfterHeaderView();
-    method public void setSelectionFromTop(int, int);
     method public void smoothScrollByOffset(int);
   }
 
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 90866d6..16fab1f 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -36,6 +36,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.LongSparseArray;
+import android.util.MathUtils;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.StateSet;
@@ -60,6 +61,8 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
 import android.view.inputmethod.BaseInputConnection;
@@ -418,7 +421,7 @@
     /**
      * Handles scrolling between positions within the list.
      */
-    PositionScroller mPositionScroller;
+    SubPositionScroller mPositionScroller;
 
     /**
      * The offset in pixels form the top of the AdapterView to the top
@@ -4839,14 +4842,14 @@
      */
     public void smoothScrollToPosition(int position) {
         if (mPositionScroller == null) {
-            mPositionScroller = new PositionScroller();
+            mPositionScroller = new SubPositionScroller();
         }
         mPositionScroller.start(position);
     }
 
     /**
      * Smoothly scroll to the specified adapter position. The view will scroll
-     * such that the indicated position is displayed <code>offset</code> pixels from
+     * such that the indicated position is displayed <code>offset</code> pixels below
      * the top edge of the view. If this is impossible, (e.g. the offset would scroll
      * the first or last item beyond the boundaries of the list) it will get as close
      * as possible. The scroll will take <code>duration</code> milliseconds to complete.
@@ -4858,14 +4861,14 @@
      */
     public void smoothScrollToPositionFromTop(int position, int offset, int duration) {
         if (mPositionScroller == null) {
-            mPositionScroller = new PositionScroller();
+            mPositionScroller = new SubPositionScroller();
         }
         mPositionScroller.startWithOffset(position, offset, duration);
     }
 
     /**
      * Smoothly scroll to the specified adapter position. The view will scroll
-     * such that the indicated position is displayed <code>offset</code> pixels from
+     * such that the indicated position is displayed <code>offset</code> pixels below
      * the top edge of the view. If this is impossible, (e.g. the offset would scroll
      * the first or last item beyond the boundaries of the list) it will get as close
      * as possible.
@@ -4876,9 +4879,9 @@
      */
     public void smoothScrollToPositionFromTop(int position, int offset) {
         if (mPositionScroller == null) {
-            mPositionScroller = new PositionScroller();
+            mPositionScroller = new SubPositionScroller();
         }
-        mPositionScroller.startWithOffset(position, offset);
+        mPositionScroller.startWithOffset(position, offset, offset);
     }
 
     /**
@@ -4892,7 +4895,7 @@
      */
     public void smoothScrollToPosition(int position, int boundPosition) {
         if (mPositionScroller == null) {
-            mPositionScroller = new PositionScroller();
+            mPositionScroller = new SubPositionScroller();
         }
         mPositionScroller.start(position, boundPosition);
     }
@@ -6991,4 +6994,311 @@
             return null;
         }
     }
+
+    /**
+     * Returns the height of a row, which is computed as the maximum height of
+     * the items in the row.
+     *
+     * @param row the row index
+     * @return row height in pixels
+     */
+    private int getHeightForRow(int row) {
+        final int firstRowPosition = getFirstPositionForRow(row);
+        final int lastRowPosition = getFirstPositionForRow(row + 1);
+        int maxHeight = 0;
+        for (int i = firstRowPosition; i < lastRowPosition; i++) {
+            final int height = getHeightForPosition(i);
+            if (height > maxHeight) {
+                maxHeight = height;
+            }
+        }
+        return maxHeight;
+    }
+
+    /**
+     * Returns the height of the view for the specified position.
+     *
+     * @param position the item position
+     * @return view height in pixels
+     */
+    int getHeightForPosition(int position) {
+        final int firstVisiblePosition = getFirstVisiblePosition();
+        final int childCount = getChildCount();
+        final int index = position - firstVisiblePosition;
+        if (position >= 0 && position < childCount) {
+            final View view = getChildAt(index);
+            return view.getHeight();
+        } else {
+            final View view = obtainView(position, mIsScrap);
+            view.measure(mWidthMeasureSpec, MeasureSpec.UNSPECIFIED);
+            final int height = view.getMeasuredHeight();
+            mRecycler.addScrapView(view, position);
+            return height;
+        }
+    }
+
+    /**
+     * Returns the row for the specified item position.
+     *
+     * @param position the item position
+     * @return the row index
+     */
+    public int getRowForPosition(int position) {
+        return position;
+    }
+
+    /**
+     * Returns the first item position within the specified row.
+     *
+     * @param row the row
+     * @return the item position
+     */
+    public int getFirstPositionForRow(int row) {
+        return row;
+    }
+
+    /**
+     * Sets the selected item and positions the selection y pixels from the top edge
+     * of the ListView. (If in touch mode, the item will not be selected but it will
+     * still be positioned appropriately.)
+     *
+     * @param position Index (starting at 0) of the data item to be selected.
+     * @param y The distance from the top edge of the ListView (plus padding) that the
+     *        item will be positioned.
+     */
+    public void setSelectionFromTop(int position, int y) {
+        if (mAdapter == null) {
+            return;
+        }
+
+        if (!isInTouchMode()) {
+            position = lookForSelectablePosition(position, true);
+            if (position >= 0) {
+                setNextSelectedPositionInt(position);
+            }
+        } else {
+            mResurrectToPosition = position;
+        }
+
+        if (position >= 0) {
+            mLayoutMode = LAYOUT_SPECIFIC;
+            mSpecificTop = mListPadding.top + y;
+
+            if (mNeedSync) {
+                mSyncPosition = position;
+                mSyncRowId = mAdapter.getItemId(position);
+            }
+
+            if (mPositionScroller != null) {
+                mPositionScroller.stop();
+            }
+            requestLayout();
+        }
+    }
+
+    class SubPositionScroller {
+        private static final int DEFAULT_SCROLL_DURATION = 200;
+
+        private SubScroller mSubScroller;
+        private int mOffset;
+
+        /**
+         * Scroll the minimum amount to get the target view entirely on-screen.
+         */
+        private void scrollToPosition(final int targetPosition, final boolean useOffset,
+                final int offset, final int boundPosition, final int duration) {
+            stop();
+
+            if (mDataChanged) {
+                // Wait until we're back in a stable state to try this.
+                mPositionScrollAfterLayout = new Runnable() {
+                    @Override
+                    public void run() {
+                        scrollToPosition(
+                                targetPosition, useOffset, offset, boundPosition, duration);
+                    }
+                };
+                return;
+            }
+
+            final int firstPosition = getFirstVisiblePosition();
+            final int lastPosition = firstPosition + getChildCount();
+            final int targetRow = getRowForPosition(targetPosition);
+            final int firstRow = getRowForPosition(firstPosition);
+            final int lastRow = getRowForPosition(lastPosition);
+            if (useOffset || targetRow <= firstRow) {
+                mOffset = offset;
+            } else if (targetRow >= lastRow - 1) {
+                final int listHeight = getHeight() - getPaddingTop() - getPaddingBottom();
+                mOffset = listHeight - getHeightForPosition(targetPosition);
+            } else {
+                // Don't scroll, target is entirely on-screen.
+                return;
+            }
+
+            float endSubRow = targetRow;
+            if (boundPosition != INVALID_POSITION) {
+                final int boundRow = getRowForPosition(boundPosition);
+                if (boundRow >= firstRow && boundRow < lastRow) {
+                    endSubRow = computeBoundSubRow(targetRow, boundRow);
+                }
+            }
+
+            final View firstChild = getChildAt(0);
+            final float startOffsetRatio = -firstChild.getTop() / (float) firstChild.getHeight();
+            final float startSubRow = firstRow + startOffsetRatio;
+            if (startSubRow == endSubRow && mOffset == 0) {
+                // Don't scroll, target is already in position.
+                return;
+            }
+
+            if (mSubScroller == null) {
+                mSubScroller = new SubScroller();
+            }
+            mSubScroller.startScroll(startSubRow, endSubRow, duration);
+
+            postOnAnimation(mAnimationFrame);
+        }
+
+        private float computeBoundSubRow(int targetRow, int boundRow) {
+            // Compute the target and offset as a sub-position.
+            int remainingOffset = mOffset;
+            int targetHeight = getHeightForRow(targetRow - 1);
+            while (remainingOffset > 0) {
+                remainingOffset -= targetHeight;
+                targetRow--;
+                targetHeight = getHeightForRow(targetRow - 1);
+            }
+            final float targetSubRow = targetRow - remainingOffset / targetHeight;
+            mOffset = 0;
+
+            if (targetSubRow >= boundRow) {
+                // End position would push the bound position above the list.
+                return boundRow;
+            }
+
+            // Compute the closest possible sub-position that wouldn't push the
+            // bound position's view further below the list.
+            final int listHeight = getHeight() - getPaddingTop() - getPaddingBottom();
+            final int boundHeight = getHeightForRow(boundRow);
+            int endRow = boundRow;
+            int totalHeight = boundHeight;
+            int endHeight;
+            do {
+                endRow--;
+                endHeight = getHeightForRow(endRow);
+                totalHeight += endHeight;
+            } while (totalHeight < listHeight && endRow > 0);
+
+            final float endOffsetRatio = (totalHeight - listHeight) / (float) endHeight;
+            final float boundSubRow = endRow + endOffsetRatio;
+            return Math.max(boundSubRow, targetSubRow);
+        }
+
+        /**
+         * @param position
+         * @param boundPosition
+         */
+        public void start(int position, int boundPosition) {
+            scrollToPosition(position, false, 0, boundPosition, DEFAULT_SCROLL_DURATION);
+        }
+
+        /**
+         * @param position
+         * @param offset
+         * @param duration
+         */
+        public void startWithOffset(int position, int offset, int duration) {
+            scrollToPosition(position, true, offset, INVALID_POSITION, duration);
+        }
+
+        /**
+         * @param position
+         */
+        public void start(int position) {
+            scrollToPosition(position, false, 0, INVALID_POSITION, DEFAULT_SCROLL_DURATION);
+        }
+
+        public void stop() {
+            removeCallbacks(mAnimationFrame);
+        }
+
+        private void onAnimationFrame() {
+            final boolean shouldPost = mSubScroller.computePosition();
+            final float subRow = mSubScroller.getPosition();
+
+            final int row = (int) subRow;
+            final int position = getFirstPositionForRow(row);
+            final int rowHeight = getHeightForRow(row);
+            final int offset = (int) (rowHeight * (subRow - row));
+            final int addOffset = (int) (mOffset * mSubScroller.getInterpolatedValue());
+            setSelectionFromTop(position, -offset + addOffset);
+
+            if (shouldPost) {
+                postOnAnimation(mAnimationFrame);
+            }
+        }
+
+        private Runnable mAnimationFrame = new Runnable() {
+            @Override
+            public void run() {
+                onAnimationFrame();
+            }
+        };
+    }
+
+    /**
+     * Scroller capable of returning floating point positions.
+     */
+    private static class SubScroller {
+        private final Interpolator mInterpolator;
+
+        private float mStartPosition;
+        private float mEndPosition;
+        private long mStartTime;
+        private long mDuration;
+
+        private float mPosition;
+        private float mInterpolatedValue;
+
+        public SubScroller() {
+            this(null);
+        }
+
+        public SubScroller(Interpolator interpolator) {
+            if (interpolator == null) {
+                mInterpolator = new AccelerateDecelerateInterpolator();
+            } else {
+                mInterpolator = interpolator;
+            }
+        }
+
+        public void startScroll(float startPosition, float endPosition, int duration) {
+            mStartPosition = startPosition;
+            mEndPosition = endPosition;
+            mDuration = duration;
+
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mPosition = startPosition;
+            mInterpolatedValue = 0;
+        }
+
+        public boolean computePosition() {
+            final long elapsed = AnimationUtils.currentAnimationTimeMillis() - mStartTime;
+            final float value = MathUtils.constrain(elapsed / (float) mDuration, 0, 1);
+
+            mInterpolatedValue = mInterpolator.getInterpolation(value);
+            mPosition = (mEndPosition - mStartPosition) * mInterpolatedValue + mStartPosition;
+
+            return elapsed < mDuration;
+        }
+
+        public float getPosition() {
+            return mPosition;
+        }
+
+        public float getInterpolatedValue() {
+            return mInterpolatedValue;
+        }
+    }
 }
diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java
index acd711d..0b424f7 100644
--- a/core/java/android/widget/GridView.java
+++ b/core/java/android/widget/GridView.java
@@ -1027,6 +1027,16 @@
     }
 
     @Override
+    public int getRowForPosition(int position) {
+        return position / mNumColumns;
+    }
+
+    @Override
+    public int getFirstPositionForRow(int row) {
+        return row * mNumColumns;
+    }
+
+    @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // Sets up mListPadding
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index c461723..f937cd6 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -1892,45 +1892,6 @@
     }
 
     /**
-     * Sets the selected item and positions the selection y pixels from the top edge
-     * of the ListView. (If in touch mode, the item will not be selected but it will
-     * still be positioned appropriately.)
-     *
-     * @param position Index (starting at 0) of the data item to be selected.
-     * @param y The distance from the top edge of the ListView (plus padding) that the
-     *        item will be positioned.
-     */
-    public void setSelectionFromTop(int position, int y) {
-        if (mAdapter == null) {
-            return;
-        }
-
-        if (!isInTouchMode()) {
-            position = lookForSelectablePosition(position, true);
-            if (position >= 0) {
-                setNextSelectedPositionInt(position);
-            }
-        } else {
-            mResurrectToPosition = position;
-        }
-
-        if (position >= 0) {
-            mLayoutMode = LAYOUT_SPECIFIC;
-            mSpecificTop = mListPadding.top + y;
-
-            if (mNeedSync) {
-                mSyncPosition = position;
-                mSyncRowId = mAdapter.getItemId(position);
-            }
-
-            if (mPositionScroller != null) {
-                mPositionScroller.stop();
-            }
-            requestLayout();
-        }
-    }
-
-    /**
      * Makes the item at the supplied position selected.
      * 
      * @param position the position of the item to select
@@ -3746,6 +3707,84 @@
     }
 
     @Override
+    int getHeightForPosition(int position) {
+        final int height = super.getHeightForPosition(position);
+        if (shouldAdjustHeightForDivider(position)) {
+            return height + mDividerHeight;
+        }
+        return height;
+    }
+
+    private boolean shouldAdjustHeightForDivider(int itemIndex) {
+        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) {
+            final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
+            final int itemCount = mItemCount;
+            final int headerCount = mHeaderViewInfos.size();
+            final int footerLimit = (itemCount - mFooterViewInfos.size());
+            final boolean isHeader = (itemIndex < headerCount);
+            final boolean isFooter = (itemIndex >= footerLimit);
+            final boolean headerDividers = mHeaderDividersEnabled;
+            final boolean footerDividers = mFooterDividersEnabled;
+            if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
+                final ListAdapter adapter = mAdapter;
+                if (!mStackFromBottom) {
+                    final boolean isLastItem = (itemIndex == (itemCount - 1));
+                    if (!drawOverscrollFooter || !isLastItem) {
+                        final int nextIndex = itemIndex + 1;
+                        // Draw dividers between enabled items, headers
+                        // and/or footers when enabled and requested, and
+                        // after the last enabled item.
+                        if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+                                && (nextIndex >= headerCount)) && (isLastItem
+                                || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter
+                                                && (nextIndex < footerLimit)))) {
+                            return true;
+                        } else if (fillForMissingDividers) {
+                            return true;
+                        }
+                    }
+                } else {
+                    final int start = drawOverscrollHeader ? 1 : 0;
+                    final boolean isFirstItem = (itemIndex == start);
+                    if (!isFirstItem) {
+                        final int previousIndex = (itemIndex - 1);
+                        // Draw dividers between enabled items, headers
+                        // and/or footers when enabled and requested, and
+                        // before the first enabled item.
+                        if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+                                && (previousIndex >= headerCount)) && (isFirstItem ||
+                                adapter.isEnabled(previousIndex) && (footerDividers || !isFooter
+                                        && (previousIndex < footerLimit)))) {
+                            return true;
+                        } else if (fillForMissingDividers) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public int getRowForPosition(int position) {
+        return position;
+    }
+
+    @Override
+    public int getFirstPositionForRow(int row) {
+        return row;
+    }
+
+    @Override
     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
         super.onInitializeAccessibilityEvent(event);
         event.setClassName(ListView.class.getName());