am 7356285f: Merge "Full animation support for LinearLayoutManager" into klp-modular-dev

* commit '7356285ff2081f14d83997aa0498c0874326ad72':
  Full animation support for LinearLayoutManager
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index 51a448a..2e3b739 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -26,6 +26,8 @@
 import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
+import java.util.List;
+
 /**
  * A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation which provides
  * similar functionality to {@link android.widget.ListView}.
@@ -366,13 +368,16 @@
         // 3) fill towards end, stacking from top
         // 4) scroll to fulfill requirements like stack from bottom.
         // create render state
-
+        if (DEBUG) {
+            Log.d(TAG, "is pre layout:" + state.isPreLayout());
+        }
         if (mPendingSavedState != null) {
             setOrientation(mPendingSavedState.mOrientation);
             setReverseLayout(mPendingSavedState.mReverseLayout);
             setStackFromEnd(mPendingSavedState.mStackFromEnd);
             mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
         }
+
         ensureRenderState();
         // resolve layout direction
         resolveShouldLayoutReverse();
@@ -495,7 +500,7 @@
             mRenderState.mCurrentPosition += mRenderState.mItemDirection;
         }
         fill(recycler, mRenderState, state, false);
-
+        int startOffset = mRenderState.mOffset;
         // fill towards end
         updateRenderStateToFillEnd(anchorItemPosition, anchorCoordinate);
         mRenderState.mExtra = extraForEnd;
@@ -503,69 +508,135 @@
             mRenderState.mCurrentPosition += mRenderState.mItemDirection;
         }
         fill(recycler, mRenderState, state, false);
-
+        int endOffset = mRenderState.mOffset;
         // changes may cause gaps on the UI, try to fix them.
         if (getChildCount() > 0) {
             // because layout from end may be changed by scroll to position
             // we re-calculate it.
             // find which side we should check for gaps.
             if (mShouldReverseLayout ^ mStackFromEnd) {
-                fixLayoutEndGap(recycler, state, true);
-                fixLayoutStartGap(recycler, state, false);
+                int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
+                startOffset += fixOffset;
+                endOffset += fixOffset;
+                fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
+                startOffset += fixOffset;
+                endOffset += fixOffset;
             } else {
-                fixLayoutStartGap(recycler, state, true);
-                fixLayoutEndGap(recycler, state, false);
+                int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
+                startOffset += fixOffset;
+                endOffset += fixOffset;
+                fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
+                startOffset += fixOffset;
+                endOffset += fixOffset;
             }
         }
 
+        // If there are scrap children that we did not layout, we need to find where they did go
+        // and layout them accordingly so that animations can work as expected.
+        // This case may happen if new views are added or an existing view expands and pushes
+        // another view out of bounds.
+        if (getChildCount() > 0 && !state.isPreLayout() && supportsItemAnimations()) {
+            // to make the logic simpler, we calculate the size of children and call fill.
+            int scrapExtraStart = 0, scrapExtraEnd = 0;
+            final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
+            final int scrapSize = scrapList.size();
+            final int firstChildPos = getPosition(getChildAt(0));
+            for (int i = 0; i < scrapSize; i++) {
+                RecyclerView.ViewHolder scrap = scrapList.get(i);
+                final int position = scrap.getPosition();
+                final int direction = position < firstChildPos != mShouldReverseLayout
+                        ? RenderState.LAYOUT_START : RenderState.LAYOUT_END;
+                if (direction == RenderState.LAYOUT_START) {
+                    scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
+                } else {
+                    scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
+                }
+            }
+
+            if (DEBUG) {
+                Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart
+                        + " towards start and " + scrapExtraEnd + " towards end");
+            }
+            mRenderState.mScrapList = scrapList;
+            if (scrapExtraStart > 0) {
+                View anchor = getChildClosestToStart();
+                updateRenderStateToFillStart(getPosition(anchor), startOffset);
+                mRenderState.mExtra = scrapExtraStart;
+                mRenderState.mAvailable = 0;
+                mRenderState.mCurrentPosition += mShouldReverseLayout ? 1 : -1;
+                fill(recycler, mRenderState, state, false);
+            }
+
+            if (scrapExtraEnd > 0) {
+                View anchor = getChildClosestToEnd();
+                updateRenderStateToFillEnd(getPosition(anchor),
+                        endOffset);
+                mRenderState.mExtra = scrapExtraEnd;
+                mRenderState.mAvailable = 0;
+                mRenderState.mCurrentPosition += mShouldReverseLayout ? -1 : 1;
+                fill(recycler, mRenderState, state, false);
+            }
+            mRenderState.mScrapList = null;
+        }
+
         mPendingScrollPosition = RecyclerView.NO_POSITION;
         mPendingScrollPositionOffset = INVALID_OFFSET;
         mLastStackFromEnd = mStackFromEnd;
         mPendingSavedState = null; // we don't need this anymore
+
         if (DEBUG) {
             validateChildOrder();
         }
     }
 
-    private void fixLayoutEndGap(RecyclerView.Recycler recycler, RecyclerView.State state,
-            boolean canOffsetChildren) {
-        View endChild = getChildClosestToEnd();
-        int gap = mOrientationHelper.getEndAfterPadding()
-                - mOrientationHelper.getDecoratedEnd(endChild);
+    /**
+     * @return The final offset amount for children
+     */
+    private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler,
+            RecyclerView.State state, boolean canOffsetChildren) {
+        int gap = mOrientationHelper.getEndAfterPadding() - endOffset;
+        int fixOffset = 0;
         if (gap > 0) {
-            scrollBy(-gap, recycler, state);
+            fixOffset = -scrollBy(-gap, recycler, state);
         } else {
-            return; // nothing to fix
+            return 0; // nothing to fix
         }
+        // move offset according to scroll amount
+        endOffset += fixOffset;
         if (canOffsetChildren) {
             // re-calculate gap, see if we could fix it
-            gap = mOrientationHelper.getEndAfterPadding()
-                    - mOrientationHelper.getDecoratedEnd(endChild);
+            gap = mOrientationHelper.getEndAfterPadding() - endOffset;
             if (gap > 0) {
                 mOrientationHelper.offsetChildren(gap);
+                return gap + fixOffset;
             }
         }
+        return fixOffset;
     }
 
-    private void fixLayoutStartGap(RecyclerView.Recycler recycler, RecyclerView.State state,
-            boolean canOffsetChildren) {
-        View startChild = getChildClosestToStart();
-        int gap = mOrientationHelper.getDecoratedStart(startChild) -
-                mOrientationHelper.getStartAfterPadding();
+    /**
+     * @return The final offset amount for children
+     */
+    private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler,
+            RecyclerView.State state, boolean canOffsetChildren) {
+        int gap = startOffset - mOrientationHelper.getStartAfterPadding();
+        int fixOffset = 0;
         if (gap > 0) {
             // check if we should fix this gap.
-            scrollBy(gap, recycler, state);
+            fixOffset = -scrollBy(gap, recycler, state);
         } else {
-            return; // nothing to fix
+            return 0; // nothing to fix
         }
+        startOffset += fixOffset;
         if (canOffsetChildren) {
             // re-calculate gap, see if we could fix it
-            gap = mOrientationHelper.getDecoratedStart(startChild) -
-                    mOrientationHelper.getStartAfterPadding();
+            gap = startOffset - mOrientationHelper.getStartAfterPadding();
             if (gap > 0) {
                 mOrientationHelper.offsetChildren(-gap);
+                return fixOffset - gap;
             }
         }
+        return fixOffset;
     }
 
     private void updateRenderStateToFillEnd(int itemPosition, int offset) {
@@ -912,18 +983,19 @@
         int remainingSpace = renderState.mAvailable + renderState.mExtra;
         while (remainingSpace > 0 && renderState.hasMore(state)) {
             View view = renderState.next(recycler);
-            if (mShouldReverseLayout) {
-                if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
+            if (view == null) {
+                // if we are laying out views in scrap, this may return null which means there is
+                // no more items to layout.
+                break;
+            }
+            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
+            if (!params.isItemRemoved() && mRenderState.mScrapList == null) {
+                if (mShouldReverseLayout == (renderState.mLayoutDirection
+                        == RenderState.LAYOUT_START)) {
                     addView(view);
                 } else {
                     addView(view, 0);
                 }
-            } else {
-                if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
-                    addView(view, 0);
-                } else {
-                    addView(view);
-                }
             }
             measureChildWithMargins(view, 0, 0);
             int consumed = mOrientationHelper.getDecoratedMeasurement(view);
@@ -957,7 +1029,6 @@
             }
             // We calculate everything with View's bounding box (which includes decor and margins)
             // To calculate correct layout position, we subtract margins.
-            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
             layoutDecorated(view, left + params.leftMargin, top + params.topMargin
                     , right - params.rightMargin, bottom - params.bottomMargin);
             if (DEBUG) {
@@ -966,9 +1037,12 @@
                         + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
             }
             renderState.mOffset += consumed * renderState.mLayoutDirection;
-            renderState.mAvailable -= consumed;
-            // we keep a separate remaining space because mAvailable is important for recycling
-            remainingSpace -= consumed;
+
+            if (!params.isItemRemoved()) {
+                renderState.mAvailable -= consumed;
+                // we keep a separate remaining space because mAvailable is important for recycling
+                remainingSpace -= consumed;
+            }
 
             if (renderState.mScrollingOffset != RenderState.SCOLLING_OFFSET_NaN) {
                 renderState.mScrollingOffset += consumed;
@@ -1216,6 +1290,12 @@
         int mExtra = 0;
 
         /**
+         * When LLM needs to layout particular views, it sets this list in which case, RenderState
+         * will only return views from this list and return null if it cannot find an item.
+         */
+        List<RecyclerView.ViewHolder> mScrapList = null;
+
+        /**
          * @return true if there are more items in the data adapter
          */
         boolean hasMore(RecyclerView.State state) {
@@ -1229,11 +1309,49 @@
          * @return The next element that we should render.
          */
         View next(RecyclerView.Recycler recycler) {
+            if (mScrapList != null) {
+                return nextFromLimitedList();
+            }
             final View view = recycler.getViewForPosition(mCurrentPosition);
             mCurrentPosition += mItemDirection;
             return view;
         }
 
+        /**
+         * Returns next item from limited list.
+         * <p>
+         * Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection
+         *
+         * @return View if an item in the current position or direction exists if not null.
+         */
+        private View nextFromLimitedList() {
+            int size = mScrapList.size();
+            RecyclerView.ViewHolder closest = null;
+            int closestDistance = Integer.MAX_VALUE;
+            for (int i = 0; i < size; i++) {
+                RecyclerView.ViewHolder viewHolder = mScrapList.get(i);
+                final int distance = (viewHolder.getPosition() - mCurrentPosition) * mItemDirection;
+                if (distance < 0) {
+                    continue; // item is not in current direction
+                }
+                if (distance < closestDistance) {
+                    closest = viewHolder;
+                    closestDistance = distance;
+                    if (distance == 0) {
+                        break;
+                    }
+                }
+            }
+            if (DEBUG) {
+                Log.d(TAG, "layout from scrap. found view:?" + (closest != null));
+            }
+            if (closest != null) {
+                mCurrentPosition = closest.getPosition() + mItemDirection;
+                return closest.itemView;
+            }
+            return null;
+        }
+
         void log() {
             Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" +
                     mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection);
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 492be71..56b8b79 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -32,10 +32,60 @@
 
     private static final String TAG = "RecyclerViewTest";
 
-    private static final boolean DEBUG = true;
+    private boolean mDebug;
+
+    protected RecyclerView mRecyclerView;
 
     public BaseRecyclerViewInstrumentationTest() {
+        this(false);
+    }
+
+    public BaseRecyclerViewInstrumentationTest(boolean debug) {
         super("android.support.v7.widget", TestActivity.class);
+        mDebug = debug;
+    }
+
+    public void removeRecyclerView() throws Throwable {
+        mRecyclerView = null;
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().mContainer.removeAllViews();
+            }
+        });
+    }
+
+    public void setRecyclerView(final RecyclerView recyclerView) throws Throwable {
+        mRecyclerView = recyclerView;
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().mContainer.addView(recyclerView);
+            }
+        });
+    }
+
+    public void requestLayoutOnUIThread(final View view) throws Throwable {
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                view.requestLayout();
+            }
+        });
+    }
+
+    public void scrollBy(final int dt) throws Throwable {
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
+                    mRecyclerView.scrollBy(dt, 0);
+                } else {
+                    mRecyclerView.scrollBy(0, dt);
+                }
+
+            }
+        });
     }
 
     class TestViewHolder extends RecyclerView.ViewHolder {
@@ -55,12 +105,26 @@
             layoutLatch = new CountDownLatch(count);
         }
 
-        public void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
-            layoutLatch.await(timeout, timeUnit);
+        public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable {
+            layoutLatch.await(timeout * (mDebug ? 100 : 1), timeUnit);
             assertEquals("all expected layouts should be executed at the expected time",
                     0, layoutLatch.getCount());
         }
 
+        public void assertLayoutCount(int count, String msg, long timeout) throws Throwable {
+            layoutLatch.await(timeout, TimeUnit.SECONDS);
+            assertEquals(msg, count, layoutLatch.getCount());
+        }
+
+        public void assertNoLayout(String msg, long timeout) throws Throwable {
+            layoutLatch.await(timeout, TimeUnit.SECONDS);
+            assertFalse(msg, layoutLatch.getCount() == 0);
+        }
+
+        public void waitForLayout(long timeout) throws Throwable {
+            waitForLayout(timeout, TimeUnit.SECONDS);
+        }
+
         @Override
         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
@@ -74,7 +138,7 @@
                 View view = getChildAt(i);
                 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
                 Item item = ((TestViewHolder) lp.mViewHolder).mBindedItem;
-                if (DEBUG) {
+                if (mDebug) {
                     Log.d(TAG, "testing item " + i);
                 }
                 assertSame("item position in LP should match adapter value",
@@ -82,18 +146,26 @@
             }
         }
 
+        RecyclerView.LayoutParams getLp(View v) {
+            return (RecyclerView.LayoutParams) v.getLayoutParams();
+        }
+
         void layoutRange(RecyclerView.Recycler recycler, int start,
                 int end) {
-            if (DEBUG) {
+            if (mDebug) {
                 Log.d(TAG, "will layout items from " + start + " to " + end);
             }
             for (int i = start; i < end; i++) {
-                if (DEBUG) {
+                if (mDebug) {
                     Log.d(TAG, "laying out item " + i);
                 }
                 View view = recycler.getViewForPosition(i);
-                assertNotNull("view should not be null for valid position", view);
-                addView(view);
+                assertNotNull("view should not be null for valid position. "
+                        + "got null view at position " + i, view);
+                if (!getLp(view).isItemRemoved()) {
+                    addView(view);
+                }
+
                 measureChildWithMargins(view, 0, 0);
                 layoutDecorated(view, 0, (i - start) * 10, getDecoratedMeasuredWidth(view)
                         , getDecoratedMeasuredHeight(view));
@@ -137,34 +209,57 @@
             holder.mBindedItem = item;
         }
 
-        public void deleteRangeAndNotify(final int start, final int end) throws Throwable {
+        public void deleteAndNotify(final int start, final int count) throws Throwable {
+            deleteAndNotify(new int[]{start, count});
+        }
+
+        /**
+         * Deletes items in the given ranges.
+         * <p>
+         * Note that each operation affects the one after so you should offset them properly.
+         * <p>
+         * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with
+         * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be
+         * A D E. Then it will delete 2,1 which means it will delete E.
+         */
+        public void deleteAndNotify(final int[]... startCountTuples) throws Throwable {
             runTestOnUiThread(new Runnable() {
                 @Override
                 public void run() {
-                    for (int i = start; i < end; i++) {
-                        mItems.remove(start);
+                    for (int t = 0; t < startCountTuples.length; t++) {
+                        int[] tuple = startCountTuples[t];
+                        for (int i = 0; i < tuple[1]; i++) {
+                            mItems.remove(tuple[0]);
+                        }
+                        notifyItemRangeRemoved(tuple[0], tuple[1]);
                     }
-                    notifyItemRangeRemoved(start, end - start);
+
                 }
             });
         }
 
-        public void addRangeAndNotify(final int start, final int end) throws Throwable {
+        public void addAndNotify(final int start, final int count) throws Throwable {
+            addAndNotify(new int[]{start, count});
+        }
+
+        public void addAndNotify(final int[]... startCountTuples) throws Throwable {
             runTestOnUiThread(new Runnable() {
                 @Override
                 public void run() {
-                    final int count = end - start;
-                    for (int i = start; i < end; i++) {
-                        mItems.add(start, new Item(i, "new item " + i));
+                    for (int t = 0; t < startCountTuples.length; t++) {
+                        int[] tuple = startCountTuples[t];
+                        for (int i = 0; i < tuple[1]; i++) {
+                            mItems.add(tuple[0], new Item(i, "new item " + i));
+                        }
+                        // offset others
+                        for (int i = tuple[0] + tuple[1]; i < mItems.size(); i++) {
+                            mItems.get(i).originalIndex += tuple[1];
+                        }
+                        notifyItemRangeInserted(tuple[0], tuple[1]);
                     }
-                    // offset others
-                    for (int i = end; i < mItems.size(); i++) {
-                        mItems.get(i).originalIndex += count;
-                    }
-                    notifyItemRangeInserted(start, count);
+
                 }
             });
-
         }
 
         @Override
@@ -172,5 +267,4 @@
             return mItems.size();
         }
     }
-}
-
+}
\ 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 b267487..7d43610 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -16,47 +16,326 @@
 
 package android.support.v7.widget;
 
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
 import java.util.concurrent.TimeUnit;
 
 public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest {
 
-    public void testBasicLayout() throws Throwable {
-        final RecyclerView recyclerView = new RecyclerView(getActivity());
-        final int itemCount = 10;
-        final TestAdapter testAdapter = new TestAdapter(itemCount);
-        recyclerView.setAdapter(testAdapter);
-        recyclerView.setItemAnimator(null);
-        TestLayoutManager layoutManager = new TestLayoutManager() {
-            @Override
-            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
-                try {
-                    detachAndScrapAttachedViews(recycler);
+    private static final boolean DEBUG = false;
 
-                    layoutRange(recycler, 0, state.getItemCount());
-                    assertEquals("correct # of children should be rendered",
-                            state.getItemCount(), getChildCount());
-                    assertVisibleItemPositions();
-                    if (getRecyclerView().getItemAnimator() == null) {
-                        removeAndRecycleScrap(recycler);
-                    }
-                } finally {
-                    layoutLatch.countDown();
-                }
+    private static final String TAG = "RecyclerViewAnimationsTest";
 
-            }
-        };
-        layoutManager.expectLayouts(1);
-        recyclerView.setLayoutManager(layoutManager);
-        runTestOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                getActivity().mContainer.addView(recyclerView);
-            }
-        });
-        layoutManager.waitForLayout(1, TimeUnit.SECONDS);
-        layoutManager.expectLayouts(1);
-        testAdapter.deleteRangeAndNotify(0, 7);
-        layoutManager.waitForLayout(1, TimeUnit.SECONDS);
+    Throwable mainThreadException;
+
+    AnimationLayoutManager mLayoutManager;
+
+    TestAdapter mTestAdapter;
+
+    public RecyclerViewAnimationsTest() {
+        super(DEBUG);
     }
 
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+    }
+
+    void checkForMainThreadException() throws Throwable {
+        if (mainThreadException != null) {
+            throw mainThreadException;
+        }
+    }
+
+    RecyclerView setupBasic(int itemCount) throws Throwable {
+        return setupBasic(itemCount, 0, itemCount);
+    }
+
+    RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount)
+            throws Throwable {
+        final RecyclerView recyclerView = new TestRecyclerView(getActivity());
+        recyclerView.setHasFixedSize(true);
+        mTestAdapter = new TestAdapter(itemCount);
+        recyclerView.setAdapter(mTestAdapter);
+        mLayoutManager = new AnimationLayoutManager();
+        recyclerView.setLayoutManager(mLayoutManager);
+        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex;
+        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount;
+
+        mLayoutManager.expectLayouts(1);
+        setRecyclerView(recyclerView);
+        mLayoutManager.waitForLayout(2);
+        return recyclerView;
+    }
+
+    public void testAddInvisibleAndVisible() throws Throwable {
+        setupBasic(10, 1, 7);
+        mLayoutManager.expectLayouts(2);
+        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
+        mTestAdapter.addAndNotify(0, 1);// add a new item 0 // invisible
+        mTestAdapter.addAndNotify(7, 1);// add a new item after 5th (old 5, new 6)
+        mLayoutManager.waitForLayout(2);
+    }
+
+    public void testAddInvisible() throws Throwable {
+        setupBasic(10, 1, 7);
+        mLayoutManager.expectLayouts(1);
+        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
+        mTestAdapter.addAndNotify(0, 1);// add a new item 0
+        mTestAdapter.addAndNotify(8, 1);// add a new item after 6th (old 6, new 7)
+        mLayoutManager.waitForLayout(2);
+    }
+
+    public void testBasicAdd() throws Throwable {
+        setupBasic(10);
+        mLayoutManager.expectLayouts(2);
+        setExpectedItemCounts(10, 13);
+        mTestAdapter.addAndNotify(2, 3);
+        mLayoutManager.waitForLayout(2);
+    }
+
+    public void testDeleteVisibleAndInvisible() throws Throwable {
+        setupBasic(11, 3, 5); //layout items  3 4 5 6 7
+        mLayoutManager.expectLayouts(2);
+        setLayoutRange(3, 6); //layout previously invisible child 10 from end of the list
+        setExpectedItemCounts(9, 8);
+        mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9
+        mLayoutManager.waitForLayout(2);
+    }
+
+    private void setLayoutRange(int start, int count) {
+        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
+        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
+    }
+
+    private void setExpectedItemCounts(int preLayout, int postLayout) {
+        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
+    }
+
+    public void testDeleteInvisible() throws Throwable {
+        setupBasic(10, 1, 7);
+        mLayoutManager.expectLayouts(1);
+        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8);
+        mTestAdapter.deleteAndNotify(0, 1);// delete item id 0
+        mTestAdapter.deleteAndNotify(7, 1);// delete item id 8
+        mLayoutManager.waitForLayout(2);
+    }
+
+    public void testBasicDelete() throws Throwable {
+        setupBasic(10);
+        final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() {
+            @Override
+            public void postDispatchLayout() {
+                // verify this only in first layout
+                assertEquals("deleted views should still be children of RV",
+                        mLayoutManager.getChildCount() + mDeletedViewCount
+                        , mRecyclerView.getChildCount());
+            }
+
+            @Override
+            void afterPreLayout(RecyclerView.Recycler recycler,
+                    AnimationLayoutManager layoutManager,
+                    RecyclerView.State state) {
+                super.afterPreLayout(recycler, layoutManager, state);
+                mLayoutItemCount = 3;
+                mLayoutMin = 0;
+            }
+        };
+        callbacks.mLayoutItemCount = 10;
+        callbacks.setExpectedItemCounts(10, 3);
+        mLayoutManager.setOnLayoutCallbacks(callbacks);
+
+        mLayoutManager.expectLayouts(2);
+        mTestAdapter.deleteAndNotify(0, 7);
+        mLayoutManager.waitForLayout(2);
+        callbacks.reset();// when animations end another layout will happen
+    }
+
+
+    class AnimationLayoutManager extends TestLayoutManager {
+
+        OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() {
+        };
+
+        @Override
+        public boolean supportsItemAnimations() {
+            return true;
+        }
+
+        @Override
+        public void expectLayouts(int count) {
+            super.expectLayouts(count);
+            mOnLayoutCallbacks.mLayoutCount = 0;
+        }
+
+        public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) {
+            mOnLayoutCallbacks = onLayoutCallbacks;
+        }
+
+        @Override
+        public final void onLayoutChildren(RecyclerView.Recycler recycler,
+                RecyclerView.State state) {
+            try {
+                mOnLayoutCallbacks.onLayoutChildren(recycler, this, state);
+            } finally {
+                layoutLatch.countDown();
+            }
+        }
+
+
+        public void onPostDispatchLayout() {
+            mOnLayoutCallbacks.postDispatchLayout();
+        }
+
+        @Override
+        public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable {
+            super.waitForLayout(timeout, timeUnit);
+            checkForMainThreadException();
+        }
+    }
+
+    abstract class OnLayoutCallbacks {
+
+        int mLayoutMin = Integer.MIN_VALUE;
+
+        int mLayoutItemCount = Integer.MAX_VALUE;
+
+        int expectedPreLayoutItemCount = -1;
+
+        int expectedPostLayoutItemCount = -1;
+
+        private int mLayoutCount;
+
+        int mDeletedViewCount;
+
+        void setExpectedItemCounts(int preLayout, int postLayout) {
+            expectedPreLayoutItemCount = preLayout;
+            expectedPostLayoutItemCount = postLayout;
+        }
+
+        void reset() {
+            mLayoutCount = 0;
+            mLayoutMin = Integer.MIN_VALUE;
+            mLayoutItemCount = Integer.MAX_VALUE;
+            expectedPreLayoutItemCount = -1;
+            expectedPostLayoutItemCount = -1;
+        }
+
+        void beforePreLayout(RecyclerView.Recycler recycler,
+                AnimationLayoutManager lm, RecyclerView.State state) {
+            mDeletedViewCount = 0;
+            for (int i = 0; i < lm.getChildCount(); i++) {
+                View v = lm.getChildAt(i);
+                if (lm.getLp(v).isItemRemoved()) {
+                    mDeletedViewCount++;
+                }
+            }
+        }
+
+        void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
+                RecyclerView.State state) {
+            if (DEBUG) {
+                Log.d(TAG, "item count " + state.getItemCount());
+            }
+            lm.detachAndScrapAttachedViews(recycler);
+            final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin;
+            final int count = mLayoutItemCount
+                    == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount;
+            lm.layoutRange(recycler, start, start + count);
+            assertEquals("correct # of children should be laid out",
+                    count - (inPreLayout() ? mDeletedViewCount : 0), lm.getChildCount());
+            if (!inPreLayout()) { // may not be the correct check
+                lm.assertVisibleItemPositions();
+            }
+        }
+
+        void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
+                RecyclerView.State state) {
+
+            if (mLayoutCount == 0) {
+                if (expectedPreLayoutItemCount != -1) {
+                    assertEquals("on pre layout, state should return abstracted adapter size",
+                            expectedPreLayoutItemCount, state.getItemCount());
+                }
+                beforePreLayout(recycler, lm, state);
+            } else if (mLayoutCount == 1) {
+                if (expectedPostLayoutItemCount != -1) {
+                    assertEquals("on post layout, state should return real adapter size",
+                            expectedPostLayoutItemCount, state.getItemCount());
+                }
+                beforePostLayout(recycler, lm, state);
+            }
+            doLayout(recycler, lm, state);
+            if (mLayoutCount == 0) {
+                afterPreLayout(recycler, lm, state);
+            } else if (mLayoutCount == 1) {
+                afterPostLayout(recycler, lm, state);
+            }
+            mLayoutCount++;
+        }
+
+        void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
+                RecyclerView.State state) {
+        }
+
+        void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
+                RecyclerView.State state) {
+        }
+
+        void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
+                RecyclerView.State state) {
+        }
+
+        void postDispatchLayout() {
+        }
+
+        boolean inPreLayout() {
+            return mLayoutCount == 0;
+        }
+    }
+
+    class TestRecyclerView extends RecyclerView {
+
+        public TestRecyclerView(Context context) {
+            super(context);
+        }
+
+        public TestRecyclerView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) {
+            super(context, attrs, defStyle);
+        }
+
+        @Override
+        void dispatchLayout() {
+            try {
+                super.dispatchLayout();
+                if (getLayoutManager() instanceof AnimationLayoutManager) {
+                    ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout();
+                }
+            } catch (Throwable t) {
+                postExceptionToInstrumentation(t);
+            }
+
+        }
+
+        private void postExceptionToInstrumentation(Throwable t) {
+            if (DEBUG) {
+                Log.e(TAG, "captured exception on main thread", t);
+            }
+            mainThreadException = t;
+            if (mLayoutManager instanceof TestLayoutManager) {
+                TestLayoutManager lm = mLayoutManager;
+                // finish all layouts so that we get the correct exception
+                while (lm.layoutLatch.getCount() > 0) {
+                    lm.layoutLatch.countDown();
+                }
+            }
+        }
+    }
 }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
index c3243a3..d218f45 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
@@ -315,6 +315,4 @@
             super(itemView);
         }
     }
-
-}
-
+}
\ No newline at end of file
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 4be8bb4..2d076ce 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -66,7 +66,7 @@
         assertEquals("in second layout,structure changed should be false", false,
                 structureChanged.get());
         testLayoutManager.expectLayouts(recyclerView.getItemAnimator() == null ? 1 : 2); //
-        adapter.deleteRangeAndNotify(3, 5);
+        adapter.deleteAndNotify(3, 2);
         testLayoutManager.waitForLayout(3, TimeUnit.SECONDS);
         assertEquals("when items are removed, item count in state should be updated",
                 adapter.getItemCount(),
@@ -74,7 +74,7 @@
         assertEquals("structure changed should be true when items are removed", true,
                 structureChanged.get());
         testLayoutManager.expectLayouts(recyclerView.getItemAnimator() == null ? 1 : 2);
-        adapter.addRangeAndNotify(2, 7);
+        adapter.addAndNotify(2, 5);
         testLayoutManager.waitForLayout(3, TimeUnit.SECONDS);
 
         assertEquals("when items are added, item count in state should be updated",
@@ -85,4 +85,4 @@
 
     }
 
-}
+}
\ No newline at end of file