Reuse hidden views if asked for

If a LayoutManager tries to get a view that is in the
hidden list, Recycler used to recycle it so that it
can be safely rebound as a new item.

This approach, although safe, creates UI glitches
as item jumps from a location to another instantly.
To overcome this issue, now Recycler unhides such
views and moves them to scrap. It also reports it
to the item animator with necessary flags so that
its appearance can properly be evaluated as a
persistence (or change).

Bug: 24176126

Change-Id: I9858abdd6faf859bdc2ee2a9d2a88228c21fa9b1
diff --git a/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java b/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
index 66ebab5..fd1cee4 100644
--- a/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
@@ -339,6 +339,25 @@
         }
     }
 
+    /**
+     * Moves a child view from hidden list to regular list.
+     * Calling this method should probably be followed by a detach, otherwise, it will suddenly
+     * show up in LayoutManager's children list.
+     *
+     * @param view The hidden View to unhide
+     */
+    void unhide(View view) {
+        final int offset = mCallback.indexOfChild(view);
+        if (offset < 0) {
+            throw new IllegalArgumentException("view is not a child, cannot hide " + view);
+        }
+        if (!mBucket.get(offset)) {
+            throw new RuntimeException("trying to unhide a view that was not hidden" + view);
+        }
+        mBucket.clear(offset);
+        unhideViewInternal(view);
+    }
+
     @Override
     public String toString() {
         return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index ee4278f..f0b0afe 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -72,6 +72,7 @@
 import java.util.List;
 
 import static android.support.v7.widget.AdapterHelper.Callback;
+import static android.support.v7.widget.AdapterHelper.POSITION_TYPE_INVISIBLE;
 import static android.support.v7.widget.AdapterHelper.UpdateOp;
 import android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
 
@@ -2773,8 +2774,8 @@
         onEnterLayoutOrScroll();
 
         processAdapterUpdatesAndSetAnimationFlags();
-        final boolean trackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
-        if (trackOldChangeHolders && mState.mOldChangedHolders == null) {
+        mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
+        if (mState.mTrackOldChangeHolders && mState.mOldChangedHolders == null) {
             mState.mOldChangedHolders = new ArrayMap<>();
         }
         mItemsAddedOrRemoved = mItemsChanged = false;
@@ -2798,9 +2799,16 @@
                                 ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                 holder.getUnmodifiedPayloads());
                 mState.mPreLayoutHolderMap.put(holder, animationInfo);
-                if (trackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
+                if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                         && !holder.shouldIgnore() && !holder.isInvalid()) {
                     long key = getChangedHolderKey(holder);
+                    // This is NOT the only place where a ViewHolder is added to old change holders
+                    // list. There is another case where:
+                    //    * A VH is currently hidden but not deleted
+                    //    * The hidden item is changed in the adapter
+                    //    * Layout manager decides to layout the item in the pre-Layout pass (step1)
+                    // When this case is detected, RV will un-hide that view and add to the old
+                    // change holders list.
                     mState.mOldChangedHolders.put(key, holder);
                 }
             }
@@ -2836,10 +2844,18 @@
                 }
                 if (!found) {
                     int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
-                    flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+                    boolean wasHidden = viewHolder
+                            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+                    if (!wasHidden) {
+                        flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+                    }
                     final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                             mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
-                    appearingViewInfo.put(child, animationInfo);
+                    if (wasHidden) {
+                        recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
+                    } else {
+                        appearingViewInfo.put(child, animationInfo);
+                    }
                 }
             }
             // we don't process disappearing list because they may re-appear in post layout pass.
@@ -2872,7 +2888,7 @@
                 long key = getChangedHolderKey(holder);
                 final ItemHolderInfo animationInfo = mItemAnimator
                         .recordPostLayoutInformation(mState, holder);
-                ViewHolder oldChangeViewHolder = trackOldChangeHolders ?
+                ViewHolder oldChangeViewHolder = mState.mTrackOldChangeHolders ?
                         mState.mOldChangedHolders.get(key) : null;
                 if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                     // run a change animation
@@ -2938,6 +2954,8 @@
         mState.mPreviousLayoutItemCount = mState.mItemCount;
         mDataSetHasChangedAfterLayout = false;
         mState.mRunSimpleAnimations = false;
+        mState.mPreLayoutHolderMap.clear();
+        mState.mPostLayoutHolderMap.clear();
         mState.mRunPredictiveAnimations = false;
         onExitLayoutOrScroll();
         mLayout.mRequestedSimpleAnimations = false;
@@ -2953,6 +2971,22 @@
         }
     }
 
+    /**
+     * Records the animation information for a view holder that was bounced from hidden list. It
+     * also clears the bounce back flag.
+     */
+    private void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder,
+            ItemHolderInfo animationInfo) {
+        // looks like this view bounced back from hidden list!
+        viewHolder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+        if (mState.mTrackOldChangeHolders && viewHolder.isUpdated()
+                && !viewHolder.isRemoved() && !viewHolder.shouldIgnore()) {
+            long key = getChangedHolderKey(viewHolder);
+            mState.mOldChangedHolders.put(key, viewHolder);
+        }
+        mState.mPreLayoutHolderMap.put(viewHolder, animationInfo);
+    }
+
     private void findMinMaxChildLayoutPositions(int[] into) {
         final int count = mChildHelper.getChildCount();
         if (count == 0) {
@@ -4466,6 +4500,23 @@
                     }
                 }
             }
+
+            // This is very ugly but the only place we can grab this information
+            // before the View is rebound and returned to the LayoutManager for post layout ops.
+            // We don't need this in pre-layout since the VH is not updated by the LM.
+            if (fromScrap && !mState.isPreLayout() && holder
+                    .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
+                holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+                if (mState.mRunSimpleAnimations) {
+                    int changeFlags = ItemAnimator
+                            .buildAdapterChangeFlagsForAnimations(holder);
+                    changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+                    final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
+                            holder, changeFlags, holder.getUnmodifiedPayloads());
+                    recordAnimationInfoIfBouncedHiddenView(holder, info);
+                }
+            }
+
             boolean bound = false;
             if (mState.isPreLayout() && holder.isBound()) {
                 // do not update unless we absolutely have to.
@@ -4812,8 +4863,20 @@
             if (!dryRun) {
                 View view = mChildHelper.findHiddenNonRemovedView(position, type);
                 if (view != null) {
-                    // ending the animation should cause it to get recycled before we reuse it
-                    mItemAnimator.endAnimation(getChildViewHolder(view));
+                    // This View is good to be used. We just need to unhide, detach and move to the
+                    // scrap list.
+                    final ViewHolder vh = getChildViewHolderInt(view);
+                    mChildHelper.unhide(view);
+                    int layoutIndex = mChildHelper.indexOfChild(view);
+                    if (layoutIndex == RecyclerView.NO_POSITION) {
+                        throw new IllegalStateException("layout index should not be -1 after "
+                                + "unhiding a view:" + vh);
+                    }
+                    mChildHelper.detachViewFromParent(layoutIndex);
+                    scrapView(view);
+                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
+                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+                    return vh;
                 }
             }
 
@@ -8192,6 +8255,21 @@
          */
         static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12;
 
+        /**
+         * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from
+         * hidden list (as if it was scrap) without being recycled in between.
+         *
+         * When a ViewHolder is hidden, there are 2 paths it can be re-used:
+         *   a) Animation ends, view is recycled and used from the recycle pool.
+         *   b) LayoutManager asks for the View for that position while the ViewHolder is hidden.
+         *
+         * This flag is used to represent "case b" where the ViewHolder is reused without being
+         * recycled (thus "bounced" from the hidden list). This state requires special handling
+         * because the ViewHolder must be added to pre layout maps for animations as if it was
+         * already there.
+         */
+        static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;
+
         private int mFlags;
 
         private static final List<Object> FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST;
@@ -9344,6 +9422,8 @@
 
         private boolean mRunPredictiveAnimations = false;
 
+        private boolean mTrackOldChangeHolders = false;
+
         State reset() {
             mTargetPosition = RecyclerView.NO_POSITION;
             if (mData != null) {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
index 1d095b4..e690f22 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
@@ -630,4 +630,69 @@
             }
         }
     }
+
+    static class LoggingInfo extends RecyclerView.ItemAnimator.ItemHolderInfo {
+        final RecyclerView.ViewHolder viewHolder;
+        @RecyclerView.ItemAnimator.AdapterChanges
+        final int changeFlags;
+        final List<Object> payloads;
+
+        LoggingInfo(RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
+            this.viewHolder = viewHolder;
+            this.changeFlags = changeFlags;
+            if (payloads != null) {
+                this.payloads = new ArrayList<>();
+                this.payloads.addAll(payloads);
+            } else {
+                this.payloads = null;
+            }
+            setFrom(viewHolder);
+        }
+    }
+
+    static class AnimateChange extends AnimatePersistence {
+
+        final RecyclerView.ViewHolder newHolder;
+
+        public AnimateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
+                LoggingInfo pre, LoggingInfo post) {
+            super(oldHolder, pre, post);
+            this.newHolder = newHolder;
+        }
+    }
+
+    static class AnimatePersistence extends AnimateAppearance {
+
+        public AnimatePersistence(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
+                LoggingInfo post) {
+            super(viewHolder, pre, post);
+        }
+    }
+
+    static class AnimateAppearance extends AnimateDisappearance {
+
+        final LoggingInfo postInfo;
+
+        public AnimateAppearance(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
+                LoggingInfo post) {
+            super(viewHolder, pre);
+            this.postInfo = post;
+        }
+    }
+
+    static class AnimateDisappearance extends AnimateLogBase {
+        public AnimateDisappearance(RecyclerView.ViewHolder viewHolder, LoggingInfo pre) {
+            super(viewHolder, pre);
+        }
+    }
+    static class AnimateLogBase {
+
+        final RecyclerView.ViewHolder viewHolder;
+        final LoggingInfo preInfo;
+
+        public AnimateLogBase(RecyclerView.ViewHolder viewHolder, LoggingInfo pre) {
+            this.viewHolder = viewHolder;
+            this.preInfo = pre;
+        }
+    }
 }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 53c3f81..c5f3408 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -202,19 +202,23 @@
         mRecyclerView = null;
     }
 
-    void waitForAnimations(int seconds) throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(2);
-        boolean running = mRecyclerView.mItemAnimator
-                .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
-                    @Override
-                    public void onAnimationsFinished() {
-                        latch.countDown();
-                    }
-                });
-        if (running) {
-            latch.countDown();
-            latch.await(seconds, TimeUnit.SECONDS);
-        }
+    void waitForAnimations(int seconds) throws Throwable {
+        final CountDownLatch latch = new CountDownLatch(1);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mRecyclerView.mItemAnimator
+                        .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
+                            @Override
+                            public void onAnimationsFinished() {
+                                latch.countDown();
+                            }
+                        });
+            }
+        });
+
+        assertTrue("animations didn't finish on expected time of " + seconds + " seconds",
+                latch.await(seconds, TimeUnit.SECONDS));
     }
 
     public boolean requestFocus(final View view) {
@@ -548,6 +552,7 @@
         int mAdapterIndex;
 
         final String mText;
+        int mType = 0;
 
         Item(int adapterIndex, String text) {
             mAdapterIndex = adapterIndex;
@@ -584,6 +589,11 @@
         }
 
         @Override
+        public int getItemViewType(int position) {
+            return getItemAt(position).mType;
+        }
+
+        @Override
         public void onViewAttachedToWindow(TestViewHolder holder) {
             super.onViewAttachedToWindow(holder);
             mAttachmentCounter.onViewAttached(holder);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java
index c9192a7..0e4bca7 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java
@@ -440,7 +440,7 @@
         @Override
         public boolean animateDisappearance(RecyclerView.ViewHolder viewHolder,
                 ItemHolderInfo preInfo) {
-            animateDisappearanceList.add(new AnimateDisappearance(viewHolder, preInfo));
+            animateDisappearanceList.add(new AnimateDisappearance(viewHolder, (LoggingInfo) preInfo));
             assertSame(preLayoutInfo.get(viewHolder), preInfo);
             dispatchAnimationFinished(viewHolder);
 
@@ -451,7 +451,7 @@
         public boolean animateAppearance(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preInfo,
                 ItemHolderInfo postInfo) {
             animateAppearanceList.add(
-                    new AnimateAppearance(viewHolder, preInfo, postInfo));
+                    new AnimateAppearance(viewHolder, (LoggingInfo) preInfo, (LoggingInfo) postInfo));
             assertSame(preLayoutInfo.get(viewHolder), preInfo);
             assertSame(postLayoutInfo.get(viewHolder), postInfo);
             dispatchAnimationFinished(viewHolder);
@@ -462,7 +462,8 @@
         public boolean animatePersistence(RecyclerView.ViewHolder viewHolder,
                 ItemHolderInfo preInfo,
                 ItemHolderInfo postInfo) {
-            animatePersistenceList.add(new AnimatePersistence(viewHolder, preInfo, postInfo));
+            animatePersistenceList.add(new AnimatePersistence(viewHolder, (LoggingInfo) preInfo,
+                    (LoggingInfo) postInfo));
             dispatchAnimationFinished(viewHolder);
             assertSame(preLayoutInfo.get(viewHolder), preInfo);
             assertSame(postLayoutInfo.get(viewHolder), postInfo);
@@ -473,7 +474,8 @@
         public boolean animateChange(RecyclerView.ViewHolder oldHolder,
                 RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo,
                 ItemHolderInfo postInfo) {
-            animateChangeList.add(new AnimateChange(oldHolder, newHolder, preInfo, postInfo));
+            animateChangeList.add(new AnimateChange(oldHolder, newHolder, (LoggingInfo) preInfo,
+                    (LoggingInfo) postInfo));
             if (oldHolder != null) {
                 dispatchAnimationFinished(oldHolder);
                 assertSame(preLayoutInfo.get(oldHolder), preInfo);
@@ -506,65 +508,6 @@
         }
     }
 
-    static class LoggingInfo extends RecyclerView.ItemAnimator.ItemHolderInfo {
-
-        final RecyclerView.ViewHolder viewHolder;
-        @RecyclerView.ItemAnimator.AdapterChanges
-        final int changeFlags;
-        final List<Object> payloads;
-
-        LoggingInfo(RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
-            this.viewHolder = viewHolder;
-            this.changeFlags = changeFlags;
-            if (payloads != null) {
-                this.payloads = new ArrayList<>();
-                this.payloads.addAll(payloads);
-            } else {
-                this.payloads = null;
-            }
-            setFrom(viewHolder);
-        }
-    }
-
-    static class AnimateChange extends AnimatePersistence {
-
-        final RecyclerView.ViewHolder newHolder;
-
-        public AnimateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
-                Object pre, Object post) {
-            super(oldHolder, pre, post);
-            this.newHolder = newHolder;
-        }
-    }
-
-    static class AnimatePersistence extends AnimateAppearance {
-
-        public AnimatePersistence(RecyclerView.ViewHolder viewHolder, Object pre, Object post) {
-            super(viewHolder, pre, post);
-        }
-    }
-
-    static class AnimateAppearance extends AnimateDisappearance {
-
-        final LoggingInfo postInfo;
-
-        public AnimateAppearance(RecyclerView.ViewHolder viewHolder, Object pre, Object post) {
-            super(viewHolder, pre);
-            this.postInfo = (LoggingInfo) post;
-        }
-    }
-
-    static class AnimateDisappearance {
-
-        final RecyclerView.ViewHolder viewHolder;
-        final LoggingInfo preInfo;
-
-        public AnimateDisappearance(RecyclerView.ViewHolder viewHolder, Object pre) {
-            this.viewHolder = viewHolder;
-            this.preInfo = (LoggingInfo) pre;
-        }
-    }
-
     interface CanReUseCallback {
 
         boolean canReUse(RecyclerView.ViewHolder viewHolder);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java b/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
index 04031d4..e41286e 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
@@ -14,6 +14,7 @@
 package android.support.v7.widget;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -29,8 +30,59 @@
 
     final ArrayList<RecyclerView.ViewHolder> mChangeNewVHs = new ArrayList<RecyclerView.ViewHolder>();
 
+    List<BaseRecyclerViewAnimationsTest.AnimateAppearance> mAnimateAppearanceList = new ArrayList<>();
+    List<BaseRecyclerViewAnimationsTest.AnimateDisappearance> mAnimateDisappearanceList = new ArrayList<>();
+    List<BaseRecyclerViewAnimationsTest.AnimatePersistence> mAnimatePersistenceList = new ArrayList<>();
+    List<BaseRecyclerViewAnimationsTest.AnimateChange> mAnimateChangeList = new ArrayList<>();
+
     CountDownLatch mWaitForPendingAnimations;
 
+    public boolean contains(RecyclerView.ViewHolder viewHolder,
+            List<? extends BaseRecyclerViewAnimationsTest.AnimateLogBase> list) {
+        for (BaseRecyclerViewAnimationsTest.AnimateLogBase log : list) {
+            if (log.viewHolder == viewHolder) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean animateDisappearance(RecyclerView.ViewHolder viewHolder,
+            ItemHolderInfo preLayoutInfo) {
+        mAnimateDisappearanceList
+                .add(new BaseRecyclerViewAnimationsTest.AnimateDisappearance(viewHolder, null));
+        return super.animateDisappearance(viewHolder, preLayoutInfo);
+    }
+
+    @Override
+    public boolean animateAppearance(RecyclerView.ViewHolder viewHolder,
+            ItemHolderInfo preLayoutInfo,
+            ItemHolderInfo postLayoutInfo) {
+        mAnimateAppearanceList
+                .add(new BaseRecyclerViewAnimationsTest.AnimateAppearance(viewHolder, null, null));
+        return super.animateAppearance(viewHolder, preLayoutInfo, postLayoutInfo);
+    }
+
+    @Override
+    public boolean animatePersistence(RecyclerView.ViewHolder viewHolder,
+            ItemHolderInfo preInfo,
+            ItemHolderInfo postInfo) {
+        mAnimatePersistenceList
+                .add(new BaseRecyclerViewAnimationsTest.AnimatePersistence(viewHolder, null, null));
+        return super.animatePersistence(viewHolder, preInfo, postInfo);
+    }
+
+    @Override
+    public boolean animateChange(RecyclerView.ViewHolder oldHolder,
+            RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo,
+            ItemHolderInfo postInfo) {
+        mAnimateChangeList
+                .add(new BaseRecyclerViewAnimationsTest.AnimateChange(oldHolder, newHolder, null,
+                        null));
+        return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
+    }
+
     @Override
     public void runPendingAnimations() {
         if (mWaitForPendingAnimations != null) {
@@ -84,5 +136,9 @@
         mMoveVHs.clear();
         mChangeOldVHs.clear();
         mChangeNewVHs.clear();
+        mAnimateChangeList.clear();
+        mAnimatePersistenceList.clear();
+        mAnimateAppearanceList.clear();
+        mAnimateDisappearanceList.clear();
     }
 }
\ No newline at end of file
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
index 85b6a5f..9887d99 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -17,6 +17,7 @@
 package android.support.v7.widget;
 
 import android.graphics.Rect;
+import android.os.Debug;
 import android.support.v4.view.ViewCompat;
 import android.util.Log;
 import android.view.View;
@@ -35,6 +36,310 @@
  */
 public class RecyclerViewAnimationsTest extends BaseRecyclerViewAnimationsTest {
 
+    final List<TestViewHolder> recycledVHs = new ArrayList<>();
+
+    public void testDontLayoutReusedViewWithoutPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    final TestViewHolder target) throws Throwable {
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+                    @Override
+                    void beforePreLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager lm, RecyclerView.State state) {
+                        fail("pre layout is not expected");
+                    }
+
+                    @Override
+                    void beforePostLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        mLayoutItemCount = 7;
+                        View targetView = recycler
+                                .getViewForPosition(target.getAdapterPosition());
+                        assertSame(targetView, target.itemView);
+                        super.beforePostLayout(recycler, layoutManager, state);
+                    }
+
+                    @Override
+                    void afterPostLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        super.afterPostLayout(recycler, layoutManager, state);
+                        assertNull("test sanity. this view should not be re-laid out in post "
+                                + "layout", target.itemView.getParent());
+                    }
+                };
+                mLayoutManager.expectLayouts(1);
+                mLayoutManager.requestSimpleAnimationsInNextLayout();
+                requestLayoutOnUIThread(mRecyclerView);
+                mLayoutManager.waitForLayout(2);
+                checkForMainThreadException();
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                // This is a LayoutManager problem if it asked for the view but didn't properly
+                // lay it out. It will move to disappearance
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                waitForAnimations(5);
+                assertTrue(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testDontLayoutReusedViewWithPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    final TestViewHolder target) throws Throwable {
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+                    @Override
+                    void beforePreLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager lm, RecyclerView.State state) {
+                        mLayoutItemCount = 9;
+                        super.beforePreLayout(recycler, lm, state);
+                    }
+
+                    @Override
+                    void beforePostLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        mLayoutItemCount = 7;
+                        super.beforePostLayout(recycler, layoutManager, state);
+                    }
+
+                    @Override
+                    void afterPostLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        super.afterPostLayout(recycler, layoutManager, state);
+                        assertNull("test sanity. this view should not be re-laid out in post "
+                                + "layout", target.itemView.getParent());
+                    }
+                };
+                mLayoutManager.expectLayouts(2);
+                mTestAdapter.deleteAndNotify(1, 1);
+                mLayoutManager.waitForLayout(2);
+                checkForMainThreadException();
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                // This is a LayoutManager problem if it asked for the view but didn't properly
+                // lay it out. It will move to disappearance.
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                waitForAnimations(5);
+                assertTrue(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testReuseHiddenViewWithoutPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(1);
+                mLayoutManager.requestSimpleAnimationsInNextLayout();
+                mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
+                requestLayoutOnUIThread(mRecyclerView);
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertFalse(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testReuseHiddenViewWithoutAnimations() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(1);
+                mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
+                requestLayoutOnUIThread(mRecyclerView);
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertFalse(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testReuseHiddenViewWithPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                // it should move to change scrap and then show up from there
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(2);
+                mTestAdapter.deleteAndNotify(2, 1);
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                // This LM does not layout the additional item so it does predictive wrong.
+                // We should still handle it and animate persistence for this item
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertTrue(itemAnimator.mMoveVHs.contains(target));
+                assertFalse(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testReuseHiddenViewWithProperPredictive() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                // it should move to change scrap and then show up from there
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+                    @Override
+                    void beforePreLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager lm, RecyclerView.State state) {
+                        mLayoutItemCount = 9;
+                        super.beforePreLayout(recycler, lm, state);
+                    }
+
+                    @Override
+                    void afterPreLayout(RecyclerView.Recycler recycler,
+                            AnimationLayoutManager layoutManager,
+                            RecyclerView.State state) {
+                        mLayoutItemCount = 8;
+                        super.afterPreLayout(recycler, layoutManager, state);
+                    }
+                };
+
+                mLayoutManager.expectLayouts(2);
+                mTestAdapter.deleteAndNotify(2, 1);
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                // This LM implements predictive animations properly by requesting target view
+                // in pre-layout.
+                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertTrue(itemAnimator.mMoveVHs.contains(target));
+                assertFalse(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testDontReuseHiddenViewOnInvalidate() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                // it should move to change scrap and then show up from there
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(1);
+                mTestAdapter.dispatchDataSetChanged();
+                mLayoutManager.waitForLayout(2);
+                waitForAnimations(5);
+                assertFalse(mRecyclerView.getItemAnimator().isRunning());
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertTrue(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    public void testDontReuseOnTypeChange() throws Throwable {
+        reuseHiddenViewTest(new ReuseTestCallback() {
+            @Override
+            public void postSetup(List<TestViewHolder> recycledList,
+                    TestViewHolder target) throws Throwable {
+                // it should move to change scrap and then show up from there
+                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+                        .getItemAnimator();
+                itemAnimator.reset();
+                mLayoutManager.expectLayouts(1);
+                target.mBoundItem.mType += 2;
+                mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
+                mTestAdapter.changeAndNotify(target.getAdapterPosition(), 1);
+                requestLayoutOnUIThread(mRecyclerView);
+                mLayoutManager.waitForLayout(2);
+
+                assertTrue(itemAnimator.mChangeOldVHs.contains(target));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+                assertTrue(mRecyclerView.mChildHelper.isHidden(target.itemView));
+                assertFalse(recycledVHs.contains(target));
+                waitForAnimations(5);
+                assertTrue(recycledVHs.contains(target));
+            }
+        });
+    }
+
+    interface ReuseTestCallback {
+
+        void postSetup(List<TestViewHolder> recycledList, TestViewHolder target) throws Throwable;
+    }
+
+    @Override
+    protected RecyclerView.ItemAnimator createItemAnimator() {
+        return new LoggingItemAnimator();
+    }
+
+    public void reuseHiddenViewTest(ReuseTestCallback callback) throws Throwable {
+        TestAdapter adapter = new TestAdapter(10) {
+            @Override
+            public void onViewRecycled(TestViewHolder holder) {
+                super.onViewRecycled(holder);
+                recycledVHs.add(holder);
+            }
+        };
+        setupBasic(10, 0, 10, adapter);
+        mRecyclerView.setItemViewCacheSize(0);
+        TestViewHolder target = (TestViewHolder) mRecyclerView.findViewHolderForAdapterPosition(9);
+        mRecyclerView.getItemAnimator().setAddDuration(1000);
+        mRecyclerView.getItemAnimator().setRemoveDuration(1000);
+        mRecyclerView.getItemAnimator().setChangeDuration(1000);
+        mRecyclerView.getItemAnimator().setMoveDuration(1000);
+        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
+        mLayoutManager.expectLayouts(2);
+        adapter.deleteAndNotify(2, 1);
+        mLayoutManager.waitForLayout(2);
+        // test sanity, make sure target is hidden now
+        assertTrue("test sanity", mRecyclerView.mChildHelper.isHidden(target.itemView));
+        callback.postSetup(recycledVHs, target);
+        // TODO TEST ITEM INVALIDATION OR TYPE CHANGE IN BETWEEN
+        // TODO TEST ITEM IS RECEIVED FROM RECYCLER BUT NOT RE-ADDED
+        // TODO TEST ITEM ANIMATOR IS CALLED TO GET NEW INFORMATION ABOUT LOCATION
+
+    }
+
     public void testDetachBeforeAnimations() throws Throwable {
         setupBasic(10, 0, 5);
         final RecyclerView rv = mRecyclerView;
@@ -76,7 +381,7 @@
                 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
                     @Override
                     public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
-                                               RecyclerView.State state) {
+                            RecyclerView.State state) {
                         if (view == targetChild[0]) {
                             outRect.set(10, 20, 30, 40);
                         } else {
@@ -230,7 +535,6 @@
         mLayoutManager.waitForLayout(2);
 
 
-
     }
 
     public void testAddRemoveSamePass() throws Throwable {
@@ -326,7 +630,7 @@
         changeAnimTest(false, false, true, false);
     }
 
-    public void testChangeAnimations()  throws Throwable {
+    public void testChangeAnimations() throws Throwable {
         final boolean[] booleans = {true, false};
         for (boolean supportsChange : booleans) {
             for (boolean changeType : booleans) {
@@ -341,7 +645,7 @@
     }
 
     public void changeAnimTest(final boolean supportsChangeAnim, final boolean changeType,
-            final boolean hasStableIds, final boolean deleteSomeItems)  throws Throwable {
+            final boolean hasStableIds, final boolean deleteSomeItems) throws Throwable {
         final int changedIndex = 3;
         final int defaultType = 1;
         final AtomicInteger changedIndexNewType = new AtomicInteger(defaultType);
@@ -376,7 +680,7 @@
         };
         testAdapter.setHasStableIds(hasStableIds);
         setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
-        ((SimpleItemAnimator)mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
+        ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
                 supportsChangeAnim);
 
         final RecyclerView.ViewHolder toBeChangedVH =
@@ -435,7 +739,7 @@
         if (list1.size() != list2.size()) {
             return false;
         }
-        for (int i= 0; i < list1.size(); i++) {
+        for (int i = 0; i < list1.size(); i++) {
             if (!list1.get(i).equals(list2.get(i))) {
                 return false;
             }
@@ -444,8 +748,8 @@
     }
 
     private void testChangeWithPayload(final boolean supportsChangeAnim,
-            Object[][] notifyPayloads,  Object[][] expectedPayloadsInOnBind)
-                    throws Throwable {
+            Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)
+            throws Throwable {
         final List<Object> expectedPayloads = new ArrayList<Object>();
         final int changedIndex = 3;
         TestAdapter testAdapter = new TestAdapter(10) {
@@ -476,11 +780,11 @@
         };
         testAdapter.setHasStableIds(false);
         setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
-        ((SimpleItemAnimator)mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
+        ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
                 supportsChangeAnim);
 
         int numTests = notifyPayloads.length;
-        for (int i= 0; i < numTests; i++) {
+        for (int i = 0; i < numTests; i++) {
             mLayoutManager.expectLayouts(1);
             expectedPayloads.clear();
             for (int j = 0; j < expectedPayloadsInOnBind[i].length; j++) {
@@ -496,45 +800,46 @@
                 }
             });
             mLayoutManager.waitForLayout(2);
+            checkForMainThreadException();
         }
     }
 
-    public void testCrossFadingChangeAnimationWithPayload()  throws Throwable {
+    public void testCrossFadingChangeAnimationWithPayload() throws Throwable {
         // for crossfading change animation,  will receive EMPTY payload in onBindViewHolder
         testChangeWithPayload(true,
                 new Object[][]{
-                    new Object[]{"abc"},
-                    new Object[]{"abc", null, "cdf"},
-                    new Object[]{"abc", null},
-                    new Object[]{null, "abc"},
-                    new Object[]{"abc", "cdf"}
+                        new Object[]{"abc"},
+                        new Object[]{"abc", null, "cdf"},
+                        new Object[]{"abc", null},
+                        new Object[]{null, "abc"},
+                        new Object[]{"abc", "cdf"}
                 },
                 new Object[][]{
-                    new Object[0],
-                    new Object[0],
-                    new Object[0],
-                    new Object[0],
-                    new Object[0]
+                        new Object[0],
+                        new Object[0],
+                        new Object[0],
+                        new Object[0],
+                        new Object[0]
                 });
     }
 
-    public void testNoChangeAnimationWithPayload()  throws Throwable {
+    public void testNoChangeAnimationWithPayload() throws Throwable {
         // for Change Animation disabled, payload should match the payloads unless
         // null payload is fired.
         testChangeWithPayload(false,
                 new Object[][]{
-                    new Object[]{"abc"},
-                    new Object[]{"abc", null, "cdf"},
-                    new Object[]{"abc", null},
-                    new Object[]{null, "abc"},
-                    new Object[]{"abc", "cdf"}
+                        new Object[]{"abc"},
+                        new Object[]{"abc", null, "cdf"},
+                        new Object[]{"abc", null},
+                        new Object[]{null, "abc"},
+                        new Object[]{"abc", "cdf"}
                 },
                 new Object[][]{
-                new Object[]{"abc"},
-                new Object[0],
-                new Object[0],
-                new Object[0],
-                new Object[]{"abc", "cdf"}
+                        new Object[]{"abc"},
+                        new Object[0],
+                        new Object[0],
+                        new Object[0],
+                        new Object[]{"abc", "cdf"}
                 });
     }
 
@@ -570,7 +875,7 @@
         });
 
         // now keep adding children to trigger more children being created etc.
-        for (int i = 0; i < 100; i ++) {
+        for (int i = 0; i < 100; i++) {
             adapter.addAndNotify(15, 1);
             Thread.sleep(50);
         }
@@ -618,19 +923,19 @@
         adapter.setHasStableIds(true);
         initialSet.addAll(adapter.mItems);
         positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() {
-            @Override
-            void onRun(TestAdapter testAdapter) throws Throwable {
-                Item item5 = adapter.mItems.get(5);
-                Item item6 = adapter.mItems.get(6);
-                item5.mAdapterIndex = 6;
-                item6.mAdapterIndex = 5;
-                adapter.mItems.remove(5);
-                adapter.mItems.add(6, item5);
-                adapter.dispatchDataSetChanged();
-                //hacky, we support only 1 layout pass
-                mLayoutManager.layoutLatch.countDown();
-            }
-        }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
+                    @Override
+                    void onRun(TestAdapter testAdapter) throws Throwable {
+                        Item item5 = adapter.mItems.get(5);
+                        Item item6 = adapter.mItems.get(6);
+                        item5.mAdapterIndex = 6;
+                        item6.mAdapterIndex = 5;
+                        adapter.mItems.remove(5);
+                        adapter.mItems.add(6, item5);
+                        adapter.dispatchDataSetChanged();
+                        //hacky, we support only 1 layout pass
+                        mLayoutManager.layoutLatch.countDown();
+                    }
+                }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
                 PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8),
                 PositionConstraint.scrap(9, -1, 9));
         // now mix items.
@@ -682,7 +987,7 @@
                     itemViewTypeQueries.contains(i));
             if (adapter.hasStableIds()) {
                 assertTrue("getItemId for existing item " + i
-                        + " should be called when adapter has stable ids",
+                                + " should be called when adapter has stable ids",
                         itemIdQueries.contains(i));
             }
         }
@@ -1035,35 +1340,35 @@
 
     public void testAddDelete2() throws Throwable {
         positionStatesTest(5, 0, 5, new AdapterOps() {
-            // 0 1 2 3 4
-            // 0 1 2 a b 3 4
-            // 0 1 b 3 4
-            // pre: 0 1 2 3 4
-            // pre w/ adap: 0 1 2 b 3 4
-            @Override
-            void onRun(TestAdapter adapter) throws Throwable {
-                adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
-            }
-        }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
+                    // 0 1 2 3 4
+                    // 0 1 2 a b 3 4
+                    // 0 1 b 3 4
+                    // pre: 0 1 2 3 4
+                    // pre w/ adap: 0 1 2 b 3 4
+                    @Override
+                    void onRun(TestAdapter adapter) throws Throwable {
+                        adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
+                    }
+                }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
                 PositionConstraint.scrap(3, 3, 3)
         );
     }
 
     public void testAddDelete1() throws Throwable {
         positionStatesTest(5, 0, 5, new AdapterOps() {
-            // 0 1 2 3 4
-            // 0 1 2 a b 3 4
-            // 0 2 a b 3 4
-            // 0 c d 2 a b 3 4
-            // 0 c d 2 a 4
-            // c d 2 a 4
-            // pre: 0 1 2 3 4
-            @Override
-            void onRun(TestAdapter adapter) throws Throwable {
-                adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
-                        new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
-            }
-        }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
+                    // 0 1 2 3 4
+                    // 0 1 2 a b 3 4
+                    // 0 2 a b 3 4
+                    // 0 c d 2 a b 3 4
+                    // 0 c d 2 a 4
+                    // c d 2 a 4
+                    // pre: 0 1 2 3 4
+                    @Override
+                    void onRun(TestAdapter adapter) throws Throwable {
+                        adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
+                                new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
+                    }
+                }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
                 PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1),
                 PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0),
                 PositionConstraint.adapter(1), PositionConstraint.adapter(3)
@@ -1072,12 +1377,12 @@
 
     public void testAddSameIndexTwice() throws Throwable {
         positionStatesTest(12, 2, 7, new AdapterOps() {
-            @Override
-            void onRun(TestAdapter adapter) throws Throwable {
-                adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
-                        new int[]{11, 1});
-            }
-        }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
+                    @Override
+                    void onRun(TestAdapter adapter) throws Throwable {
+                        adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
+                                new int[]{11, 1});
+                    }
+                }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
                 PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7),
                 PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12),
                 PositionConstraint.scrap(8, 8, 13)
@@ -1086,12 +1391,12 @@
 
     public void testDeleteTwice() throws Throwable {
         positionStatesTest(12, 2, 7, new AdapterOps() {
-            @Override
-            void onRun(TestAdapter adapter) throws Throwable {
-                adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
-                        new int[]{0, 1});// delete item ids 0,2,9,1
-            }
-        }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
+                    @Override
+                    void onRun(TestAdapter adapter) throws Throwable {
+                        adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
+                                new int[]{0, 1});// delete item ids 0,2,9,1
+                    }
+                }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
                 PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2),
                 PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5),
                 PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7)
@@ -1103,10 +1408,11 @@
             int firstLayoutItemCount, AdapterOps adapterChanges,
             final PositionConstraint... constraints) throws Throwable {
         positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null,
-                adapterChanges,  constraints);
+                adapterChanges, constraints);
     }
+
     public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
-            int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges,
+            int firstLayoutItemCount, TestAdapter adapter, AdapterOps adapterChanges,
             final PositionConstraint... constraints) throws Throwable {
         setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter);
         mLayoutManager.expectLayouts(2);
@@ -1125,7 +1431,8 @@
                         = collectPositions(lm.mRecyclerView, recycler, state, ids);
                 StringBuilder positionLog = new StringBuilder("\nPosition logs:\n");
                 for (Map.Entry<Integer, CollectPositionResult> entry : positions.entrySet()) {
-                    positionLog.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
+                    positionLog.append(entry.getKey()).append(":").append(entry.getValue())
+                            .append("\n");
                 }
                 for (PositionConstraint constraint : constraints) {
                     if (constraint.mPreLayoutPos != -1) {
@@ -1170,7 +1477,8 @@
     public void testAddThenRecycleRemovedView() throws Throwable {
         setupBasic(10);
         final AtomicInteger step = new AtomicInteger(0);
-        final List<RecyclerView.ViewHolder> animateRemoveList = new ArrayList<RecyclerView.ViewHolder>();
+        final List<RecyclerView.ViewHolder> animateRemoveList
+                = new ArrayList<RecyclerView.ViewHolder>();
         DefaultItemAnimator animator = new DefaultItemAnimator() {
             @Override
             public boolean animateRemove(RecyclerView.ViewHolder holder) {