Merge "Update smoothScrollToPosition to move faster for large offsets"

This commit is contained in:
Alan Viverette
2014-01-16 19:13:32 +00:00
committed by Android (Google) Code Review
4 changed files with 409 additions and 48 deletions

View File

@@ -31465,10 +31465,12 @@ package android.widget {
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();
@@ -31514,6 +31516,7 @@ package android.widget {
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);
@@ -32629,7 +32632,6 @@ package android.widget {
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);
}

View File

@@ -36,6 +36,7 @@ import android.text.TextWatcher;
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.ViewTreeObserver;
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 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
/**
* Handles scrolling between positions within the list.
*/
PositionScroller mPositionScroller;
SubPositionScroller mPositionScroller;
/**
* The offset in pixels form the top of the AdapterView to the top
@@ -4840,14 +4843,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
*/
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.
@@ -4859,14 +4862,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
*/
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.
@@ -4877,9 +4880,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
*/
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);
}
/**
@@ -4893,7 +4896,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
*/
public void smoothScrollToPosition(int position, int boundPosition) {
if (mPositionScroller == null) {
mPositionScroller = new PositionScroller();
mPositionScroller = new SubPositionScroller();
}
mPositionScroller.start(position, boundPosition);
}
@@ -6992,4 +6995,311 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
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;
}
}
}

View File

@@ -1026,6 +1026,16 @@ public class GridView extends AbsListView {
return didNotInitiallyFit;
}
@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

View File

@@ -1891,45 +1891,6 @@ public class ListView extends AbsListView {
setSelectionFromTop(position, 0);
}
/**
* 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.
*
@@ -3745,6 +3706,84 @@ public class ListView extends AbsListView {
return new long[0];
}
@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);