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());