Improve out of bounds anchor.

Bug fixes involve a behavior change where
if all in bounds children are removed from a
LinearLayoutManager, and out of bounds children
are found to become potential anchors, out of bounds
children that are "further down in the scroll direction"
are moved to fill in the RV.

Bug: 154124815
Test: ./gradlew recyclerview:recyclerview:connectedCheck --info --daemon -Pandroid.testInstrumentationRunnerArguments.class=androidx.recyclerview.widget.LinearLayoutManagerRemoveShownItemsTest

Change-Id: I63b177d1bedd8016125bc6aa619fb12714797252
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerFindReferenceChildTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerFindReferenceChildTest.kt
index 7a6a5ff..12a8a44 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerFindReferenceChildTest.kt
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerFindReferenceChildTest.kt
@@ -124,11 +124,11 @@
         internal override fun findReferenceChild(
             recycler: RecyclerView.Recycler?,
             state: RecyclerView.State?,
-            start: Int,
-            end: Int,
-            itemCount: Int
+            layoutFromEnd: Boolean,
+            traverseChildrenInReverseOrder: Boolean
         ): View {
-            val referenceChild = super.findReferenceChild(recycler, state, start, end, itemCount)
+            val referenceChild = super
+                .findReferenceChild(recycler, state, layoutFromEnd, traverseChildrenInReverseOrder)
             recordedReferenceChildren.add(getPosition(referenceChild))
             return referenceChild
         }
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerFindZeroPxReferenceChildTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerFindZeroPxReferenceChildTest.kt
index e7e0f73..5476284 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerFindZeroPxReferenceChildTest.kt
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerFindZeroPxReferenceChildTest.kt
@@ -157,11 +157,11 @@
         internal override fun findReferenceChild(
             recycler: RecyclerView.Recycler?,
             state: RecyclerView.State?,
-            start: Int,
-            end: Int,
-            itemCount: Int
+            layoutFromEnd: Boolean,
+            traverseChildrenInReverseOrder: Boolean
         ): View {
-            val referenceChild = super.findReferenceChild(recycler, state, start, end, itemCount)
+            val referenceChild = super
+                .findReferenceChild(recycler, state, layoutFromEnd, traverseChildrenInReverseOrder)
             recordedReferenceChildren.add(getPosition(referenceChild))
             return referenceChild
         }
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerRemoveShownItemsTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerRemoveShownItemsTest.kt
new file mode 100644
index 0000000..7dbbc0a
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerRemoveShownItemsTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.widget
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
+import androidx.recyclerview.widget.RecyclerView.VERTICAL
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Tests that we're looking at the right item(s) after removing the current item(s) in the case
+ * where all item views have the same size as RecyclerViews view port.
+ */
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LinearLayoutManagerRemoveShownItemsTest(
+    private val config: Config,
+    private val extraLayoutSpaceItems: Int
+) : BaseLinearLayoutManagerTest() {
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0},extraLayoutSpaceItems={1}")
+        fun spec(): List<Array<Any>> =
+            listOf(VERTICAL, HORIZONTAL).flatMap { orientation ->
+                listOf(false, true).flatMap { stackFromEnd ->
+                    listOf(false, true).flatMap { reverseLayout ->
+                        listOf(0, 1, 2).map { extraLayoutSpaceItems ->
+                            arrayOf(
+                                Config(orientation, reverseLayout, stackFromEnd).apply {
+                                    mItemCount = Config.DEFAULT_ITEM_COUNT
+                                },
+                                extraLayoutSpaceItems
+                            )
+                        }
+                    }
+                }
+            }
+    }
+
+    /**
+     * 1 item removed.
+     * Middle of the set of items.
+     */
+    @Test
+    fun notifyItemRangeRemoved_1_onlyCorrectItemVisible() {
+        val llm = MyLayoutManager(activity, 500)
+        config.mTestLayoutManager = llm
+        setupByConfig(
+            config,
+            true,
+            RecyclerView.LayoutParams(500, 500),
+            RecyclerView.LayoutParams(500, 500)
+        )
+
+        val startingAdapterPosition = 10 // Item with label 11
+
+        // Expected behavior when removing the shown view is
+        // that the view(s) after it are moved into the gap.
+        val expectedResultingAdapterPosition =
+            startingAdapterPosition - if (config.mStackFromEnd) 1 else 0
+
+        // Given an RV showing the 11th view that is as big as RV itself ..
+        mLayoutManager.expectLayouts(1)
+        scrollToPosition(startingAdapterPosition)
+        mLayoutManager.waitForLayout(2)
+
+        // .. when we remove that view ..
+        mLayoutManager.expectLayouts(2)
+        mTestAdapter.deleteAndNotify(startingAdapterPosition, 1)
+        mLayoutManager.waitForLayout(2)
+
+        // .. then the views after the removed view are moved into the gap
+        assertEquals(expectedResultingAdapterPosition, llm.findFirstVisibleItemPosition())
+        assertEquals(expectedResultingAdapterPosition, llm.findFirstCompletelyVisibleItemPosition())
+        assertEquals(expectedResultingAdapterPosition, llm.findLastCompletelyVisibleItemPosition())
+        assertEquals(expectedResultingAdapterPosition, llm.findLastVisibleItemPosition())
+    }
+
+    /**
+     * 2 items removed.
+     * Middle of the set of items.
+     */
+    @Test
+    fun notifyItemRangeRemoved_2_onlyCorrectItemsVisible() {
+        val llm = MyLayoutManager(activity, 500)
+        config.mTestLayoutManager = llm
+        val rvLayoutParams =
+            if (config.mOrientation == VERTICAL) {
+                RecyclerView.LayoutParams(500, 1000)
+            } else {
+                RecyclerView.LayoutParams(1000, 500)
+            }
+        setupByConfig(
+            config,
+            true,
+            RecyclerView.LayoutParams(500, 500),
+            rvLayoutParams
+        )
+
+        val startingAdapterPosition = 10 // Items 10 and 11 (with labels 11 and 12) will show.
+
+        // Expected behavior when removing the shown view is
+        // that the view(s) after it are moved into the gap.
+        val expectedResultingAdapterPosition =
+            startingAdapterPosition - if (config.mStackFromEnd) 2 else 0
+
+        // This will make sure that items 10 and 11 are shown
+        val adapterPositionToScrollTo = startingAdapterPosition + if (config.mStackFromEnd) 0 else 1
+        mLayoutManager.expectLayouts(1)
+        scrollToPosition(adapterPositionToScrollTo)
+        mLayoutManager.waitForLayout(2)
+
+        // .. when we remove that view ..
+        mLayoutManager.expectLayouts(2)
+        mTestAdapter.deleteAndNotify(startingAdapterPosition, 2)
+        mLayoutManager.waitForLayout(2)
+
+        // .. then the views after the removed view are moved into the gap
+        assertEquals(expectedResultingAdapterPosition, llm.findFirstVisibleItemPosition())
+        assertEquals(expectedResultingAdapterPosition, llm.findFirstCompletelyVisibleItemPosition())
+        assertEquals(
+            expectedResultingAdapterPosition + 1,
+            llm.findLastCompletelyVisibleItemPosition()
+        )
+        assertEquals(expectedResultingAdapterPosition + 1, llm.findLastVisibleItemPosition())
+    }
+
+    /**
+     * All attached items removed.
+     * Middle of the set of items.
+     */
+    @Test
+    fun notifyItemRangeRemoved_all_onlyCorrectItemVisible() {
+        val llm = MyLayoutManager(activity, 500)
+        config.mTestLayoutManager = llm
+        setupByConfig(
+            config,
+            true,
+            RecyclerView.LayoutParams(500, 500),
+            RecyclerView.LayoutParams(500, 500)
+        )
+
+        val startingAdapterPosition = 10 // Item with label 11
+
+        // Expected behavior when removing the shown view is
+        // that the view(s) after it are moved into the gap.
+        val expectedResultingAdapterPosition =
+            (startingAdapterPosition - extraLayoutSpaceItems) - if (config.mStackFromEnd) 1 else 0
+
+        // Given an RV showing the 11th view that is as big as RV itself ..
+        mLayoutManager.expectLayouts(1)
+        scrollToPosition(startingAdapterPosition)
+        mLayoutManager.waitForLayout(2)
+
+        // .. when we remove all laid out items ..
+        val removeFrom = llm.run { List(childCount) { getPosition(getChildAt(it)!!) }.min() }!!
+        mLayoutManager.expectLayouts(2)
+        mTestAdapter.deleteAndNotify(removeFrom, llm.childCount)
+        mLayoutManager.waitForLayout(2)
+
+        // .. then the views after the removed view are moved into the gap
+        assertEquals(expectedResultingAdapterPosition, llm.findFirstVisibleItemPosition())
+        assertEquals(expectedResultingAdapterPosition, llm.findFirstCompletelyVisibleItemPosition())
+        assertEquals(expectedResultingAdapterPosition, llm.findLastCompletelyVisibleItemPosition())
+        assertEquals(expectedResultingAdapterPosition, llm.findLastVisibleItemPosition())
+    }
+
+    private inner class MyLayoutManager internal constructor(context: Context, val itemSize: Int) :
+        WrappedLinearLayoutManager(context, config.mOrientation, config.mReverseLayout) {
+
+        override fun calculateExtraLayoutSpace(
+            state: RecyclerView.State,
+            extraLayoutSpace: IntArray
+        ) {
+            when (extraLayoutSpaceItems) {
+                0 -> super.calculateExtraLayoutSpace(state, extraLayoutSpace)
+                else -> {
+                    extraLayoutSpace[0] = itemSize * extraLayoutSpaceItems
+                    extraLayoutSpace[1] = itemSize * extraLayoutSpaceItems
+                }
+            }
+        }
+    }
+}
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
index 370b43c..428c779 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
@@ -417,13 +417,25 @@
 
     @Override
     View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
-                            int start, int end, int itemCount) {
+            boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) {
+
+        int start = 0;
+        int end = getChildCount();
+        int diff = 1;
+        if (traverseChildrenInReverseOrder) {
+            start = getChildCount() - 1;
+            end = -1;
+            diff = -1;
+        }
+
+        int itemCount = state.getItemCount();
+
         ensureLayoutState();
         View invalidMatch = null;
         View outOfBoundsMatch = null;
+
         final int boundsStart = mOrientationHelper.getStartAfterPadding();
         final int boundsEnd = mOrientationHelper.getEndAfterPadding();
-        final int diff = end > start ? 1 : -1;
 
         for (int i = start; i != end; i += diff) {
             final View view = getChildAt(i);
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index cd4f240..536e47c 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -130,9 +130,9 @@
     SavedState mPendingSavedState = null;
 
     /**
-     *  Re-used variable to keep anchor information on re-layout.
-     *  Anchor position and coordinate defines the reference point for LLM while doing a layout.
-     * */
+     * Re-used variable to keep anchor information on re-layout.
+     * Anchor position and coordinate defines the reference point for LLM while doing a layout.
+     */
     final AnchorInfo mAnchorInfo = new AnchorInfo();
 
     /**
@@ -442,7 +442,7 @@
      * enough to handle it.</p>
      *
      * @return The extra space that should be laid out (in pixels).
-     * @deprecated  Use {@link #calculateExtraLayoutSpace(RecyclerView.State, int[])} instead.
+     * @deprecated Use {@link #calculateExtraLayoutSpace(RecyclerView.State, int[])} instead.
      */
     @SuppressWarnings("DeprecatedIsStillUsed")
     @Deprecated
@@ -561,7 +561,7 @@
             updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
             mAnchorInfo.mValid = true;
         } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
-                        >= mOrientationHelper.getEndAfterPadding()
+                >= mOrientationHelper.getEndAfterPadding()
                 || mOrientationHelper.getDecoratedEnd(focused)
                 <= mOrientationHelper.getStartAfterPadding())) {
             // This case relates to when the anchor child is the focused view and due to layout
@@ -735,9 +735,10 @@
     /**
      * Method called when Anchor position is decided. Extending class can setup accordingly or
      * even update anchor info if necessary.
-     * @param recycler The recycler for the layout
-     * @param state The layout state
-     * @param anchorInfo The mutable POJO that keeps the position and offset.
+     *
+     * @param recycler                 The recycler for the layout
+     * @param state                    The layout state
+     * @param anchorInfo               The mutable POJO that keeps the position and offset.
      * @param firstLayoutItemDirection The direction of the first layout filling in terms of adapter
      *                                 indices.
      */
@@ -755,7 +756,7 @@
         // and layout them accordingly so that animations can work as expected.
         // This case may happen if new views are added or an existing view expands and pushes
         // another view out of bounds.
-        if (!state.willRunPredictiveAnimations() ||  getChildCount() == 0 || state.isPreLayout()
+        if (!state.willRunPredictiveAnimations() || getChildCount() == 0 || state.isPreLayout()
                 || !supportsPredictiveItemAnimations()) {
             return;
         }
@@ -845,9 +846,12 @@
         if (mLastStackFromEnd != mStackFromEnd) {
             return false;
         }
-        View referenceChild = anchorInfo.mLayoutFromEnd
-                ? findReferenceChildClosestToEnd(recycler, state)
-                : findReferenceChildClosestToStart(recycler, state);
+        View referenceChild =
+                findReferenceChild(
+                        recycler,
+                        state,
+                        anchorInfo.mLayoutFromEnd,
+                        mStackFromEnd);
         if (referenceChild != null) {
             anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
             // If all visible views are removed in 1 pass, reference child might be out of bounds.
@@ -860,9 +864,9 @@
                 final int boundsEnd = mOrientationHelper.getEndAfterPadding();
                 // b/148869110: usually if childStart >= boundsEnd the child is out of
                 // bounds, except if the child is 0 pixels!
-                final boolean notVisible = (childStart >= boundsEnd && childEnd > boundsEnd)
-                        || (childEnd <= boundsStart && childStart < boundsStart);
-                if (notVisible) {
+                boolean outOfBoundsBefore = childEnd <= boundsStart && childStart < boundsStart;
+                boolean outOfBoundsAfter = childStart >= boundsEnd && childEnd > boundsEnd;
+                if (outOfBoundsBefore || outOfBoundsAfter) {
                     anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd ? boundsEnd : boundsStart;
                 }
             }
@@ -1181,7 +1185,7 @@
         return ScrollbarHelper.computeScrollExtent(state, mOrientationHelper,
                 findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
                 findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
-                this,  mSmoothScrollbarEnabled);
+                this, mSmoothScrollbarEnabled);
     }
 
     private int computeScrollRange(RecyclerView.State state) {
@@ -1209,7 +1213,6 @@
      * with varying widths / heights.
      *
      * @param enabled Whether or not to enable smooth scrollbar.
-     *
      * @see #setSmoothScrollbarEnabled(boolean)
      */
     public void setSmoothScrollbarEnabled(boolean enabled) {
@@ -1220,7 +1223,6 @@
      * Returns the current state of the smooth scrollbar feature. It is enabled by default.
      *
      * @return True if smooth scrollbar is enabled, false otherwise.
-     *
      * @see #setSmoothScrollbarEnabled(boolean)
      */
     public boolean isSmoothScrollbarEnabled() {
@@ -1341,7 +1343,6 @@
      * the number of Views created and in active use.</p>
      *
      * @param itemCount Number of items to prefetch
-     *
      * @see #isItemPrefetchEnabled()
      * @see #getInitialPrefetchItemCount()
      * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
@@ -1356,11 +1357,10 @@
      * how many inner items should be prefetched when this LayoutManager's RecyclerView
      * is nested inside another RecyclerView.
      *
+     * @return number of items to prefetch.
      * @see #isItemPrefetchEnabled()
      * @see #setInitialPrefetchItemCount(int)
      * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)
-     *
-     * @return number of items to prefetch.
      */
     public int getInitialPrefetchItemCount() {
         return mInitialPrefetchItemCount;
@@ -1444,13 +1444,13 @@
      * <p>
      * Checks both layout position and visible position to guarantee that the view is not visible.
      *
-     * @param recycler Recycler instance of {@link RecyclerView}
+     * @param recycler        Recycler instance of {@link RecyclerView}
      * @param scrollingOffset This can be used to add additional padding to the visible area. This
      *                        is used to detect children that will go out of bounds after scrolling,
      *                        without actually moving them.
-     * @param noRecycleSpace Extra space that should be excluded from recycling. This is the space
-     *                       from {@code extraLayoutSpace[0]}, calculated in {@link
-     *                       #calculateExtraLayoutSpace}.
+     * @param noRecycleSpace  Extra space that should be excluded from recycling. This is the space
+     *                        from {@code extraLayoutSpace[0]}, calculated in {@link
+     *                        #calculateExtraLayoutSpace}.
      */
     private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
             int noRecycleSpace) {
@@ -1493,13 +1493,13 @@
      * <p>
      * Checks both layout position and visible position to guarantee that the view is not visible.
      *
-     * @param recycler Recycler instance of {@link RecyclerView}
+     * @param recycler        Recycler instance of {@link RecyclerView}
      * @param scrollingOffset This can be used to add additional padding to the visible area. This
      *                        is used to detect children that will go out of bounds after scrolling,
      *                        without actually moving them.
-     * @param noRecycleSpace Extra space that should be excluded from recycling. This is the space
-     *                       from {@code extraLayoutSpace[1]}, calculated in {@link
-     *                       #calculateExtraLayoutSpace}.
+     * @param noRecycleSpace  Extra space that should be excluded from recycling. This is the space
+     *                        from {@code extraLayoutSpace[1]}, calculated in {@link
+     *                        #calculateExtraLayoutSpace}.
      */
     private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
             int noRecycleSpace) {
@@ -1811,57 +1811,50 @@
         }
     }
 
-
-    /**
-     * Among the children that are suitable to be considered as an anchor child, returns the one
-     * closest to the end of the layout.
-     * <p>
-     * Due to ambiguous adapter updates or children being removed, some children's positions may be
-     * invalid. This method is a best effort to find a position within adapter bounds if possible.
-     * <p>
-     * It also prioritizes children that are within the visible bounds.
-     * @return A View that can be used an an anchor View.
-     */
-    private View findReferenceChildClosestToEnd(RecyclerView.Recycler recycler,
-            RecyclerView.State state) {
-        return mShouldReverseLayout ? findFirstReferenceChild(recycler, state) :
-                findLastReferenceChild(recycler, state);
-    }
-
-    /**
-     * Among the children that are suitable to be considered as an anchor child, returns the one
-     * closest to the start of the layout.
-     * <p>
-     * Due to ambiguous adapter updates or children being removed, some children's positions may be
-     * invalid. This method is a best effort to find a position within adapter bounds if possible.
-     * <p>
-     * It also prioritizes children that are within the visible bounds.
-     *
-     * @return A View that can be used an an anchor View.
-     */
-    private View findReferenceChildClosestToStart(RecyclerView.Recycler recycler,
-            RecyclerView.State state) {
-        return mShouldReverseLayout ? findLastReferenceChild(recycler, state) :
-                findFirstReferenceChild(recycler, state);
-    }
-
-    private View findFirstReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) {
-        return findReferenceChild(recycler, state, 0, getChildCount(), state.getItemCount());
-    }
-
-    private View findLastReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) {
-        return findReferenceChild(recycler, state, getChildCount() - 1, -1, state.getItemCount());
-    }
-
     // overridden by GridLayoutManager
+
+    /**
+     * Finds a suitable anchor child.
+     * <p>
+     * Due to ambiguous adapter updates or children being removed, some children's positions may be
+     * invalid. This method is a best effort to find a position within adapter bounds if possible.
+     * <p>
+     * It also prioritizes children from best to worst in this order:
+     * <ol>
+     *   <li> An in bounds child.
+     *   <li> An out of bounds child.
+     *   <li> An invalid child.
+     * </ol>
+     *
+     * @param layoutFromEnd True if the RV scrolls in the reverse direction, which is the same as
+     *                      (reverseLayout ^ stackFromEnd).
+     * @param traverseChildrenInReverseOrder True if the children should be traversed in reverse
+     *                                       order (stackFromEnd).
+     * @return A View that can be used an an anchor View.
+     */
     View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
-            int start, int end, int itemCount) {
+            boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) {
         ensureLayoutState();
-        View invalidMatch = null;
-        View outOfBoundsMatch = null;
+
+        // Determine which direction through the view children we are going iterate.
+        int start = 0;
+        int end = getChildCount();
+        int diff = 1;
+        if (traverseChildrenInReverseOrder) {
+            start = getChildCount() - 1;
+            end = -1;
+            diff = -1;
+        }
+
+        int itemCount = state.getItemCount();
+
         final int boundsStart = mOrientationHelper.getStartAfterPadding();
         final int boundsEnd = mOrientationHelper.getEndAfterPadding();
-        final int diff = end > start ? 1 : -1;
+
+        View invalidMatch = null;
+        View bestFirstFind = null;
+        View bestSecondFind = null;
+
         for (int i = start; i != end; i += diff) {
             final View view = getChildAt(i);
             final int position = getPosition(view);
@@ -1875,19 +1868,42 @@
                 } else {
                     // b/148869110: usually if childStart >= boundsEnd the child is out of
                     // bounds, except if the child is 0 pixels!
-                    if ((childStart >= boundsEnd && childEnd > boundsEnd)
-                            || (childEnd <= boundsStart && childStart < boundsStart)) {
-                        // item is not visible, less preferred
-                        if (outOfBoundsMatch == null) {
-                            outOfBoundsMatch = view;
+                    boolean outOfBoundsBefore = childEnd <= boundsStart && childStart < boundsStart;
+                    boolean outOfBoundsAfter = childStart >= boundsEnd && childEnd > boundsEnd;
+                    if (outOfBoundsBefore || outOfBoundsAfter) {
+                        // The item is out of bounds.
+                        // We want to find the items closest to the in bounds items and because we
+                        // are always going through the items linearly, the 2 items we want are the
+                        // last out of bounds item on the side we start searching on, and the first
+                        // out of bounds item on the side we are ending on.  The side that we are
+                        // ending on ultimately takes priority because we want items later in the
+                        // layout to move forward if no in bounds anchors are found.
+                        if (layoutFromEnd) {
+                            if (outOfBoundsAfter) {
+                                bestFirstFind = view;
+                            } else if (bestSecondFind == null) {
+                                bestSecondFind = view;
+                            }
+                        } else {
+                            if (outOfBoundsBefore) {
+                                bestFirstFind = view;
+                            } else if (bestSecondFind == null) {
+                                bestSecondFind = view;
+                            }
                         }
                     } else {
+                        // We found an in bounds item, greedily return it.
                         return view;
                     }
                 }
             }
         }
-        return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch;
+        // We didn't find an in bounds item so we will settle for an item in this order:
+        // 1. bestSecondFind
+        // 2. bestFirstFind
+        // 3. invalidMatch
+        return bestSecondFind != null ? bestSecondFind :
+                (bestFirstFind != null ? bestFirstFind : invalidMatch);
     }
 
     // returns the out-of-bound child view closest to RV's end bounds. An out-of-bound child is
@@ -2327,7 +2343,8 @@
             final int size = mScrapList.size();
             for (int i = 0; i < size; i++) {
                 final View view = mScrapList.get(i).itemView;
-                final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
+                final RecyclerView.LayoutParams lp =
+                        (RecyclerView.LayoutParams) view.getLayoutParams();
                 if (lp.isItemRemoved()) {
                     continue;
                 }
@@ -2362,7 +2379,8 @@
             }
             for (int i = 0; i < size; i++) {
                 View view = mScrapList.get(i).itemView;
-                final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
+                final RecyclerView.LayoutParams lp =
+                        (RecyclerView.LayoutParams) view.getLayoutParams();
                 if (view == ignore || lp.isItemRemoved()) {
                     continue;
                 }
diff --git a/samples/Support7Demos/src/main/AndroidManifest.xml b/samples/Support7Demos/src/main/AndroidManifest.xml
index b71092f..fd36e80 100644
--- a/samples/Support7Demos/src/main/AndroidManifest.xml
+++ b/samples/Support7Demos/src/main/AndroidManifest.xml
@@ -543,6 +543,15 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".widget.RemoveLargeItemsDemo"
+            android:label="RecyclerView/Remove Large Items Demo"
+            android:theme="@style/Theme.AppCompat">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.supportv7.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
         <activity android:name=".widget.NestedRecyclerViewActivity"
                   android:label="@string/nested_recycler_view"
                   android:theme="@style/Theme.AppCompat">
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RemoveLargeItemsDemo.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RemoveLargeItemsDemo.java
new file mode 100644
index 0000000..e803693
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RemoveLargeItemsDemo.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.android.supportv7.widget;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.example.android.supportv7.R;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Test activity that displays behavior when all or some visible items are removed from a
+ * LinearLayoutManager.
+ */
+public class RemoveLargeItemsDemo extends Activity {
+
+    RecyclerView mRecyclerView;
+    private LinearLayoutManager mLinearLayoutManager;
+    MyAdapter mAdapter;
+    private int mNumItemsAdded = 0;
+    ArrayList<Item> mItems = new ArrayList<>();
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.remove_large_items_demo);
+
+        mRecyclerView = new RecyclerView(this);
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setLayoutParams(
+                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT));
+
+        mLinearLayoutManager = new LinearLayoutManager(this);
+        mRecyclerView.setLayoutManager(mLinearLayoutManager);
+
+        for (int i = 0; i < 6; ++i) {
+            mItems.add(new Item("Item #" + i));
+        }
+        mAdapter = new MyAdapter(mItems);
+        mRecyclerView.setAdapter(mAdapter);
+
+        ((ViewGroup) findViewById(R.id.container)).addView(mRecyclerView);
+
+        CheckBox reverseLayout = findViewById(R.id.reverse);
+        reverseLayout.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> mLinearLayoutManager.setReverseLayout(isChecked)
+        );
+
+        CheckBox enableStackFromEnd = findViewById(R.id.enableStackFromEnd);
+        enableStackFromEnd.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> mLinearLayoutManager.setStackFromEnd(isChecked)
+        );
+    }
+
+    /**
+     * Called by xml when a check box is checked.
+     */
+    public void checkboxClicked(@NonNull View view) {
+        ViewGroup parent = (ViewGroup) view.getParent();
+        boolean selected = ((CheckBox) view).isChecked();
+        MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent);
+        mAdapter.selectItem(holder, selected);
+    }
+
+    /**
+     * Called by xml when a item is clicked.
+     */
+    public void itemClicked(@NonNull View view) {
+        ViewGroup parent = (ViewGroup) view;
+        MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent);
+        final int position = holder.getBindingAdapterPosition();
+        if (position == RecyclerView.NO_POSITION) {
+            return;
+        }
+        mAdapter.toggleExpanded(holder);
+        mAdapter.notifyItemChanged(position);
+    }
+
+    /**
+     * Called by xml onClick to delete items that have been checked.
+     */
+    public void deleteSelectedItems(@NonNull View view) {
+        int numItems = mItems.size();
+        if (numItems > 0) {
+            for (int i = numItems - 1; i >= 0; --i) {
+                final Item item = mItems.get(i);
+                //noinspection ConstantConditions
+                Boolean selected = mAdapter.mSelected.get(item);
+                if (selected != null && selected) {
+                    removeAtPosition(i);
+                }
+            }
+        }
+    }
+
+    private void removeAtPosition(int position) {
+        if (position < mItems.size()) {
+            mItems.remove(position);
+            mAdapter.notifyItemRemoved(position);
+        }
+    }
+
+    private void addAtPosition(String text) {
+        int position = 3;
+        if (position > mItems.size()) {
+            position = mItems.size();
+        }
+        Item item = new Item(text);
+        mItems.add(position, item);
+        mAdapter.mSelected.put(item, Boolean.FALSE);
+        mAdapter.mExpanded.put(item, Boolean.FALSE);
+        mAdapter.notifyItemInserted(position);
+    }
+
+    /**
+     * Animates an item in.
+     */
+    public void addItem(@NonNull View view) {
+        addAtPosition("Added Item #" + mNumItemsAdded++);
+    }
+
+    private class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
+        private int mBackground;
+        List<Item> mData;
+        ArrayMap<Item, Boolean> mSelected = new ArrayMap<>();
+        ArrayMap<Item, Boolean> mExpanded = new ArrayMap<>();
+
+        MyAdapter(List<Item> data) {
+            TypedValue val = new TypedValue();
+            RemoveLargeItemsDemo.this.getTheme().resolveAttribute(
+                    R.attr.selectableItemBackground, val, true);
+            mBackground = val.resourceId;
+            mData = data;
+        }
+
+        @NotNull
+        @Override
+        public MyViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) {
+            MyViewHolder h =
+                    new MyViewHolder(getLayoutInflater().inflate(
+                            R.layout.remove_large_items_demo_item,
+                            mRecyclerView, false));
+            h.textView.setMinimumHeight(128);
+            h.textView.setFocusable(true);
+            h.textView.setBackgroundResource(mBackground);
+            return h;
+        }
+
+        @SuppressLint("SetTextI18n")
+        @Override
+        public void onBindViewHolder(@NotNull MyViewHolder myViewHolder, int position) {
+            Item item = mData.get(position);
+            myViewHolder.boundItem = item;
+            myViewHolder.textView.setText(item.mString);
+            Boolean selected = mSelected.get(item);
+            if (selected == null) {
+                selected = false;
+            }
+            myViewHolder.checkBox.setChecked(selected);
+            Boolean expanded = mExpanded.get(item);
+            if (Boolean.TRUE.equals(expanded)) {
+                myViewHolder.textView.setText("More text for the expanded version");
+            } else {
+                myViewHolder.textView.setText(item.mString);
+                myViewHolder.container.setBackgroundColor(item.mColor);
+            }
+        }
+
+        @Override
+        public int getItemCount() {
+            return mData.size();
+        }
+
+        public void selectItem(MyViewHolder holder, boolean selected) {
+            mSelected.put(holder.boundItem, selected);
+        }
+
+        public void toggleExpanded(MyViewHolder holder) {
+            Boolean expanded = mExpanded.get(holder.boundItem);
+            if (expanded == null) {
+                expanded = false;
+            }
+            mExpanded.put(holder.boundItem, !expanded);
+        }
+    }
+
+    static class MyViewHolder extends RecyclerView.ViewHolder {
+        public TextView textView;
+        public CheckBox checkBox;
+        public View container;
+        public Item boundItem;
+
+        MyViewHolder(View v) {
+            super(v);
+            container = v;
+            textView = v.findViewById(R.id.text);
+            checkBox = v.findViewById(R.id.selected);
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return super.toString() + " \"" + textView.getText() + "\"";
+        }
+    }
+
+    static class Item {
+        public String mString;
+        public int mColor;
+
+        Item(String string) {
+            mString = string;
+            Random rnd = new Random();
+            mColor = Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/main/res/layout/remove_large_items_demo.xml b/samples/Support7Demos/src/main/res/layout/remove_large_items_demo.xml
new file mode 100644
index 0000000..e44c9c1
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/layout/remove_large_items_demo.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <CheckBox
+            android:id="@+id/reverse"
+            android:checked="false"
+            android:text="Reverse"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+
+        <CheckBox
+            android:id="@+id/enableStackFromEnd"
+            android:checked="false"
+            android:text="StackFromEnd"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+
+    </LinearLayout>
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <Button
+            android:id="@+id/deleteButton"
+            android:layout_weight=".5"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:onClick="deleteSelectedItems"
+            android:text="@string/delete_item"/>
+
+        <Button
+            android:layout_weight=".5"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:onClick="addItem"
+            android:text="@string/add_item"/>
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/Support7Demos/src/main/res/layout/remove_large_items_demo_item.xml b/samples/Support7Demos/src/main/res/layout/remove_large_items_demo_item.xml
new file mode 100644
index 0000000..fc887c1
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/layout/remove_large_items_demo_item.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:onClick="itemClicked"
+    android:orientation="horizontal"
+    android:paddingTop="400dp"
+    android:paddingBottom="400dp">
+
+    <CheckBox
+        android:id="@+id/selected"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:onClick="checkboxClicked" />
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+</LinearLayout>
\ No newline at end of file