diff --git a/api/current.txt b/api/current.txt index 0c336e165a0ab..2a90018f74103 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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); } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 4d8975cdcbede..e9107d67597e8 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -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 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 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 offset pixels from + * such that the indicated position is displayed offset 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 duration milliseconds to complete. @@ -4859,14 +4862,14 @@ public abstract class AbsListView extends AdapterView 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 offset pixels from + * such that the indicated position is displayed offset 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 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 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 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; + } + } } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index acd711d61e8c1..0b424f72be21d 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -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 diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index c4617236d041a..f937cd629d906 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -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);