Merge "Ensure that the play/pause key double tap is only handled once" into oc-support-26.0-dev
diff --git a/graphics/drawable/static/src/android/support/graphics/drawable/VectorDrawableCompat.java b/graphics/drawable/static/src/android/support/graphics/drawable/VectorDrawableCompat.java
index 0dc2cda..f9753eb 100644
--- a/graphics/drawable/static/src/android/support/graphics/drawable/VectorDrawableCompat.java
+++ b/graphics/drawable/static/src/android/support/graphics/drawable/VectorDrawableCompat.java
@@ -518,7 +518,7 @@
*/
@RestrictTo(LIBRARY_GROUP)
public float getPixelSize() {
- if ((mVectorState == null && mVectorState.mVPathRenderer == null)
+ if (mVectorState == null || mVectorState.mVPathRenderer == null
|| mVectorState.mVPathRenderer.mBaseWidth == 0
|| mVectorState.mVPathRenderer.mBaseHeight == 0
|| mVectorState.mVPathRenderer.mViewportHeight == 0
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Grid.java b/v17/leanback/src/android/support/v17/leanback/widget/Grid.java
index 6f5f366..148fef0 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/Grid.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/Grid.java
@@ -51,7 +51,17 @@
/**
* Return how many items (are in the adapter).
*/
- public abstract int getCount();
+ int getCount();
+
+ /**
+ * @return Min index to prepend, usually it's 0; but in the preLayout case,
+ * when grid was showing 5,6,7. Removing 3,4,5 will make the layoutPosition to
+ * be 3(deleted),4,5 in prelayout pass; Grid's index is still 5,6,7 in prelayout.
+ * When we prepend in prelayout, we can call createItem(4), createItem(3), createItem(2),
+ * the minimal index is 2, which is also the delta to mapping to layoutPosition in
+ * prelayout pass.
+ */
+ int getMinIndex();
/**
* Create visible item and where the provider should measure it.
@@ -62,7 +72,7 @@
* @param item item[0] returns created item that will be passed in addItem() call.
* @return length of the item.
*/
- public abstract int createItem(int index, boolean append, Object[] item);
+ int createItem(int index, boolean append, Object[] item);
/**
* add item to given row and given edge. The call is always after createItem().
@@ -72,26 +82,26 @@
* @param rowIndex Row index to put the item
* @param edge min_edge if not reversed or max_edge if reversed.
*/
- public abstract void addItem(Object item, int index, int length, int rowIndex, int edge);
+ void addItem(Object item, int index, int length, int rowIndex, int edge);
/**
* Remove visible item at index.
* @param index 0-based index of the item in provider
*/
- public abstract void removeItem(int index);
+ void removeItem(int index);
/**
* Get edge of an existing visible item. edge will be the min_edge
* if not reversed or the max_edge if reversed.
* @param index 0-based index of the item in provider
*/
- public abstract int getEdge(int index);
+ int getEdge(int index);
/**
* Get size of an existing visible item.
* @param index 0-based index of the item in provider
*/
- public abstract int getSize(int index);
+ int getSize(int index);
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index 8149487..b9d4967 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -17,6 +17,7 @@
import static android.support.v7.widget.RecyclerView.NO_ID;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
+import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING;
import static android.support.v7.widget.RecyclerView.VERTICAL;
import android.content.Context;
@@ -275,13 +276,13 @@
}
void increasePendingMoves() {
- if (mPendingMoves < MAX_PENDING_MOVES) {
+ if (mPendingMoves < mMaxPendingMoves) {
mPendingMoves++;
}
}
void decreasePendingMoves() {
- if (mPendingMoves > -MAX_PENDING_MOVES) {
+ if (mPendingMoves > -mMaxPendingMoves) {
mPendingMoves--;
}
}
@@ -377,7 +378,8 @@
static final boolean TRACE = false;
// maximum pending movement in one direction.
- final static int MAX_PENDING_MOVES = 10;
+ static final int DEFAULT_MAX_PENDING_MOVES = 10;
+ int mMaxPendingMoves = DEFAULT_MAX_PENDING_MOVES;
// minimal milliseconds to scroll window size in major direction, we put a cap to prevent the
// effect smooth scrolling too over to bind an item view then drag the item view back.
final static int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30;
@@ -423,6 +425,18 @@
private OrientationHelper mOrientationHelper = OrientationHelper.createHorizontalHelper(this);
RecyclerView.State mState;
+ // Suppose currently showing 4, 5, 6, 7; removing 2,3,4 will make the layoutPosition to be
+ // 2(deleted), 3, 4, 5 in prelayout pass. So when we add item in prelayout, we must subtract 2
+ // from index of Grid.createItem.
+ int mPositionDeltaInPreLayout;
+ // Extra layout space needs to fill in prelayout pass. Note we apply the extra space to both
+ // appends and prepends due to the fact leanback is doing mario scrolling: removing items to
+ // the left of focused item might need extra layout on the right.
+ int mExtraLayoutSpaceInPreLayout;
+ // mRetainedFirstPosInPostLayout and mRetainedLastPosInPostLayout are used in post layout pass
+ // to add those item even they are out side visible area.
+ int mRetainedFirstPosInPostLayout;
+ int mRetainedLastPosInPostLayout;
RecyclerView.Recycler mRecycler;
private static final Rect sTempRect = new Rect();
@@ -871,7 +885,7 @@
mChildLaidOutListener = listener;
}
- private int getPositionByView(View view) {
+ private int getAdapterPositionByView(View view) {
if (view == null) {
return NO_POSITION;
}
@@ -908,8 +922,8 @@
return 0;
}
- private int getPositionByIndex(int index) {
- return getPositionByView(getChildAt(index));
+ private int getAdapterPositionByIndex(int index) {
+ return getAdapterPositionByView(getChildAt(index));
}
void dispatchChildSelected() {
@@ -1105,6 +1119,10 @@
}
mRecycler = recycler;
mState = state;
+ mPositionDeltaInPreLayout = 0;
+ mExtraLayoutSpaceInPreLayout = 0;
+ mRetainedFirstPosInPostLayout = Integer.MAX_VALUE;
+ mRetainedLastPosInPostLayout = Integer.MIN_VALUE;
}
/**
@@ -1113,6 +1131,10 @@
private void leaveContext() {
mRecycler = null;
mState = null;
+ mPositionDeltaInPreLayout = 0;
+ mExtraLayoutSpaceInPreLayout = 0;
+ mRetainedFirstPosInPostLayout = Integer.MAX_VALUE;
+ mRetainedLastPosInPostLayout = Integer.MIN_VALUE;
}
/**
@@ -1122,9 +1144,6 @@
* @return true if can fastRelayout()
*/
private boolean layoutInit() {
- boolean focusViewWasInTree = mGrid != null && mFocusPosition >= 0
- && mFocusPosition >= mGrid.getFirstVisibleIndex()
- && mFocusPosition <= mGrid.getLastVisibleIndex();
final int newItemCount = mState.getItemCount();
if (newItemCount == 0) {
mFocusPosition = NO_POSITION;
@@ -1142,13 +1161,9 @@
updateScrollController();
updateSecondaryScrollLimits();
mGrid.setSpacing(mSpacingPrimary);
- if (!focusViewWasInTree && mFocusPosition != NO_POSITION) {
- mGrid.setStart(mFocusPosition);
- }
return true;
} else {
mForceFullLayout = false;
- int firstVisibleIndex = focusViewWasInTree ? mGrid.getFirstVisibleIndex() : 0;
if (mGrid == null || mNumRows != mGrid.getNumRows()
|| mReverseFlowPrimary != mGrid.isReversedFlow()) {
@@ -1163,14 +1178,6 @@
mGrid.resetVisibleIndex();
mWindowAlignment.mainAxis().invalidateScrollMin();
mWindowAlignment.mainAxis().invalidateScrollMax();
- if (focusViewWasInTree && firstVisibleIndex <= mFocusPosition) {
- // if focusView was in tree, we will add item from first visible item
- mGrid.setStart(firstVisibleIndex);
- } else {
- // if focusView was not in tree, it's probably because focus position jumped
- // far away from visible range, so use mFocusPosition as start
- mGrid.setStart(mFocusPosition);
- }
return false;
}
}
@@ -1505,15 +1512,20 @@
private Grid.Provider mGridProvider = new Grid.Provider() {
@Override
+ public int getMinIndex() {
+ return mPositionDeltaInPreLayout;
+ }
+
+ @Override
public int getCount() {
- return mState.getItemCount();
+ return mState.getItemCount() + mPositionDeltaInPreLayout;
}
@Override
public int createItem(int index, boolean append, Object[] item) {
if (TRACE) TraceCompat.beginSection("createItem");
if (TRACE) TraceCompat.beginSection("getview");
- View v = getViewForPosition(index);
+ View v = getViewForPosition(index - mPositionDeltaInPreLayout);
if (TRACE) TraceCompat.endSection();
LayoutParams lp = (LayoutParams) v.getLayoutParams();
RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
@@ -1610,7 +1622,7 @@
@Override
public void removeItem(int index) {
if (TRACE) TraceCompat.beginSection("removeItem");
- View v = findViewByPosition(index);
+ View v = findViewByPosition(index - mPositionDeltaInPreLayout);
if (mInLayout) {
detachAndScrapView(v, mRecycler);
} else {
@@ -1622,15 +1634,15 @@
@Override
public int getEdge(int index) {
if (mReverseFlowPrimary) {
- return getViewMax(findViewByPosition(index));
+ return getViewMax(findViewByPosition(index - mPositionDeltaInPreLayout));
} else {
- return getViewMin(findViewByPosition(index));
+ return getViewMin(findViewByPosition(index - mPositionDeltaInPreLayout));
}
}
@Override
public int getSize(int index) {
- return getViewPrimarySize(findViewByPosition(index));
+ return getViewPrimarySize(findViewByPosition(index - mPositionDeltaInPreLayout));
}
};
@@ -1719,14 +1731,16 @@
private void removeInvisibleViewsAtEnd() {
if (mPruneChild && !mIsSlidingChildViews) {
- mGrid.removeInvisibleItemsAtEnd(mFocusPosition,
+ int retainedLastPos = Math.max(mRetainedLastPosInPostLayout, mFocusPosition);
+ mGrid.removeInvisibleItemsAtEnd(retainedLastPos,
mReverseFlowPrimary ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace);
}
}
private void removeInvisibleViewsAtFront() {
if (mPruneChild && !mIsSlidingChildViews) {
- mGrid.removeInvisibleItemsAtFront(mFocusPosition,
+ int retainedFirstPos = Math.min(mRetainedFirstPosInPostLayout, mFocusPosition);
+ mGrid.removeInvisibleItemsAtFront(retainedFirstPos,
mReverseFlowPrimary ? mSizePrimary + mExtraLayoutSpace: -mExtraLayoutSpace);
}
}
@@ -1823,13 +1837,15 @@
}
private void appendVisibleItems() {
- mGrid.appendVisibleItems(mReverseFlowPrimary ? -mExtraLayoutSpace
- : mSizePrimary + mExtraLayoutSpace);
+ mGrid.appendVisibleItems(mReverseFlowPrimary
+ ? -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout
+ : mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout);
}
private void prependVisibleItems() {
- mGrid.prependVisibleItems(mReverseFlowPrimary ? mSizePrimary + mExtraLayoutSpace
- : -mExtraLayoutSpace);
+ mGrid.prependVisibleItems(mReverseFlowPrimary
+ ? mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout
+ : -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout);
}
/**
@@ -1843,7 +1859,7 @@
int position = -1;
for (int index = 0; index < childCount; index++) {
View view = getChildAt(index);
- position = getPositionByIndex(index);
+ position = getAdapterPositionByView(view);
Grid.Location location = mGrid.getLocation(position);
if (location == null) {
if (DEBUG) Log.w(getTag(), "fastRelayout(): no Location at " + position);
@@ -1916,7 +1932,7 @@
// called by onLayoutChildren, either focus to FocusPosition or declare focusViewAvailable
// and scroll to the view if framework focus on it.
- private void scrollToFocusViewInLayout(boolean hadFocus, boolean alignToView) {
+ private void focusToViewInLayout(boolean hadFocus, boolean alignToView) {
View focusView = findViewByPosition(mFocusPosition);
if (focusView != null && alignToView) {
scrollToView(focusView, false);
@@ -1958,6 +1974,11 @@
}
}
+ @Override
+ public boolean supportsPredictiveItemAnimations() {
+ return true;
+ }
+
// Lays out items based on the current scroll position
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
@@ -1993,14 +2014,66 @@
}
mInLayout = true;
- if (state.didStructureChange()) {
- // didStructureChange() == true means attached item has been removed/added.
- // scroll animation: we are unable to continue a scroll animation,
- // kill the scroll animation, and let ItemAnimation move the item to new position.
- // position smooth scroller: kill the animation and stop at final position.
- // pending smooth scroller: stop and scroll to current focus position.
- mBaseGridView.stopScroll();
+ saveContext(recycler, state);
+ if (state.isPreLayout()) {
+ int childCount = getChildCount();
+ if (mGrid != null && childCount > 0) {
+ int minChangedEdge = Integer.MAX_VALUE;
+ int maxChangeEdge = Integer.MIN_VALUE;
+ for (int i = 0; i < childCount; i++) {
+ View view = getChildAt(i);
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (i == 0) {
+ // first child's layout position can be smaller than index if there were
+ // items removed before first visible index.
+ mPositionDeltaInPreLayout = mGrid.getFirstVisibleIndex()
+ - lp.getViewLayoutPosition();
+ }
+ // if either of following happening
+ // 1. item itself has changed or layout parameter changed
+ // 2. item is losing focus
+ // 3. item is gaining focus
+ if (lp.isItemChanged() || lp.isItemRemoved() || view.isLayoutRequested()
+ || (!view.hasFocus() && mFocusPosition == lp.getViewAdapterPosition())
+ || (view.hasFocus() && mFocusPosition != lp.getViewAdapterPosition())) {
+ minChangedEdge = Math.min(minChangedEdge, getViewMin(view));
+ maxChangeEdge = Math.max(maxChangeEdge, getViewMax(view));
+ }
+ }
+ if (maxChangeEdge > minChangedEdge) {
+ mExtraLayoutSpaceInPreLayout = maxChangeEdge - minChangedEdge;
+ }
+ // append items for mExtraLayoutSpaceInPreLayout
+ appendVisibleItems();
+ prependVisibleItems();
+ }
+ mInLayout = false;
+ leaveContext();
+ if (DEBUG) Log.v(getTag(), "layoutChildren end");
+ return;
}
+
+ // figure out the child positions that needs to be retained in post layout pass, e.g.
+ // When a child is pushed out, they needs to be added in post layout pass.
+ if (state.willRunPredictiveAnimations() && getChildCount() > 0) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ int pos = mBaseGridView.getChildViewHolder(child).getAdapterPosition();
+ if (pos >= 0) {
+ mRetainedFirstPosInPostLayout = pos;
+ break;
+ }
+ }
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ int pos = mBaseGridView.getChildViewHolder(child).getAdapterPosition();
+ if (pos >= 0) {
+ mRetainedLastPosInPostLayout = pos;
+ break;
+ }
+ }
+ }
+ // check if we need align to mFocusPosition, this is usually true unless in smoothScrolling
final boolean scrollToFocus = !isSmoothScrolling()
&& mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED;
if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
@@ -2008,37 +2081,35 @@
mSubFocusPosition = 0;
}
mFocusPositionOffset = 0;
- saveContext(recycler, state);
View savedFocusView = findViewByPosition(mFocusPosition);
int savedFocusPos = mFocusPosition;
int savedSubFocusPos = mSubFocusPosition;
boolean hadFocus = mBaseGridView.hasFocus();
-
- // Track the old focus view so we can adjust our system scroll position
- // so that any scroll animations happening now will remain valid.
- // We must use same delta in Pre Layout (if prelayout exists) and second layout.
- // So we cache the deltas in PreLayout and use it in second layout.
- int delta = 0, deltaSecondary = 0;
- if (mFocusPosition != NO_POSITION && scrollToFocus
- && mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
- // FIXME: we should get the remaining scroll animation offset from RecyclerView
- if (savedFocusView != null) {
- if (getScrollPosition(savedFocusView, savedFocusView.findFocus(), sTwoInts)) {
- delta = sTwoInts[0];
- deltaSecondary = sTwoInts[1];
- }
- }
- }
+ final int firstVisibleIndex = mGrid != null ? mGrid.getFirstVisibleIndex() : NO_POSITION;
+ final int lastVisibleIndex = mGrid != null ? mGrid.getLastVisibleIndex() : NO_POSITION;
if (mInFastRelayout = layoutInit()) {
+ // If grid view is empty, we will start from mFocusPosition
+ mGrid.setStart(mFocusPosition);
fastRelayout();
} else {
+ // layoutInit() has detached all views, so start from scratch
mInLayoutSearchFocus = hadFocus;
- if (mFocusPosition != NO_POSITION) {
- // appends items till focus position.
- while (appendOneColumnVisibleItems()
- && findViewByPosition(mFocusPosition) == null) ;
+ int startFromPosition, endPos;
+ if (scrollToFocus && (firstVisibleIndex < 0 || mFocusPosition > lastVisibleIndex
+ || mFocusPosition < firstVisibleIndex)) {
+ startFromPosition = endPos = mFocusPosition;
+ } else {
+ startFromPosition = firstVisibleIndex;
+ endPos = lastVisibleIndex;
+ }
+
+ mGrid.setStart(startFromPosition);
+ if (endPos != NO_POSITION) {
+ while (appendOneColumnVisibleItems() && findViewByPosition(endPos) == null) {
+ // continuously append items until endPos
+ }
}
}
// multiple rounds: scrollToView of first round may drag first/last child into
@@ -2050,7 +2121,7 @@
updateScrollLimits();
oldFirstVisible = mGrid.getFirstVisibleIndex();
oldLastVisible = mGrid.getLastVisibleIndex();
- scrollToFocusViewInLayout(hadFocus, scrollToFocus);
+ focusToViewInLayout(hadFocus, scrollToFocus);
appendVisibleItems();
prependVisibleItems();
removeInvisibleViewsAtFront();
@@ -2058,14 +2129,32 @@
} while (mGrid.getFirstVisibleIndex() != oldFirstVisible
|| mGrid.getLastVisibleIndex() != oldLastVisible);
- if (scrollToFocus) {
- scrollDirectionPrimary(-delta);
- scrollDirectionSecondary(-deltaSecondary);
+ if (scrollToFocus && mBaseGridView.getScrollState() == SCROLL_STATE_SETTLING) {
+ // if still running scroll animation (excluding smoothscroll case), compensate for
+ // remaining distance so that the animation will continue and later stopped at
+ // perfectly aligned position.
+ final int remainingScrollX = state.getRemainingScrollHorizontal();
+ final int remainingScrollY = state.getRemainingScrollVertical();
+ if (remainingScrollX != 0 || remainingScrollY != 0) {
+ if (mOrientation == HORIZONTAL) {
+ scrollDirectionPrimary(-remainingScrollX);
+ scrollDirectionSecondary(-remainingScrollY);
+ } else {
+ scrollDirectionPrimary(-remainingScrollY);
+ scrollDirectionSecondary(-remainingScrollX);
+ }
+ appendVisibleItems();
+ prependVisibleItems();
+ removeInvisibleViewsAtFront();
+ removeInvisibleViewsAtEnd();
+ }
}
- appendVisibleItems();
- prependVisibleItems();
- removeInvisibleViewsAtFront();
- removeInvisibleViewsAtEnd();
+ while (mGrid.getLastVisibleIndex() < mRetainedLastPosInPostLayout) {
+ appendOneColumnVisibleItems();
+ }
+ while (mGrid.getFirstVisibleIndex() > mRetainedFirstPosInPostLayout) {
+ prependOneColumnVisibleItems();
+ }
if (DEBUG) {
StringWriter sw = new StringWriter();
@@ -2090,10 +2179,10 @@
dispatchChildSelected();
}
dispatchChildSelectedAndPositioned();
-
if (mIsSlidingChildViews) {
scrollDirectionPrimary(getSlideOutDistance());
}
+
mInLayout = false;
leaveContext();
if (DEBUG) Log.v(getTag(), "layoutChildren end");
@@ -2524,8 +2613,8 @@
int pos = mFocusPosition + mFocusPositionOffset;
if (positionStart <= pos) {
if (positionStart + itemCount > pos) {
- // stop updating offset after the focus item was removed
- mFocusPositionOffset = Integer.MIN_VALUE;
+ // the focus item was removed
+ mFocusPositionOffset += positionStart - pos;
} else {
mFocusPositionOffset -= itemCount;
}
@@ -2569,7 +2658,7 @@
if (mFocusSearchDisabled) {
return true;
}
- if (getPositionByView(child) == NO_POSITION) {
+ if (getAdapterPositionByView(child) == NO_POSITION) {
// This is could be the last view in DISAPPEARING animation.
return true;
}
@@ -2637,7 +2726,7 @@
if (mIsSlidingChildViews) {
return;
}
- int newFocusPosition = getPositionByView(view);
+ int newFocusPosition = getAdapterPositionByView(view);
int newSubFocusPosition = getSubPositionByView(view, childView);
if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) {
mFocusPosition = newFocusPosition;
@@ -2678,7 +2767,7 @@
}
private boolean getNoneAlignedPosition(View view, int[] deltas) {
- int pos = getPositionByView(view);
+ int pos = getAdapterPositionByView(view);
int viewMin = getViewMin(view);
int viewMax = getViewMax(view);
// we either align "firstView" to left/top padding edge
@@ -2942,7 +3031,7 @@
}
final int focusedRow = mGrid.getLocation(pos).row;
for (int i = getChildCount() - 1; i >= 0; i--) {
- int position = getPositionByIndex(i);
+ int position = getAdapterPositionByIndex(i);
Grid.Location loc = mGrid.getLocation(position);
if (loc != null && loc.row == focusedRow) {
if (position < pos) {
@@ -2974,7 +3063,7 @@
final int movement = getMovement(direction);
final View focused = recyclerView.findFocus();
final int focusedIndex = findImmediateChildIndex(focused);
- final int focusedPos = getPositionByIndex(focusedIndex);
+ final int focusedPos = getAdapterPositionByIndex(focusedIndex);
// Add focusables of focused item.
if (focusedPos != NO_POSITION) {
findViewByPosition(focusedPos).addFocusables(views, direction, focusableMode);
@@ -3013,7 +3102,7 @@
}
continue;
}
- int position = getPositionByIndex(i);
+ int position = getAdapterPositionByIndex(i);
Grid.Location loc = mGrid.getLocation(position);
if (loc == null) {
continue;
@@ -3315,7 +3404,7 @@
// save views currently is on screen (TODO save cached views)
for (int i = 0, count = getChildCount(); i < count; i++) {
View view = getChildAt(i);
- int position = getPositionByView(view);
+ int position = getAdapterPositionByView(view);
if (position != NO_POSITION) {
bundle = mChildrenStates.saveOnScreenView(bundle, view, position);
}
@@ -3426,7 +3515,7 @@
if (!canScrollTo(child)) {
continue;
}
- int position = getPositionByIndex(index);
+ int position = getAdapterPositionByIndex(index);
int rowIndex = mGrid.getRowIndex(position);
if (focusedRow == NO_POSITION) {
focusPosition = position;
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java b/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
index 04a384a..226bd51 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
@@ -76,7 +76,8 @@
return false;
}
boolean filledOne = false;
- for (int index = getStartIndexForPrepend(); index >= 0; index--) {
+ int minIndex = mProvider.getMinIndex();
+ for (int index = getStartIndexForPrepend(); index >= minIndex; index--) {
int size = mProvider.createItem(index, false, mTmpItem);
int edge;
if (mFirstVisibleIndex < 0 || mLastVisibleIndex < 0) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
index 7fa54e2..eb9528e 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
@@ -141,8 +141,6 @@
if (mLocations.size() == 0) {
return false;
}
- final int count = mProvider.getCount();
- final int firstIndex = getFirstIndex();
int itemIndex;
int edge;
int offset;
@@ -165,7 +163,8 @@
return false;
}
}
- for (; itemIndex >= mFirstIndex; itemIndex--) {
+ int firstIndex = Math.max(mProvider.getMinIndex(), mFirstIndex);
+ for (; itemIndex >= firstIndex; itemIndex--) {
Location loc = getLocation(itemIndex);
int rowIndex = loc.row;
int size = mProvider.createItem(itemIndex, false, mTmpItem);
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
index 6f5529e..605adfa 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
@@ -33,6 +33,11 @@
}
@Override
+ public int getMinIndex() {
+ return 0;
+ }
+
+ @Override
public int getCount() {
return mCount;
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 4d4bede..8b38db0 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -442,16 +442,19 @@
* To wait the ItemAnimator start, you can use waitForLayout() to make sure layout pass has
* processed adapter change.
*/
- protected void waitForItemAnimation(int timeoutMs) {
- RecyclerView.ItemAnimator.ItemAnimatorFinishedListener listener = mock(
+ protected void waitForItemAnimation(int timeoutMs) throws Throwable {
+ final RecyclerView.ItemAnimator.ItemAnimatorFinishedListener listener = mock(
RecyclerView.ItemAnimator.ItemAnimatorFinishedListener.class);
- if (mGridView.getItemAnimator().isRunning(listener)) {
- verify(listener, timeout(timeoutMs).atLeastOnce())
- .onAnimationsFinished();
- }
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().isRunning(listener);
+ }
+ });
+ verify(listener, timeout(timeoutMs).atLeastOnce()).onAnimationsFinished();
}
- protected void waitForItemAnimation() {
+ protected void waitForItemAnimation() throws Throwable {
waitForItemAnimation(WAIT_FOR_ITEM_ANIMATION_FINISH_TIMEOUT_MS);
}
@@ -774,6 +777,241 @@
verifyEdgesSame(endEdges, endEdges2);
}
+ void preparePredictiveLayout() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.getItemAnimator().setAddDuration(1000);
+ mGridView.getItemAnimator().setRemoveDuration(1000);
+ mGridView.getItemAnimator().setMoveDuration(1000);
+ mGridView.getItemAnimator().setChangeDuration(1000);
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ }
+
+ @Test
+ public void testPredictiveLayoutAdd1() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(51, new int[]{300, 300, 300, 300});
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation(5000);
+ assertEquals(50, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveLayoutAdd2() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(50, new int[]{300, 300, 300, 300});
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation(5000);
+ assertEquals(54, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove1() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(51, 3);
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation(5000);
+ assertEquals(50, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove2() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(46, 3);
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation(5000);
+ assertEquals(47, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove3() throws Throwable {
+ preparePredictiveLayout();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(0, 51);
+ }
+ });
+ waitForItemAnimationStart();
+ waitForItemAnimation(5000);
+ assertEquals(0, mGridView.getSelectedPosition());
+ assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove4() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ waitForScrollIdle();
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(0, 49);
+ }
+ });
+ assertEquals(1, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testPredictiveLayoutRemove5() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, true);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 3;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ waitForScrollIdle();
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(50, 40);
+ }
+ });
+ assertEquals(50, mGridView.getSelectedPosition());
+ scrollToBegin(mVerifyLayout);
+ verifyBeginAligned();
+ }
+
+ @Test
+ public void testContinuousSwapForward() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(150);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ for (int i = 150; i < 199; i++) {
+ final int swapIndex = i;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.swap(swapIndex, swapIndex + 1);
+ }
+ });
+ Thread.sleep(10);
+ }
+ waitForItemAnimation();
+ assertEquals(199, mGridView.getSelectedPosition());
+ // check if ItemAnimation finishes at aligned positions:
+ int leftEdge = mGridView.getLayoutManager().findViewByPosition(199).getLeft();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(leftEdge, mGridView.getLayoutManager().findViewByPosition(199).getLeft());
+ }
+
+ @Test
+ public void testContinuousSwapBackward() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ for (int i = 50; i > 0; i--) {
+ final int swapIndex = i;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.swap(swapIndex, swapIndex - 1);
+ }
+ });
+ Thread.sleep(10);
+ }
+ waitForItemAnimation();
+ assertEquals(0, mGridView.getSelectedPosition());
+ // check if ItemAnimation finishes at aligned positions:
+ int leftEdge = mGridView.getLayoutManager().findViewByPosition(0).getLeft();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(leftEdge, mGridView.getLayoutManager().findViewByPosition(0).getLeft());
+ }
+
@Test
public void testItemMovedHorizontal() throws Throwable {
Intent intent = new Intent();
@@ -784,7 +1022,12 @@
mOrientation = BaseGridView.HORIZONTAL;
mNumRows = 3;
- mGridView.setSelectedPositionSmooth(150);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(150);
+ }
+ });
waitForScrollIdle(mVerifyLayout);
performAndWaitForAnimation(new Runnable() {
@Override
@@ -1216,13 +1459,24 @@
}
});
- performAndWaitForAnimation(new Runnable() {
+ mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
int[] newItems = new int[]{300, 300, 300};
mActivity.addItems(0, newItems);
}
});
+ waitForScrollIdle();
+ int topEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getTop();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.requestLayout();
+ }
+ });
+ waitForScrollIdle();
+ assertEquals(topEdge,
+ mGridView.getLayoutManager().findViewByPosition(focusToIndex).getTop());
}
@Test
@@ -1274,12 +1528,12 @@
Intent intent = new Intent();
intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 300);
initActivity(intent);
mOrientation = BaseGridView.HORIZONTAL;
mNumRows = 1;
- final int focusToIndex = 40;
+ final int focusToIndex = 200;
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -1316,12 +1570,12 @@
Intent intent = new Intent();
intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
R.layout.horizontal_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 300);
initActivity(intent);
mOrientation = BaseGridView.HORIZONTAL;
mNumRows = 1;
- final int focusToIndex = 40;
+ final int focusToIndex = 200;
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -1340,9 +1594,10 @@
});
waitForLayout();
- assertFalse("removing the index of attached child should kill smooth scroller",
+ assertTrue("removing the index of attached child should not kill smooth scroller",
mGridView.getLayoutManager().isSmoothScrolling());
waitForItemAnimation();
+ waitForScrollIdle();
int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusToIndex).getLeft();
mActivityTestRule.runOnUiThread(new Runnable() {
@@ -1378,7 +1633,8 @@
assertTrue(mGridView.getChildAt(0).hasFocus());
// Pressing lots of key to make sure smooth scroller is running
- for (int i = 0; i < 20; i++) {
+ mGridView.mLayoutManager.mMaxPendingMoves = 100;
+ for (int i = 0; i < 100; i++) {
sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
}
@@ -1394,10 +1650,11 @@
});
waitForLayout();
- assertFalse("removing the index of attached child should kill smooth scroller",
+ assertTrue("removing the index of attached child should not kill smooth scroller",
mGridView.getLayoutManager().isSmoothScrolling());
waitForItemAnimation();
+ waitForScrollIdle();
int focusIndex = mGridView.getSelectedPosition();
int topEdge = mGridView.getLayoutManager().findViewByPosition(focusIndex).getTop();
@@ -2349,7 +2606,8 @@
mGridView.stopScroll();
}
});
- Thread.sleep(100);
+ waitForScrollIdle();
+ waitForItemAnimation();
assertEquals(mGridView.getSelectedPosition(), targetPosition);
assertSame(mGridView.getLayoutManager().findViewByPosition(targetPosition),
mGridView.findFocus());
@@ -3797,7 +4055,7 @@
initActivity(intent);
- final HashSet<View> removeAnimationFinishedViews = new HashSet();
+ final HashSet<View> moveAnimationViews = new HashSet();
mActivity.mImportantForAccessibilityListener =
new GridActivity.ImportantForAccessibilityListener() {
RecyclerView.LayoutManager mLM = mGridView.getLayoutManager();
@@ -3805,7 +4063,7 @@
public void onImportantForAccessibilityChanged(View view, int newValue) {
// simulates talkack, having setImportantForAccessibility to call
// onInitializeAccessibilityNodeInfoForItem() for the DISAPPEARING items.
- if (removeAnimationFinishedViews.contains(view)) {
+ if (moveAnimationViews.contains(view)) {
AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
mLM.onInitializeAccessibilityNodeInfoForItem(
null, null, view, info);
@@ -3814,19 +4072,23 @@
};
final int lastPos = mGridView.getChildAdapterPosition(
mGridView.getChildAt(mGridView.getChildCount() - 1));
+ final int numItemsToPushOut = mNumRows;
+ for (int i = 0; i < numItemsToPushOut; i++) {
+ moveAnimationViews.add(
+ mGridView.getChildAt(mGridView.getChildCount() - 1 - i));
+ }
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mGridView.setItemAnimator(new DefaultItemAnimator() {
@Override
- public void onRemoveFinished(RecyclerView.ViewHolder item) {
- removeAnimationFinishedViews.add(item.itemView);
+ public void onMoveFinished(RecyclerView.ViewHolder item) {
+ moveAnimationViews.remove(item.itemView);
}
});
mGridView.getLayoutManager().setItemPrefetchEnabled(false);
}
});
- final int numItemsToPushOut = mNumRows;
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -3838,7 +4100,7 @@
mActivity.addItems(lastPos - numItemsToPushOut + 1, newItems);
}
});
- while (removeAnimationFinishedViews.size() != numItemsToPushOut) {
+ while (moveAnimationViews.size() != 0) {
Thread.sleep(100);
}
}
diff --git a/v7/appcompat/src/android/support/v7/widget/ActionBarOverlayLayout.java b/v7/appcompat/src/android/support/v7/widget/ActionBarOverlayLayout.java
index dff9fed..b5cdc7a 100644
--- a/v7/appcompat/src/android/support/v7/widget/ActionBarOverlayLayout.java
+++ b/v7/appcompat/src/android/support/v7/widget/ActionBarOverlayLayout.java
@@ -80,6 +80,7 @@
private final Rect mLastBaseContentInsets = new Rect();
private final Rect mContentInsets = new Rect();
private final Rect mBaseInnerInsets = new Rect();
+ private final Rect mLastBaseInnerInsets = new Rect();
private final Rect mInnerInsets = new Rect();
private final Rect mLastInnerInsets = new Rect();
@@ -293,6 +294,10 @@
mBaseInnerInsets.set(systemInsets);
ViewUtils.computeFitSystemWindows(this, mBaseInnerInsets, mBaseContentInsets);
+ if (!mLastBaseInnerInsets.equals(mBaseInnerInsets)) {
+ changed = true;
+ mLastBaseInnerInsets.set(mBaseInnerInsets);
+ }
if (!mLastBaseContentInsets.equals(mBaseContentInsets)) {
changed = true;
mLastBaseContentInsets.set(mBaseContentInsets);