Merge "Make TextViewCompat#setTextAppearance work with fonts" into oc-support-26.0-dev
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();