Nested RecyclerView Prefetch
Bug: 27106058
Fixes: 32343355
Test: tests added, existing @SmallTests pass
- Stash RecyclerView if found within ViewHolder
- Prefetch nested LinearLayout content based on anchor
- Avoid double query of Display#getRefreshRate()
- Avoid dispatching recycled callbacks for unbound holders
- Reorganized State members based on lifecycle.
Change-Id: I51701576d8f5985e3cab7b684694c50bf03ef280
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/NestedRecyclerViewActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/NestedRecyclerViewActivity.java
index a9caf40..220c0ff 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/widget/NestedRecyclerViewActivity.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/NestedRecyclerViewActivity.java
@@ -17,6 +17,7 @@
package com.example.android.supportv7.widget;
import android.graphics.Color;
+import android.os.Parcelable;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -47,7 +48,7 @@
static class InnerAdapter extends RecyclerView.Adapter<InnerAdapter.ViewHolder> {
public static class ViewHolder extends RecyclerView.ViewHolder {
- public TextView mTextView;
+ TextView mTextView;
public ViewHolder(TextView itemView) {
super(itemView);
@@ -57,7 +58,7 @@
private String[] mData;
- public InnerAdapter(String[] data) {
+ InnerAdapter(String[] data) {
mData = data;
}
@@ -93,15 +94,17 @@
}
ArrayList<InnerAdapter> mAdapters = new ArrayList<>();
+ ArrayList<Parcelable> mSavedStates = new ArrayList<>();
RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
- public OuterAdapter(String[] data) {
+ OuterAdapter(String[] data) {
int currentCharIndex = 0;
char currentChar = data[0].charAt(0);
for (int i = 1; i <= data.length; i++) {
if (i == data.length || data[i].charAt(0) != currentChar) {
mAdapters.add(new InnerAdapter(
Arrays.copyOfRange(data, currentCharIndex, i - 1)));
+ mSavedStates.add(null);
if (i < data.length) {
currentChar = data[i].charAt(0);
currentCharIndex = i;
@@ -121,7 +124,21 @@
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
+ // Note: would be equally valid to replace adapter content instead of swapping adapter
+ //holder.mRecyclerView.swapAdapter(mAdapters.get(position), true);
holder.mRecyclerView.setAdapter(mAdapters.get(position));
+
+ Parcelable savedState = mSavedStates.get(position);
+ if (savedState != null) {
+ holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState);
+ mSavedStates.set(position, null);
+ }
+ }
+
+ @Override
+ public void onViewRecycled(ViewHolder holder) {
+ mSavedStates.set(holder.getAdapterPosition(),
+ holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
}
@Override
diff --git a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
index 3631e7d..ab51de7 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
@@ -24,7 +24,7 @@
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
-class GapWorker implements Runnable {
+final class GapWorker implements Runnable {
static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();
@@ -56,7 +56,7 @@
private ArrayList<Task> mTasks = new ArrayList<>();
/**
- * Prefetch information associated with a specfic RecyclerView.
+ * Prefetch information associated with a specific RecyclerView.
*/
static class PrefetchRegistryImpl implements RecyclerView.PrefetchRegistry {
private int mPrefetchDx;
@@ -70,7 +70,7 @@
mPrefetchDy = dy;
}
- void collectPrefetchPositionsFromView(RecyclerView view) {
+ void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
mCount = 0;
if (mPrefetchArray != null) {
Arrays.fill(mPrefetchArray, -1);
@@ -79,9 +79,21 @@
final RecyclerView.LayoutManager layout = view.mLayout;
if (view.mAdapter != null
&& layout != null
- && layout.isItemPrefetchEnabled()
- && !view.hasPendingAdapterUpdates()) {
- layout.collectPrefetchPositions(mPrefetchDx, mPrefetchDy, view.mState, this);
+ && layout.isItemPrefetchEnabled()) {
+ if (nested) {
+ // nested prefetch, only if no adapter updates pending. Note: we don't query
+ // view.hasPendingAdapterUpdates(), as first layout may not have occurred
+ if (!view.mAdapterHelper.hasPendingUpdates()) {
+ layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
+ }
+ } else {
+ // momentum based prefetch, only if we trust current child/adapter state
+ if (!view.hasPendingAdapterUpdates()) {
+ layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
+ view.mState, this);
+ }
+ }
+
if (mCount > layout.mPrefetchMaxCountObserved) {
layout.mPrefetchMaxCountObserved = mCount;
view.mRecycler.updateViewCacheSize();
@@ -195,7 +207,7 @@
int totalTaskCount = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
- view.mPrefetchRegistry.collectPrefetchPositionsFromView(view);
+ view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
totalTaskCount += view.mPrefetchRegistry.mCount;
}
@@ -244,36 +256,67 @@
return false;
}
+ private RecyclerView.ViewHolder flushWorkWithDeadline(RecyclerView view,
+ int position, long deadlineNs) {
+ if (isPrefetchPositionAttached(view, position)) {
+ // don't attempt to prefetch attached views
+ return null;
+ }
+
+ RecyclerView.Recycler recycler = view.mRecycler;
+ RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
+ position, false, deadlineNs);
+
+ if (holder != null) {
+ if (holder.isBound()) {
+ // Only give the view a chance to go into the cache if binding succeeded
+ // Note that we must use public method, since item may need cleanup
+ recycler.recycleView(holder.itemView);
+ } else {
+ // Didn't bind, so we can't cache the view, but it will stay in the pool until
+ // next prefetch/traversal. If a View fails to bind, it means we didn't have
+ // enough time prior to the deadline (and won't for other instances of this
+ // type, during this GapWorker prefetch pass).
+ recycler.addViewHolderToRecycledViewPool(holder, false);
+ }
+ }
+ return holder;
+ }
+
+ private void flushTaskWithDeadline(Task task, long deadlineNs) {
+ long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
+ RecyclerView.ViewHolder holder = flushWorkWithDeadline(task.view,
+ task.position, taskDeadlineNs);
+ if (holder != null && holder.mNestedRecyclerView != null) {
+ // do nested prefetch!
+ final RecyclerView innerView = holder.mNestedRecyclerView;
+ final PrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry;
+ innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true);
+
+ if (innerPrefetchRegistry.mCount != 0) {
+ try {
+ TraceCompat.beginSection(RecyclerView.TRACE_NESTED_PREFETCH_TAG);
+ innerView.mState.prepareForNestedPrefetch(innerView.mAdapter);
+ for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) {
+ // Note that we ignore immediate flag for inner items because
+ // we have lower confidence they're needed next frame.
+ final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i];
+ flushWorkWithDeadline(innerView, innerPosition, deadlineNs);
+ }
+ } finally {
+ TraceCompat.endSection();
+ }
+ }
+ }
+ }
+
private void flushTasksWithDeadline(long deadlineNs) {
for (int i = 0; i < mTasks.size(); i++) {
final Task task = mTasks.get(i);
if (task.view == null) {
- // abort, only empty Tasks left
- return;
+ break; // done with populated tasks
}
-
- if (isPrefetchPositionAttached(task.view, task.position)) {
- // don't attempt to prefetch attached views
- continue;
- }
-
- RecyclerView.Recycler recycler = task.view.mRecycler;
- RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
- task.position, false, task.immediate ? RecyclerView.FOREVER_NS : deadlineNs);
-
- if (holder != null) {
- if (holder.isBound()) {
- // Only give the view a chance to go into the cache if binding succeeded
- // Note that we must use public method, since item may need cleanup
- recycler.recycleView(holder.itemView);
- } else {
- // Didn't bind, so we can't cache the view, but it will stay in the pool until
- // next prefetch/traversal. If a View fails to bind, it means we didn't have
- // enough time prior to the deadline (and won't for other instances of this
- // type, during this GapWorker prefetch pass).
- recycler.addViewHolderToRecycledViewPool(holder);
- }
- }
+ flushTaskWithDeadline(task, deadlineNs);
task.clear();
}
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index 1f4682f..ceabf01 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -144,6 +144,11 @@
private final LayoutChunkResult mLayoutChunkResult = new LayoutChunkResult();
/**
+ * Number of items to prefetch when first coming on screen with new data.
+ */
+ private int mInitialItemPrefetchCount = 2;
+
+ /**
* Creates a vertical LinearLayoutManager
*
* @param context Current context, will be used to access resources.
@@ -1190,9 +1195,56 @@
}
}
+ /**
+ * TODO: we expand cache by largest prefetch seen - not appropriate for nested prefetch?
+ * @hide
+ */
+ @Override
+ public void collectInitialPrefetchPositions(int adapterItemCount,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
+ final boolean fromEnd;
+ final int anchorPos;
+ if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
+ // use restored state, since it hasn't been resolved yet
+ fromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
+ anchorPos = mPendingSavedState.mAnchorPosition;
+ } else {
+ resolveShouldLayoutReverse();
+ fromEnd = mShouldReverseLayout;
+ if (mPendingScrollPosition == NO_POSITION) {
+ anchorPos = fromEnd ? adapterItemCount - 1 : 0;
+ } else {
+ anchorPos = mPendingScrollPosition;
+ }
+ }
+
+ final int direction = fromEnd
+ ? LayoutState.ITEM_DIRECTION_HEAD
+ : LayoutState.ITEM_DIRECTION_TAIL;
+ int targetPos = anchorPos;
+ for (int i = 0; i < mInitialItemPrefetchCount; i++) {
+ if (targetPos >= 0 && targetPos < adapterItemCount) {
+ prefetchRegistry.addPosition(targetPos, 0);
+ } else {
+ break; // no more to prefetch
+ }
+ targetPos += direction;
+ }
+ }
+
+ /** @hide */
+ public void setInitialPrefetchItemCount(int itemCount) {
+ mInitialItemPrefetchCount = itemCount;
+ }
+
+ /** @hide */
+ public int getInitialItemPrefetchCount() {
+ return mInitialItemPrefetchCount;
+ }
+
/** @hide */
@Override
- public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
RecyclerView.PrefetchRegistry prefetchRegistry) {
int delta = (mOrientation == HORIZONTAL) ? dx : dy;
if (getChildCount() == 0 || delta == 0) {
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 1463722..948d026 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -256,6 +256,12 @@
static final String TRACE_PREFETCH_TAG = "RV Prefetch";
/**
+ * RecyclerView is attempting to pre-populate off screen itemviews within an off screen
+ * RecyclerView.
+ */
+ static final String TRACE_NESTED_PREFETCH_TAG = "RV Nested Prefetch";
+
+ /**
* RecyclerView is creating a new View.
* If too many of these present in Systrace:
* - There might be a problem in Recycling (e.g. custom Animations that set transient state and
@@ -2420,9 +2426,11 @@
// NOTE: we only do this query once, statically, because it's very expensive (> 1ms)
Display display = ViewCompat.getDisplay(this);
float refreshRate = 60.0f;
- if (display != null
- && display.getRefreshRate() >= 30.0f) {
- refreshRate = display.getRefreshRate();
+ if (display != null) {
+ float displayRefreshRate = display.getRefreshRate();
+ if (displayRefreshRate >= 30.0f) {
+ refreshRate = displayRefreshRate;
+ }
}
mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
GapWorker.sGapWorker.set(mGapWorker);
@@ -4961,6 +4969,50 @@
}
}
+ /**
+ * Utility method for finding an internal RecyclerView, if present
+ */
+ @Nullable
+ static RecyclerView findNestedRecyclerView(@NonNull View view) {
+ if (!(view instanceof ViewGroup)) {
+ return null;
+ }
+ if (view instanceof RecyclerView) {
+ return (RecyclerView) view;
+ }
+ final ViewGroup parent = (ViewGroup) view;
+ final int count = parent.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = parent.getChildAt(i);
+ final RecyclerView descendant = findNestedRecyclerView(child);
+ if (descendant != null) {
+ return descendant;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Utility method for clearing holder's internal RecyclerView, if present
+ */
+ static void clearNestedRecyclerViewIfNotNested(@NonNull ViewHolder holder) {
+ if (holder.mNestedRecyclerView != null) {
+ View item = holder.mNestedRecyclerView;
+ while (item != null) {
+ if (item == holder.itemView) {
+ return; // match found, don't need to clear
+ }
+
+ ViewParent parent = item.getParent();
+ if (parent instanceof View) {
+ item = (View) parent;
+ } else {
+ item = null;
+ }
+ }
+ holder.mNestedRecyclerView = null; // not nested
+ }
+ }
/**
* Time base for deadline-aware work scheduling. Overridable for testing.
@@ -5324,6 +5376,11 @@
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
+ if (ALLOW_THREAD_GAP_WORK) {
+ // only bother finding nested RV if prefetching
+ holder.mNestedRecyclerView = findNestedRecyclerView(holder.itemView);
+ }
+
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
if (DEBUG) {
@@ -5482,7 +5539,7 @@
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
- addViewHolderToRecycledViewPool(viewHolder);
+ addViewHolderToRecycledViewPool(viewHolder, true);
mCachedViews.remove(cachedViewIndex);
}
@@ -5550,12 +5607,15 @@
cached = true;
}
if (!cached) {
- addViewHolderToRecycledViewPool(holder);
+ addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
- } else if (DEBUG) {
- Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
- + "re-visit here. We are still removing it from animation lists");
+ } else {
+ holder.mNestedRecyclerView = null;
+ if (DEBUG) {
+ Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
+ + "re-visit here. We are still removing it from animation lists");
+ }
}
// even if the holder is not removed, we still call this method so that it is removed
// from view holder lists.
@@ -5565,9 +5625,20 @@
}
}
- void addViewHolderToRecycledViewPool(ViewHolder holder) {
+ /**
+ * Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool.
+ *
+ * Pass false to dispatchRecycled for views that have not been bound.
+ *
+ * @param holder Holder to be added to the pool.
+ * @param dispatchRecycled True to dispatch View recycled callbacks.
+ */
+ void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
+ clearNestedRecyclerViewIfNotNested(holder);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
- dispatchViewRecycled(holder);
+ if (dispatchRecycled) {
+ dispatchViewRecycled(holder);
+ }
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
@@ -6623,7 +6694,8 @@
/**
* Written by {@link GapWorker} when prefetches occur to track largest number of view ever
- * requested by a {@link #collectPrefetchPositions(int, int, State, PrefetchRegistry)} call.
+ * requested by a {@link #collectInitialPrefetchPositions(int, PrefetchRegistry)} or
+ * {@link #collectAdjacentPrefetchPositions(int, int, State, PrefetchRegistry)} call.
*/
int mPrefetchMaxCountObserved;
@@ -6949,15 +7021,16 @@
}
/**
- * Gather all positions from the LayoutManager to be prefetched.
+ * Gather all positions from the LayoutManager to be prefetched, given specified momentum.
*
* <p>If item prefetch is enabled, this method is called in between traversals to gather
* which positions the LayoutManager will soon need, given upcoming movement in subsequent
* traversals.</p>
*
* <p>The LayoutManager should call {@link PrefetchRegistry#addPosition(int, int)} for each
- * item to be prepared, and these positions will have their ViewHolders created and bound
- * in advance of being needed by a scroll or layout.</p>
+ * item to be prepared, and these positions will have their ViewHolders created and bound,
+ * if there is sufficient time available, in advance of being needed by a
+ * scroll or layout.</p>
*
* @param dx X movement component.
* @param dy Y movement component.
@@ -6965,10 +7038,41 @@
* @param prefetchRegistry PrefetchRegistry to add prefetch entries into.
*
* @see #isItemPrefetchEnabled()
+ * @see #collectInitialPrefetchPositions(int, PrefetchRegistry)
*
* @hide
*/
- public void collectPrefetchPositions(int dx, int dy, State state,
+ public void collectAdjacentPrefetchPositions(int dx, int dy, State state,
+ PrefetchRegistry prefetchRegistry) {}
+
+ /**
+ * Gather all positions from the LayoutManager to be prefetched in preperation for its
+ * Recyclerview to come on screen, due to the movement of another, containing RecyclerView.
+ *
+ * <p>This method is only called when a RecyclerView is nested in another RecyclerView.</p>
+ *
+ * <p>If item prefetch is enabled for this LayoutManager, as well in another containing
+ * LayoutManager, this method is called in between draw traversals to gather
+ * which positions this LayoutManager will first need, once it appears on the screen.</p>
+ *
+ * <p>For example, if this LayoutManager represents a horizontally scrolling list within a
+ * vertically scrolling LayoutManager, this method would be called when the horizontal list
+ * is about to come onscreen.</p>
+ *
+ * <p>The LayoutManager should call {@link PrefetchRegistry#addPosition(int, int)} for each
+ * item to be prepared, and these positions will have their ViewHolders created and bound,
+ * if there is sufficient time available, in advance of being needed by a
+ * scroll or layout.</p>
+ *
+ * @param adapterItemCount number of items in the associated adapter.
+ * @param prefetchRegistry PrefetchRegistry to add prefetch entries into.
+ *
+ * @see #isItemPrefetchEnabled()
+ * @see #collectAdjacentPrefetchPositions(int, int, State, PrefetchRegistry
+ *
+ * @hide
+ */
+ public void collectInitialPrefetchPositions(int adapterItemCount,
PrefetchRegistry prefetchRegistry) {}
void dispatchAttachedToWindow(RecyclerView view) {
@@ -9666,6 +9770,7 @@
*/
public static abstract class ViewHolder {
public final View itemView;
+ RecyclerView mNestedRecyclerView;
int mPosition = NO_POSITION;
int mOldPosition = NO_POSITION;
long mItemId = NO_ID;
@@ -10056,6 +10161,7 @@
clearPayload();
mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
+ clearNestedRecyclerViewIfNotNested(this);
}
/**
@@ -10960,23 +11066,15 @@
}
}
- @IntDef(flag = true, value = {
- STEP_START, STEP_LAYOUT, STEP_ANIMATIONS
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface LayoutState {}
+ /** Owned by SmoothScroller */
private int mTargetPosition = RecyclerView.NO_POSITION;
- @LayoutState
- int mLayoutStep = STEP_START;
-
private SparseArray<Object> mData;
- /**
- * Number of items adapter has.
- */
- int mItemCount = 0;
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // Fields below are carried from one layout pass to the next
+ ////////////////////////////////////////////////////////////////////////////////////////////
/**
* Number of items adapter had in the previous layout.
@@ -10989,18 +11087,40 @@
*/
int mDeletedInvisibleItemCountSincePreviousLayout = 0;
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // Fields below must be updated or cleared before they are used (generally before a pass)
+ ////////////////////////////////////////////////////////////////////////////////////////////
+
+ @IntDef(flag = true, value = {
+ STEP_START, STEP_LAYOUT, STEP_ANIMATIONS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface LayoutState {}
+
+ @LayoutState
+ int mLayoutStep = STEP_START;
+
+ /**
+ * Number of items adapter has.
+ */
+ int mItemCount = 0;
+
boolean mStructureChanged = false;
boolean mInPreLayout = false;
- boolean mRunSimpleAnimations = false;
-
- boolean mRunPredictiveAnimations = false;
-
boolean mTrackOldChangeHolders = false;
boolean mIsMeasuring = false;
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // Fields below are always reset outside of the pass (or passes) that use them
+ ////////////////////////////////////////////////////////////////////////////////////////////
+
+ boolean mRunSimpleAnimations = false;
+
+ boolean mRunPredictiveAnimations = false;
+
/**
* This data is saved before a layout calculation happens. After the layout is finished,
* if the previously focused view has been replaced with another view for the same item, we
@@ -11012,6 +11132,8 @@
// that one instead
int mFocusedSubChildId;
+ ////////////////////////////////////////////////////////////////////////////////////////////
+
State reset() {
mTargetPosition = RecyclerView.NO_POSITION;
if (mData != null) {
@@ -11024,6 +11146,22 @@
}
/**
+ * Prepare for a prefetch occurring on the RecyclerView in between traversals, potentially
+ * prior to any layout passes.
+ *
+ * <p>Don't touch any state stored between layout passes, only reset per-layout state, so
+ * that Recycler#getViewForPosition() can function safely.</p>
+ */
+ void prepareForNestedPrefetch(Adapter adapter) {
+ mLayoutStep = STEP_START;
+ mItemCount = adapter.getItemCount();
+ mStructureChanged = false;
+ mInPreLayout = false;
+ mTrackOldChangeHolders = false;
+ mIsMeasuring = false;
+ }
+
+ /**
* Returns true if the RecyclerView is currently measuring the layout. This value is
* {@code true} only if the LayoutManager opted into the auto measure API and RecyclerView
* has non-exact measurement specs.
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
index 7040bcc..62a7f09 100644
--- a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -213,8 +213,8 @@
private boolean mSmoothScrollbarEnabled = true;
/**
- * Temporary array used (solely in {@link #collectPrefetchPositions}) for stashing and sorting
- * distances to views being prefetched.
+ * Temporary array used (solely in {@link #collectAdjacentPrefetchPositions}) for stashing and
+ * sorting distances to views being prefetched.
*/
private int[] mPrefetchDistances;
@@ -2073,7 +2073,7 @@
/** @hide */
@Override
- public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
RecyclerView.PrefetchRegistry prefetchRegistry) {
/* This method uses the simplifying assumption that the next N items (where N = span count)
* will be assigned, one-to-one, to spans, where ordering is based on which span extends
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
index 8d93553..4eee5de 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
@@ -318,10 +318,10 @@
}
@Override
- public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
RecyclerView.PrefetchRegistry prefetchRegistry) {
if (prefetchLatch != null) prefetchLatch.countDown();
- super.collectPrefetchPositions(dx, dy, state, prefetchRegistry);
+ super.collectAdjacentPrefetchPositions(dx, dy, state, prefetchRegistry);
}
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
index 9bea02c..505b1c3 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
@@ -635,10 +635,10 @@
}
@Override
- public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
RecyclerView.PrefetchRegistry prefetchRegistry) {
if (prefetchLatch != null) prefetchLatch.countDown();
- super.collectPrefetchPositions(dx, dy, state, prefetchRegistry);
+ super.collectAdjacentPrefetchPositions(dx, dy, state, prefetchRegistry);
}
}
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
index ea88391..59175ac 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
@@ -805,10 +805,10 @@
}
@Override
- public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
RecyclerView.PrefetchRegistry prefetchRegistry) {
if (prefetchLatch != null) prefetchLatch.countDown();
- super.collectPrefetchPositions(dx, dy, state, prefetchRegistry);
+ super.collectAdjacentPrefetchPositions(dx, dy, state, prefetchRegistry);
}
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/CacheUtils.java b/v7/recyclerview/tests/src/android/support/v7/widget/CacheUtils.java
index 0bd67f7..70a8644 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/CacheUtils.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/CacheUtils.java
@@ -28,7 +28,7 @@
static void verifyPositionsPrefetched(RecyclerView view, int dx, int dy,
Integer[]... positionData) {
RecyclerView.PrefetchRegistry prefetchRegistry = mock(RecyclerView.PrefetchRegistry.class);
- view.mLayout.collectPrefetchPositions(
+ view.mLayout.collectAdjacentPrefetchPositions(
dx, dy, view.mState, prefetchRegistry);
verify(prefetchRegistry, times(positionData.length)).addPosition(anyInt(), anyInt());
@@ -78,4 +78,15 @@
}
}
}
+
+ static RecyclerView.ViewHolder peekAtCachedViewForPosition(RecyclerView view, int position) {
+ for (int i = 0; i < view.mRecycler.mCachedViews.size(); i++) {
+ RecyclerView.ViewHolder holder = view.mRecycler.mCachedViews.get(i);
+ if (holder.mPosition == position) {
+ return holder;
+ }
+ }
+ fail("Unable to find view with position " + position);
+ return null;
+ }
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
index fff41f5..48abe68 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
@@ -40,6 +40,7 @@
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
+import android.widget.FrameLayout;
import android.widget.TextView;
import org.junit.Before;
@@ -368,7 +369,7 @@
mRecyclerView.setAdapter(new MockAdapter(20));
MockLayoutManager mlm = new MockLayoutManager() {
@Override
- public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
RecyclerView.PrefetchRegistry prefetchManager) {
prefetchManager.addPosition(0, 0);
prefetchManager.addPosition(1, 0);
@@ -387,7 +388,7 @@
mRecyclerView.layout(0, 0, 100, 100);
// prefetch gets 3 items, so expands cache by 3
- mRecyclerView.mPrefetchRegistry.collectPrefetchPositionsFromView(mRecyclerView);
+ mRecyclerView.mPrefetchRegistry.collectPrefetchPositionsFromView(mRecyclerView, false);
assertEquals(3, mRecyclerView.mPrefetchRegistry.mCount);
assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE + 3, recycler.mViewCacheMax);
@@ -414,6 +415,49 @@
}
}
+ @Test
+ public void findNestedRecyclerView() {
+ RecyclerView recyclerView = new RecyclerView(getContext());
+ assertEquals(recyclerView, RecyclerView.findNestedRecyclerView(recyclerView));
+
+ ViewGroup parent = new FrameLayout(getContext());
+ assertEquals(null, RecyclerView.findNestedRecyclerView(parent));
+ parent.addView(recyclerView);
+ assertEquals(recyclerView, RecyclerView.findNestedRecyclerView(parent));
+
+ ViewGroup grandParent = new FrameLayout(getContext());
+ assertEquals(null, RecyclerView.findNestedRecyclerView(grandParent));
+ grandParent.addView(parent);
+ assertEquals(recyclerView, RecyclerView.findNestedRecyclerView(grandParent));
+ }
+
+ @Test
+ public void clearNestedRecyclerViewIfNotNested() {
+ RecyclerView recyclerView = new RecyclerView(getContext());
+ ViewGroup parent = new FrameLayout(getContext());
+ parent.addView(recyclerView);
+ ViewGroup grandParent = new FrameLayout(getContext());
+ grandParent.addView(parent);
+
+ // verify trivial noop case
+ RecyclerView.ViewHolder holder = new RecyclerView.ViewHolder(recyclerView) {};
+ holder.mNestedRecyclerView = recyclerView;
+ RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
+ assertEquals(recyclerView, holder.mNestedRecyclerView);
+
+ // verify clear case
+ holder = new RecyclerView.ViewHolder(new View(getContext())) {};
+ holder.mNestedRecyclerView = recyclerView;
+ RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
+ assertNull(holder.mNestedRecyclerView);
+
+ // verify more deeply nested case
+ holder = new RecyclerView.ViewHolder(grandParent) {};
+ holder.mNestedRecyclerView = recyclerView;
+ RecyclerView.clearNestedRecyclerViewIfNotNested(holder);
+ assertEquals(recyclerView, holder.mNestedRecyclerView);
+ }
+
static class MockLayoutManager extends RecyclerView.LayoutManager {
int mLayoutCount = 0;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
index 755173d..b128554 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
@@ -17,10 +17,12 @@
package android.support.v7.widget;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -36,6 +38,8 @@
import android.view.View;
import android.view.ViewGroup;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -43,6 +47,7 @@
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -109,7 +114,7 @@
}
@Override
- public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
RecyclerView.PrefetchRegistry prefetchManager) {
prefetchManager.addPosition(0, 0);
prefetchManager.addPosition(1, 0);
@@ -151,7 +156,7 @@
any(RecyclerView.ViewHolder.class), anyInt(), any(List.class));
assertTrue(mRecycler.mCachedViews.size() == 3);
- CacheUtils.verifyCacheContainsPositions(mRecyclerView, 0, 1, 2);
+ CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 0, 1, 2);
}
}
@@ -181,7 +186,7 @@
mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
assertEquals(5, mRecyclerView.mRecycler.mViewCacheMax);
- CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5);
+ CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 3, 4, 5);
// further views recycled, as though from scrolling, shouldn't evict prefetched views:
mRecycler.recycleView(mRecycler.getViewForPosition(10));
@@ -308,6 +313,62 @@
}
@Test
+ public void partialPrefetchAvoidsViewRecycledCallback() {
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+
+ // 100x100 pixel views
+ RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ mRecyclerView.registerTimePassingMs(5);
+ View view = new View(getContext());
+ view.setMinimumWidth(100);
+ view.setMinimumHeight(100);
+ return new RecyclerView.ViewHolder(view) {};
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ mRecyclerView.registerTimePassingMs(5);
+ }
+
+ @Override
+ public int getItemCount() {
+ return 100;
+ }
+
+ @Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ // verify unbound view doesn't get
+ assertNotEquals(RecyclerView.NO_POSITION, holder.getAdapterPosition());
+ }
+ };
+ mRecyclerView.setAdapter(adapter);
+
+ layout(100, 300);
+
+ // offset scroll so that no prefetch-able views are directly adjacent to viewport
+ mRecyclerView.scrollBy(0, 50);
+
+ assertTrue(mRecycler.mCachedViews.size() == 0);
+ assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);
+
+ // Should take 10 ms to inflate + bind, so just give it 9 so it doesn't have time to bind
+ final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(9);
+
+ // Timed prefetch
+ mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+ mRecyclerView.mGapWorker.prefetch(deadlineNs);
+
+ // will have enough time to inflate but not bind one view
+ assertTrue(mRecycler.mCachedViews.size() == 0);
+ assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 1);
+ RecyclerView.ViewHolder pooledHolder = mRecyclerView.getRecycledViewPool()
+ .mScrap.get(0).mScrapHeap.get(0);
+ assertEquals(RecyclerView.NO_POSITION, pooledHolder.getAdapterPosition());
+ }
+
+ @Test
public void prefetchStaggeredItemsPriority() {
StaggeredGridLayoutManager sglm =
new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
@@ -392,8 +453,7 @@
final RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- return new RecyclerView.ViewHolder(new View(parent.getContext())) {
- };
+ return new RecyclerView.ViewHolder(new View(parent.getContext())) {};
}
@Override
@@ -450,4 +510,134 @@
assertEquals(2, llm.getChildCount());
assertEquals(4, mRecyclerView.getChildCount());
}
+
+ @Test
+ public void viewHolderFindsNestedRecyclerViews() {
+ LinearLayoutManager llm = new LinearLayoutManager(getContext());
+ mRecyclerView.setLayoutManager(llm);
+
+ RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
+ when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
+ .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
+ @Override
+ public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
+ throws Throwable {
+ View view = new RecyclerView(getContext());
+ view.setLayoutParams(new RecyclerView.LayoutParams(100, 100));
+ return new RecyclerView.ViewHolder(view) {};
+ }
+ });
+ when(mockAdapter.getItemCount()).thenReturn(100);
+ mRecyclerView.setAdapter(mockAdapter);
+
+ layout(100, 200);
+
+ verify(mockAdapter, times(2)).onCreateViewHolder(any(ViewGroup.class), anyInt());
+ verify(mockAdapter, times(2)).onBindViewHolder(
+ argThat(new BaseMatcher<RecyclerView.ViewHolder>() {
+ @Override
+ public boolean matches(Object item) {
+ RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder) item;
+ return holder.itemView == holder.mNestedRecyclerView;
+ }
+
+ @Override
+ public void describeTo(Description description) { }
+ }),
+ anyInt(),
+ any(List.class));
+ }
+
+ static class InnerAdapter extends RecyclerView.Adapter<InnerAdapter.ViewHolder> {
+ private static final int INNER_ITEM_COUNT = 10;
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ ViewHolder(View itemView) {
+ super(itemView);
+ }
+ }
+
+ InnerAdapter() {}
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = new View(parent.getContext());
+ view.setLayoutParams(new RecyclerView.LayoutParams(100, 100));
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {}
+
+ @Override
+ public int getItemCount() {
+ return INNER_ITEM_COUNT;
+ }
+ }
+
+ static class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
+ private static final int OUTER_ITEM_COUNT = 10;
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ private final RecyclerView mRecyclerView;
+ ViewHolder(RecyclerView itemView) {
+ super(itemView);
+ mRecyclerView = itemView;
+ }
+ }
+
+ ArrayList<InnerAdapter> mAdapters = new ArrayList<>();
+ RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
+
+ OuterAdapter() {
+ for (int i = 0; i <= OUTER_ITEM_COUNT; i++) {
+ mAdapters.add(new InnerAdapter());
+ }
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ RecyclerView rv = new RecyclerView(parent.getContext());
+ rv.setLayoutManager(new LinearLayoutManager(parent.getContext(),
+ LinearLayoutManager.HORIZONTAL, false));
+ rv.setRecycledViewPool(mSharedPool);
+ rv.setLayoutParams(new RecyclerView.LayoutParams(200, 100));
+ return new ViewHolder(rv);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ // Note: would be equally valid to replace adapter content instead of swapping adapter
+ holder.mRecyclerView.setAdapter(mAdapters.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return OUTER_ITEM_COUNT;
+ }
+ }
+
+ @Test
+ public void simpleNestedPrefetch() {
+ LinearLayoutManager llm = new LinearLayoutManager(getContext());
+ assertEquals(2, llm.getInitialItemPrefetchCount());
+
+ mRecyclerView.setLayoutManager(llm);
+ mRecyclerView.setAdapter(new OuterAdapter());
+
+ layout(200, 200);
+ mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+
+ // prefetch 2 (default)
+ mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+ RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
+ assertNotNull(holder);
+ assertNotNull(holder.mNestedRecyclerView);
+ CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView, 0, 1);
+
+ // prefetch 4
+ ((LinearLayoutManager) holder.mNestedRecyclerView.getLayoutManager())
+ .setInitialPrefetchItemCount(4);
+ mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+ CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView, 0, 1, 2, 3);
+ }
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewPrefetchTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewPrefetchTest.java
index b3f44a8..3001240 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewPrefetchTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewPrefetchTest.java
@@ -57,7 +57,7 @@
}
@Override
- public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
RecyclerView.PrefetchRegistry prefetchRegistry) {
prefetchLatch.countDown();
prefetchRegistry.addPosition(6, 0);