leanback: fix predictive animation move

1. When move items out, we need use addDisappearingView to
tell recyclerview to slide them out and those views will
not be pruned by scroll pass. This is done by adding
scrapList views in post layout fillDisappearingItems()

2. When move items out, we need check if item is moving
out side old adapter's range,  prelayout needs layout extra
space for these cases.

Bug: 38339297
Test: testDontPruneMovingItem testMoveItemToTheRight
testMoveItemToTheLeft

Change-Id: I6e2c4ffeb8befb62f060247bb6c5345dc8f6bab7
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Grid.java b/v17/leanback/src/android/support/v17/leanback/widget/Grid.java
index 148fef0..ebfa813 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/Grid.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/Grid.java
@@ -17,8 +17,10 @@
 import android.support.annotation.Nullable;
 import android.support.v4.util.CircularIntArray;
 import android.support.v7.widget.RecyclerView;
+import android.util.SparseIntArray;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
 
 /**
  * A grid is representation of single or multiple rows layout data structure and algorithm.
@@ -42,6 +44,8 @@
      */
     public static final int START_DEFAULT = -1;
 
+    Object[] mTmpItem = new Object[1];
+
     /**
      * When user uses Grid,  he should provide count of items and
      * the method to create item and remove item.
@@ -70,9 +74,12 @@
          * @param append  True if new item is after last visible item, false if new item is
          *                before first visible item.
          * @param item    item[0] returns created item that will be passed in addItem() call.
+         * @param disappearingItem The item is a disappearing item added by
+         *                         {@link Grid#fillDisappearingItems(int[], int, SparseIntArray)}.
+         *
          * @return length of the item.
          */
-        int createItem(int index, boolean append, Object[] item);
+        int createItem(int index, boolean append, Object[] item, boolean disappearingItem);
 
         /**
          * add item to given row and given edge.  The call is always after createItem().
@@ -399,6 +406,8 @@
 
     /**
      * Removes invisible items from end until reaches item at aboveIndex or toLimit.
+     * @param aboveIndex Don't remove items whose index is equals or smaller than aboveIndex
+     * @param toLimit Don't remove items whose left edge is less than toLimit.
      */
     public void removeInvisibleItemsAtEnd(int aboveIndex, int toLimit) {
         while(mLastVisibleIndex >= mFirstVisibleIndex && mLastVisibleIndex > aboveIndex) {
@@ -416,13 +425,15 @@
 
     /**
      * Removes invisible items from front until reaches item at belowIndex or toLimit.
+     * @param belowIndex Don't remove items whose index is equals or larger than belowIndex
+     * @param toLimit Don't remove items whose right edge is equals or greater than toLimit.
      */
     public void removeInvisibleItemsAtFront(int belowIndex, int toLimit) {
         while(mLastVisibleIndex >= mFirstVisibleIndex && mFirstVisibleIndex < belowIndex) {
-            boolean offFront = !mReversedFlow ? mProvider.getEdge(mFirstVisibleIndex)
-                    + mProvider.getSize(mFirstVisibleIndex) <= toLimit
-                    : mProvider.getEdge(mFirstVisibleIndex)
-                            - mProvider.getSize(mFirstVisibleIndex) >= toLimit;
+            final int size = mProvider.getSize(mFirstVisibleIndex);
+            boolean offFront = !mReversedFlow
+                    ? mProvider.getEdge(mFirstVisibleIndex) + size <= toLimit
+                    : mProvider.getEdge(mFirstVisibleIndex) - size >= toLimit;
             if (offFront) {
                 mProvider.removeItem(mFirstVisibleIndex);
                 mFirstVisibleIndex++;
@@ -440,6 +451,73 @@
     }
 
     /**
+     * Fill disappearing items, i.e. the items are moved out of window, we need give them final
+     * location so recyclerview will run a slide out animation. The positions that was greater than
+     * last visible index will be appended to end, the positions that was smaller than first visible
+     * index will be prepend to beginning.
+     * @param positions Sorted list of positions of disappearing items.
+     * @param positionToRow Which row we want to put the disappearing item.
+     */
+    public void fillDisappearingItems(int[] positions, int positionsLength,
+            SparseIntArray positionToRow) {
+        final int lastPos = getLastVisibleIndex();
+        final int resultSearchLast = lastPos >= 0
+                ? Arrays.binarySearch(positions, 0, positionsLength, lastPos) : 0;
+        if (resultSearchLast < 0) {
+            // we shouldn't find lastPos in disappearing position list.
+            int firstDisappearingIndex = -resultSearchLast - 1;
+            int edge;
+            if (mReversedFlow) {
+                edge = mProvider.getEdge(lastPos) - mProvider.getSize(lastPos) - mSpacing;
+            } else {
+                edge = mProvider.getEdge(lastPos) + mProvider.getSize(lastPos) + mSpacing;
+            }
+            for (int i = firstDisappearingIndex; i < positionsLength; i++) {
+                int disappearingIndex = positions[i];
+                int disappearingRow = positionToRow.get(disappearingIndex);
+                if (disappearingRow < 0) {
+                    disappearingRow = 0; // if not found put in row 0
+                }
+                int size = mProvider.createItem(disappearingIndex, true, mTmpItem, true);
+                mProvider.addItem(mTmpItem[0], disappearingIndex, size, disappearingRow, edge);
+                if (mReversedFlow) {
+                    edge = edge - size - mSpacing;
+                } else {
+                    edge = edge + size + mSpacing;
+                }
+            }
+        }
+
+        final int firstPos = getFirstVisibleIndex();
+        final int resultSearchFirst = firstPos >= 0
+                ? Arrays.binarySearch(positions, 0, positionsLength, firstPos) : 0;
+        if (resultSearchFirst < 0) {
+            // we shouldn't find firstPos in disappearing position list.
+            int firstDisappearingIndex = -resultSearchFirst - 2;
+            int edge;
+            if (mReversedFlow) {
+                edge = mProvider.getEdge(firstPos);
+            } else {
+                edge = mProvider.getEdge(firstPos);
+            }
+            for (int i = firstDisappearingIndex; i >= 0; i--) {
+                int disappearingIndex = positions[i];
+                int disappearingRow = positionToRow.get(disappearingIndex);
+                if (disappearingRow < 0) {
+                    disappearingRow = 0; // if not found put in row 0
+                }
+                int size = mProvider.createItem(disappearingIndex, false, mTmpItem, true);
+                if (mReversedFlow) {
+                    edge = edge + mSpacing + size;
+                } else {
+                    edge = edge - mSpacing - size;
+                }
+                mProvider.addItem(mTmpItem[0], disappearingIndex, size, disappearingRow, edge);
+            }
+        }
+    }
+
+    /**
      * Queries items adjacent to the viewport (in the direction of da) into the prefetch registry.
      */
     public void collectAdjacentPrefetchPositions(int fromLimit, int da,
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index b9d4967..0fbffca 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -38,6 +38,7 @@
 import android.support.v7.widget.RecyclerView.State;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.SparseIntArray;
 import android.view.FocusFinder;
 import android.view.Gravity;
 import android.view.View;
@@ -49,6 +50,8 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 final class GridLayoutManager extends RecyclerView.LayoutManager {
 
@@ -433,10 +436,10 @@
     // appends and prepends due to the fact leanback is doing mario scrolling: removing items to
     // the left of focused item might need extra layout on the right.
     int mExtraLayoutSpaceInPreLayout;
-    // mRetainedFirstPosInPostLayout and mRetainedLastPosInPostLayout are used in post layout pass
-    // to add those item even they are out side visible area.
-    int mRetainedFirstPosInPostLayout;
-    int mRetainedLastPosInPostLayout;
+    // mPositionToRowInPostLayout and mDisappearingPositions are temp variables in post layout.
+    final SparseIntArray mPositionToRowInPostLayout = new SparseIntArray();
+    int[] mDisappearingPositions;
+
     RecyclerView.Recycler mRecycler;
 
     private static final Rect sTempRect = new Rect();
@@ -1121,8 +1124,6 @@
         mState = state;
         mPositionDeltaInPreLayout = 0;
         mExtraLayoutSpaceInPreLayout = 0;
-        mRetainedFirstPosInPostLayout = Integer.MAX_VALUE;
-        mRetainedLastPosInPostLayout = Integer.MIN_VALUE;
     }
 
     /**
@@ -1133,8 +1134,6 @@
         mState = null;
         mPositionDeltaInPreLayout = 0;
         mExtraLayoutSpaceInPreLayout = 0;
-        mRetainedFirstPosInPostLayout = Integer.MAX_VALUE;
-        mRetainedLastPosInPostLayout = Integer.MIN_VALUE;
     }
 
     /**
@@ -1522,7 +1521,7 @@
         }
 
         @Override
-        public int createItem(int index, boolean append, Object[] item) {
+        public int createItem(int index, boolean append, Object[] item, boolean disappearingItem) {
             if (TRACE) TraceCompat.beginSection("createItem");
             if (TRACE) TraceCompat.beginSection("getview");
             View v = getViewForPosition(index - mPositionDeltaInPreLayout);
@@ -1533,10 +1532,18 @@
             // See recyclerView docs:  we don't need re-add scraped view if it was removed.
             if (!lp.isItemRemoved()) {
                 if (TRACE) TraceCompat.beginSection("addView");
-                if (append) {
-                    addView(v);
+                if (disappearingItem) {
+                    if (append) {
+                        addDisappearingView(v);
+                    } else {
+                        addDisappearingView(v, 0);
+                    }
                 } else {
-                    addView(v, 0);
+                    if (append) {
+                        addView(v);
+                    } else {
+                        addView(v, 0);
+                    }
                 }
                 if (TRACE) TraceCompat.endSection();
                 if (mChildVisibility != -1) {
@@ -1731,16 +1738,14 @@
 
     private void removeInvisibleViewsAtEnd() {
         if (mPruneChild && !mIsSlidingChildViews) {
-            int retainedLastPos = Math.max(mRetainedLastPosInPostLayout, mFocusPosition);
-            mGrid.removeInvisibleItemsAtEnd(retainedLastPos,
+            mGrid.removeInvisibleItemsAtEnd(mFocusPosition,
                     mReverseFlowPrimary ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace);
         }
     }
 
     private void removeInvisibleViewsAtFront() {
         if (mPruneChild && !mIsSlidingChildViews) {
-            int retainedFirstPos = Math.min(mRetainedFirstPosInPostLayout, mFocusPosition);
-            mGrid.removeInvisibleItemsAtFront(retainedFirstPos,
+            mGrid.removeInvisibleItemsAtFront(mFocusPosition,
                     mReverseFlowPrimary ? mSizePrimary + mExtraLayoutSpace: -mExtraLayoutSpace);
         }
     }
@@ -1979,6 +1984,48 @@
         return true;
     }
 
+    void updatePositionToRowMapInPostLayout() {
+        mPositionToRowInPostLayout.clear();
+        final int childCount = getChildCount();
+        for (int i = 0;  i < childCount; i++) {
+            int position = getAdapterPositionByIndex(i);
+            Grid.Location loc = mGrid.getLocation(position);
+            if (loc != null) {
+                mPositionToRowInPostLayout.put(position, loc.row);
+            }
+        }
+    }
+
+    void fillScrapViewsInPostLayout() {
+        List<RecyclerView.ViewHolder> scrapList = mRecycler.getScrapList();
+        final int scrapSize = scrapList.size();
+        if (scrapSize == 0) {
+            return;
+        }
+        // initialize the int array or re-allocate the array.
+        if (mDisappearingPositions == null  || scrapSize > mDisappearingPositions.length) {
+            int length = mDisappearingPositions == null ? 16 : mDisappearingPositions.length;
+            while (length < scrapSize) {
+                length = length << 1;
+            }
+            mDisappearingPositions = new int[length];
+        }
+        int totalItems = 0;
+        for (int i = 0; i < scrapSize; i++) {
+            int pos = scrapList.get(i).getAdapterPosition();
+            if (pos >= 0) {
+                mDisappearingPositions[totalItems++] = pos;
+            }
+        }
+        // totalItems now has the length of disappearing items
+        if (totalItems > 0) {
+            Arrays.sort(mDisappearingPositions, 0, totalItems);
+            mGrid.fillDisappearingItems(mDisappearingPositions, totalItems,
+                    mPositionToRowInPostLayout);
+        }
+        mPositionToRowInPostLayout.clear();
+    }
+
     // Lays out items based on the current scroll position
     @Override
     public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
@@ -2020,6 +2067,10 @@
             if (mGrid != null && childCount > 0) {
                 int minChangedEdge = Integer.MAX_VALUE;
                 int maxChangeEdge = Integer.MIN_VALUE;
+                int minOldAdapterPosition = mBaseGridView.getChildViewHolder(
+                        getChildAt(0)).getOldPosition();
+                int maxOldAdapterPosition = mBaseGridView.getChildViewHolder(
+                        getChildAt(childCount - 1)).getOldPosition();
                 for (int i = 0; i < childCount; i++) {
                     View view = getChildAt(i);
                     LayoutParams lp = (LayoutParams) view.getLayoutParams();
@@ -2029,13 +2080,17 @@
                         mPositionDeltaInPreLayout = mGrid.getFirstVisibleIndex()
                                 - lp.getViewLayoutPosition();
                     }
+                    int newAdapterPosition = mBaseGridView.getChildAdapterPosition(view);
                     // if either of following happening
                     // 1. item itself has changed or layout parameter changed
                     // 2. item is losing focus
                     // 3. item is gaining focus
+                    // 4. item is moved out of old adapter position range.
                     if (lp.isItemChanged() || lp.isItemRemoved() || view.isLayoutRequested()
                             || (!view.hasFocus() && mFocusPosition == lp.getViewAdapterPosition())
-                            || (view.hasFocus() && mFocusPosition != lp.getViewAdapterPosition())) {
+                            || (view.hasFocus() && mFocusPosition != lp.getViewAdapterPosition())
+                            || newAdapterPosition < minOldAdapterPosition
+                            || newAdapterPosition > maxOldAdapterPosition) {
                         minChangedEdge = Math.min(minChangedEdge, getViewMin(view));
                         maxChangeEdge = Math.max(maxChangeEdge, getViewMax(view));
                     }
@@ -2053,25 +2108,9 @@
             return;
         }
 
-        // figure out the child positions that needs to be retained in post layout pass, e.g.
-        // When a child is pushed out, they needs to be added in post layout pass.
-        if (state.willRunPredictiveAnimations() && getChildCount() > 0) {
-            for (int i = 0; i < getChildCount(); i++) {
-                View child = getChildAt(i);
-                int pos = mBaseGridView.getChildViewHolder(child).getAdapterPosition();
-                if (pos >= 0) {
-                    mRetainedFirstPosInPostLayout = pos;
-                    break;
-                }
-            }
-            for (int i = getChildCount() - 1; i >= 0; i--) {
-                View child = getChildAt(i);
-                int pos = mBaseGridView.getChildViewHolder(child).getAdapterPosition();
-                if (pos >= 0) {
-                    mRetainedLastPosInPostLayout = pos;
-                    break;
-                }
-            }
+        // save all view's row information before detach all views
+        if (state.willRunPredictiveAnimations()) {
+            updatePositionToRowMapInPostLayout();
         }
         // check if we need align to mFocusPosition, this is usually true unless in smoothScrolling
         final boolean scrollToFocus = !isSmoothScrolling()
@@ -2149,11 +2188,8 @@
                 removeInvisibleViewsAtEnd();
             }
         }
-        while (mGrid.getLastVisibleIndex() < mRetainedLastPosInPostLayout) {
-            appendOneColumnVisibleItems();
-        }
-        while (mGrid.getFirstVisibleIndex() > mRetainedFirstPosInPostLayout) {
-            prependOneColumnVisibleItems();
+        if (state.willRunPredictiveAnimations()) {
+            fillScrapViewsInPostLayout();
         }
 
         if (DEBUG) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java b/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
index 226bd51..56a3188 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
@@ -25,7 +25,6 @@
 class SingleRow extends Grid {
 
     private final Location mTmpLocation = new Location(0);
-    private Object[] mTmpItem = new Object[1];
 
     SingleRow() {
         setNumRows(1);
@@ -78,7 +77,7 @@
         boolean filledOne = false;
         int minIndex = mProvider.getMinIndex();
         for (int index = getStartIndexForPrepend(); index >= minIndex; index--) {
-            int size = mProvider.createItem(index, false, mTmpItem);
+            int size = mProvider.createItem(index, false, mTmpItem, false);
             int edge;
             if (mFirstVisibleIndex < 0 || mLastVisibleIndex < 0) {
                 edge = mReversedFlow ? Integer.MIN_VALUE : Integer.MAX_VALUE;
@@ -111,7 +110,7 @@
         }
         boolean filledOne = false;
         for (int index = getStartIndexForAppend(); index < mProvider.getCount(); index++) {
-            int size = mProvider.createItem(index, true, mTmpItem);
+            int size = mProvider.createItem(index, true, mTmpItem, false);
             int edge;
             if (mFirstVisibleIndex < 0 || mLastVisibleIndex< 0) {
                 edge = mReversedFlow ? Integer.MAX_VALUE : Integer.MIN_VALUE;
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
index eb9528e..fb47c5b 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
@@ -67,8 +67,6 @@
     //    <= mFirstIndex + mLocations.size() - 1
     protected int mFirstIndex = -1;
 
-    private Object[] mTmpItem = new Object[1];
-
     protected Object mPendingItem;
     protected int mPendingItemSize;
 
@@ -167,7 +165,7 @@
         for (; itemIndex >= firstIndex; itemIndex--) {
             Location loc = getLocation(itemIndex);
             int rowIndex = loc.row;
-            int size = mProvider.createItem(itemIndex, false, mTmpItem);
+            int size = mProvider.createItem(itemIndex, false, mTmpItem, false);
             if (size != loc.size) {
                 mLocations.removeFromStart(itemIndex + 1 - mFirstIndex);
                 mFirstIndex = mFirstVisibleIndex;
@@ -254,7 +252,7 @@
             item = mPendingItem;
             mPendingItem = null;
         } else {
-            loc.size = mProvider.createItem(itemIndex, false, mTmpItem);
+            loc.size = mProvider.createItem(itemIndex, false, mTmpItem, false);
             item = mTmpItem[0];
         }
         mFirstIndex = mFirstVisibleIndex = itemIndex;
@@ -324,7 +322,7 @@
                 edge = edge + loc.offset;
             }
             int rowIndex = loc.row;
-            int size = mProvider.createItem(itemIndex, true, mTmpItem);
+            int size = mProvider.createItem(itemIndex, true, mTmpItem, false);
             if (size != loc.size) {
                 loc.size = size;
                 mLocations.removeFromEnd(lastIndex - itemIndex);
@@ -388,7 +386,7 @@
             item = mPendingItem;
             mPendingItem = null;
         } else {
-            loc.size = mProvider.createItem(itemIndex, true, mTmpItem);
+            loc.size = mProvider.createItem(itemIndex, true, mTmpItem, false);
             item = mTmpItem[0];
         }
         if (mLocations.size() == 1) {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
index c97617d..d9f446f 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
@@ -263,6 +263,17 @@
         mGridView.getAdapter().notifyItemMoved(index2 - 1, index1);
     }
 
+    void moveItem(int index1, int index2, boolean notify) {
+        if (index1 == index2) {
+            return;
+        }
+        int[] items = removeItems(index1, 1, false);
+        addItems(index2, items, false);
+        if (notify) {
+            mGridView.getAdapter().notifyItemMoved(index1, index2);
+        }
+    }
+
     void changeArraySize(int length) {
         mNumItems = length;
         mGridView.getAdapter().notifyDataSetChanged();
@@ -299,6 +310,10 @@
     }
 
     void addItems(int index, int[] items) {
+        addItems(index, items, true);
+    }
+
+    void addItems(int index, int[] items, boolean notify) {
         int length = items.length;
         if (mItemLengths.length < mNumItems + length) {
             int[] array = new int[mNumItems + length];
@@ -308,7 +323,7 @@
         System.arraycopy(mItemLengths, index, mItemLengths, index + length, mNumItems - index);
         System.arraycopy(items, 0, mItemLengths, index, length);
         mNumItems += length;
-        if (mGridView.getAdapter() != null) {
+        if (notify && mGridView.getAdapter() != null) {
             mGridView.getAdapter().notifyItemRangeInserted(index, length);
         }
     }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
index 605adfa..43793f9 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
@@ -43,7 +43,7 @@
         }
 
         @Override
-        public int createItem(int index, boolean append, Object[] item) {
+        public int createItem(int index, boolean append, Object[] item, boolean disappearingItem) {
             return mItems[index];
         }
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 8b38db0..e3af480 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
@@ -930,6 +931,194 @@
         verifyBeginAligned();
     }
 
+    void waitOneUiCycle() throws Throwable {
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+            }
+        });
+    }
+
+    @Test
+    public void testDontPruneMovingItem() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
+        initActivity(intent);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.getItemAnimator().setMoveDuration(2000);
+                mGridView.setSelectedPosition(50);
+            }
+        });
+        waitForScrollIdle();
+        final ArrayList<RecyclerView.ViewHolder> moveViewHolders = new ArrayList();
+        for (int i = 51;; i++) {
+            RecyclerView.ViewHolder vh = mGridView.findViewHolderForAdapterPosition(i);
+            if (vh == null) {
+                break;
+            }
+            moveViewHolders.add(vh);
+        }
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // add a lot of items, so we will push everything to right of 51 out side window
+                int[] lots_items = new int[1000];
+                for (int i = 0; i < lots_items.length; i++) {
+                    lots_items[i] = 300;
+                }
+                mActivity.addItems(51, lots_items);
+            }
+        });
+        waitOneUiCycle();
+        // run a scroll pass, the scroll pass should not remove the animating views even they are
+        // outside visible areas.
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.scrollBy(-3, 0);
+            }
+        });
+        waitOneUiCycle();
+        for (int i = 0; i < moveViewHolders.size(); i++) {
+            assertSame(mGridView, moveViewHolders.get(i).itemView.getParent());
+        }
+    }
+
+    @Test
+    public void testMoveItemToTheRight() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
+        initActivity(intent);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.getItemAnimator().setAddDuration(2000);
+                mGridView.getItemAnimator().setMoveDuration(2000);
+                mGridView.setSelectedPosition(50);
+            }
+        });
+        waitForScrollIdle();
+        RecyclerView.ViewHolder moveViewHolder = mGridView.findViewHolderForAdapterPosition(51);
+
+        int lastPos = mGridView.getChildAdapterPosition(mGridView.getChildAt(
+                mGridView.getChildCount() - 1));
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.moveItem(51, 1000, true);
+            }
+        });
+        final ArrayList<View> moveInViewHolders = new ArrayList();
+        waitForItemAnimationStart();
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                for (int i = 0; i < mGridView.getLayoutManager().getChildCount(); i++) {
+                    View v = mGridView.getLayoutManager().getChildAt(i);
+                    if (Math.abs(v.getTranslationX()) > 5) {
+                        moveInViewHolders.add(v);
+                    }
+                }
+            }
+        });
+        waitOneUiCycle();
+        assertTrue("prelayout should layout extra items to slide in",
+                moveInViewHolders.size() > lastPos - 51);
+        // run a scroll pass, the scroll pass should not remove the animating views even they are
+        // outside visible areas.
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.scrollBy(-3, 0);
+            }
+        });
+        waitOneUiCycle();
+        for (int i = 0; i < moveInViewHolders.size(); i++) {
+            assertSame(mGridView, moveInViewHolders.get(i).getParent());
+        }
+        assertSame(mGridView, moveViewHolder.itemView.getParent());
+        assertFalse(moveViewHolder.isRecyclable());
+        waitForItemAnimation();
+        assertNull(moveViewHolder.itemView.getParent());
+        assertTrue(moveViewHolder.isRecyclable());
+    }
+
+    @Test
+    public void testMoveItemToTheLeft() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 2000);
+        initActivity(intent);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.getItemAnimator().setAddDuration(2000);
+                mGridView.getItemAnimator().setMoveDuration(2000);
+                mGridView.setSelectedPosition(1500);
+            }
+        });
+        waitForScrollIdle();
+        RecyclerView.ViewHolder moveViewHolder = mGridView.findViewHolderForAdapterPosition(1499);
+
+        int firstPos = mGridView.getChildAdapterPosition(mGridView.getChildAt(0));
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.moveItem(1499, 1, true);
+            }
+        });
+        final ArrayList<View> moveInViewHolders = new ArrayList();
+        waitForItemAnimationStart();
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                for (int i = 0; i < mGridView.getLayoutManager().getChildCount(); i++) {
+                    View v = mGridView.getLayoutManager().getChildAt(i);
+                    if (Math.abs(v.getTranslationX()) > 5) {
+                        moveInViewHolders.add(v);
+                    }
+                }
+            }
+        });
+        waitOneUiCycle();
+        assertTrue("prelayout should layout extra items to slide in ",
+                moveInViewHolders.size() > 1499 - firstPos);
+        // run a scroll pass, the scroll pass should not remove the animating views even they are
+        // outside visible areas.
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.scrollBy(3, 0);
+            }
+        });
+        waitOneUiCycle();
+        for (int i = 0; i < moveInViewHolders.size(); i++) {
+            assertSame(mGridView, moveInViewHolders.get(i).getParent());
+        }
+        assertSame(mGridView, moveViewHolder.itemView.getParent());
+        assertFalse(moveViewHolder.isRecyclable());
+        waitForItemAnimation();
+        assertNull(moveViewHolder.itemView.getParent());
+        assertTrue(moveViewHolder.isRecyclable());
+    }
+
     @Test
     public void testContinuousSwapForward() throws Throwable {
         Intent intent = new Intent();