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