[go: nahoru, domu]

1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.widget;
18
19import org.hamcrest.CoreMatchers;
20import org.hamcrest.MatcherAssert;
21import org.junit.Test;
22import org.junit.runner.RunWith;
23
24import android.graphics.Rect;
25import android.support.annotation.NonNull;
26import android.support.test.runner.AndroidJUnit4;
27import android.support.v4.view.ViewCompat;
28import android.test.suitebuilder.annotation.MediumTest;
29import android.util.Log;
30import android.view.View;
31import android.view.ViewGroup;
32
33import java.util.ArrayList;
34import java.util.HashMap;
35import java.util.HashSet;
36import java.util.List;
37import java.util.Map;
38import java.util.Set;
39import java.util.concurrent.atomic.AtomicBoolean;
40import java.util.concurrent.atomic.AtomicInteger;
41import static org.junit.Assert.*;
42
43/**
44 * Tests for {@link SimpleItemAnimator} API.
45 */
46@MediumTest
47@RunWith(AndroidJUnit4.class)
48public class RecyclerViewAnimationsTest extends BaseRecyclerViewAnimationsTest {
49
50    final List<TestViewHolder> recycledVHs = new ArrayList<>();
51
52    @Test
53    public void keepFocusAfterChangeAnimation() throws Throwable {
54        setupBasic(10, 0, 5, new TestAdapter(10) {
55            @Override
56            public void onBindViewHolder(TestViewHolder holder,
57                    int position) {
58                super.onBindViewHolder(holder, position);
59                holder.itemView.setFocusableInTouchMode(true);
60            }
61        });
62        ((SimpleItemAnimator)(mRecyclerView.getItemAnimator())).setSupportsChangeAnimations(true);
63
64        final RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForAdapterPosition(3);
65        assertNotNull("test sanity", oldVh);
66        runTestOnUiThread(new Runnable() {
67            @Override
68            public void run() {
69                oldVh.itemView.requestFocus();
70            }
71        });
72        assertTrue("test sanity", oldVh.itemView.hasFocus());
73        mLayoutManager.expectLayouts(2);
74        mTestAdapter.changeAndNotify(3, 1);
75        mLayoutManager.waitForLayout(2);
76
77        RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(3);
78        assertNotNull("test sanity", newVh);
79        assertNotSame(oldVh, newVh);
80        assertFalse(oldVh.itemView.hasFocus());
81        assertTrue(newVh.itemView.hasFocus());
82    }
83
84    @Test
85    public void changeAndDisappearDontReUseViewHolder() throws Throwable {
86        changeAndDisappearTest(false, false);
87    }
88
89    @Test
90    public void changeAndDisappearReUseViewHolder() throws Throwable {
91        changeAndDisappearTest(true, false);
92    }
93
94    @Test
95    public void changeAndDisappearReUseWithScrapViewHolder() throws Throwable {
96        changeAndDisappearTest(true, true);
97    }
98
99    public void changeAndDisappearTest(final boolean reUse, final boolean useScrap)
100            throws Throwable {
101        final List<RecyclerView.ViewHolder> mRecycled = new ArrayList<>();
102        final TestAdapter adapter = new TestAdapter(1) {
103            @Override
104            public void onViewRecycled(TestViewHolder holder) {
105                super.onViewRecycled(holder);
106                mRecycled.add(holder);
107            }
108        };
109        setupBasic(1, 0, 1, adapter);
110        RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(mRecyclerView.getChildAt(0));
111        LoggingItemAnimator animator = new LoggingItemAnimator() {
112            @Override
113            public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
114                                                     @NonNull List<Object> payloads) {
115                return reUse;
116            }
117        };
118        mRecyclerView.setItemAnimator(animator);
119        mLayoutManager.expectLayouts(2);
120        final RecyclerView.ViewHolder[] updatedVH = new RecyclerView.ViewHolder[1];
121        runTestOnUiThread(new Runnable() {
122            @Override
123            public void run() {
124                adapter.notifyItemChanged(0);
125                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
126                    @Override
127                    void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
128                                  RecyclerView.State state) {
129                        if (state.isPreLayout()) {
130                            super.doLayout(recycler, lm, state);
131                        } else {
132                            lm.detachAndScrapAttachedViews(recycler);
133                            final View view;
134                            if (reUse && useScrap) {
135                                view = recycler.getScrapViewAt(0);
136                            } else {
137                                view = recycler.getViewForPosition(0);
138                            }
139                            updatedVH[0] = RecyclerView.getChildViewHolderInt(view);
140                            lm.addDisappearingView(view);
141                        }
142                    }
143                };
144            }
145        });
146        mLayoutManager.waitForLayout(2);
147
148        MatcherAssert.assertThat(animator.contains(vh, animator.mAnimateDisappearanceList),
149                CoreMatchers.is(reUse));
150        MatcherAssert.assertThat(animator.contains(vh, animator.mAnimateChangeList),
151                CoreMatchers.is(!reUse));
152        MatcherAssert.assertThat(animator.contains(updatedVH[0], animator.mAnimateChangeList),
153                CoreMatchers.is(!reUse));
154        MatcherAssert.assertThat(animator.contains(updatedVH[0],
155                animator.mAnimateDisappearanceList), CoreMatchers.is(reUse));
156        waitForAnimations(10);
157        MatcherAssert.assertThat(mRecyclerView.getChildCount(), CoreMatchers.is(0));
158        if (useScrap || !reUse) {
159            MatcherAssert.assertThat(mRecycled.contains(vh), CoreMatchers.is(true));
160        } else {
161            MatcherAssert.assertThat(mRecyclerView.mRecycler.mCachedViews.contains(vh),
162                    CoreMatchers.is(true));
163        }
164
165        if (!reUse) {
166            MatcherAssert.assertThat(mRecycled.contains(updatedVH[0]), CoreMatchers.is(false));
167            MatcherAssert.assertThat(mRecyclerView.mRecycler.mCachedViews.contains(updatedVH[0]),
168                    CoreMatchers.is(true));
169        }
170    }
171
172    @Test
173    public void detectStableIdError() throws Throwable {
174        setIgnoreMainThreadException(true);
175        final AtomicBoolean useBadIds = new AtomicBoolean(false);
176        TestAdapter adapter = new TestAdapter(10) {
177            @Override
178            public long getItemId(int position) {
179                if (useBadIds.get() && position == 5) {
180                    return super.getItemId(position) - 1;
181                }
182                return super.getItemId(position);
183            }
184
185            @Override
186            public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
187                // ignore validation
188            }
189        };
190        adapter.setHasStableIds(true);
191        setupBasic(10, 0, 10, adapter);
192        mLayoutManager.expectLayouts(2);
193        useBadIds.set(true);
194        adapter.changeAndNotify(4, 2);
195        mLayoutManager.waitForLayout(2);
196        assertTrue(getMainThreadException() instanceof IllegalStateException);
197        assertTrue(getMainThreadException().getMessage()
198                .contains("Two different ViewHolders have the same stable ID."));
199        // TODO don't use this after moving this class to Junit 4
200        try {
201            removeRecyclerView();
202        } catch (Throwable t){}
203    }
204
205
206    @Test
207    public void dontLayoutReusedViewWithoutPredictive() throws Throwable {
208        reuseHiddenViewTest(new ReuseTestCallback() {
209            @Override
210            public void postSetup(List<TestViewHolder> recycledList,
211                    final TestViewHolder target) throws Throwable {
212                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
213                        .getItemAnimator();
214                itemAnimator.reset();
215                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
216                    @Override
217                    void beforePreLayout(RecyclerView.Recycler recycler,
218                            AnimationLayoutManager lm, RecyclerView.State state) {
219                        fail("pre layout is not expected");
220                    }
221
222                    @Override
223                    void beforePostLayout(RecyclerView.Recycler recycler,
224                            AnimationLayoutManager layoutManager,
225                            RecyclerView.State state) {
226                        mLayoutItemCount = 7;
227                        View targetView = recycler
228                                .getViewForPosition(target.getAdapterPosition());
229                        assertSame(targetView, target.itemView);
230                        super.beforePostLayout(recycler, layoutManager, state);
231                    }
232
233                    @Override
234                    void afterPostLayout(RecyclerView.Recycler recycler,
235                            AnimationLayoutManager layoutManager,
236                            RecyclerView.State state) {
237                        super.afterPostLayout(recycler, layoutManager, state);
238                        assertNull("test sanity. this view should not be re-laid out in post "
239                                + "layout", target.itemView.getParent());
240                    }
241                };
242                mLayoutManager.expectLayouts(1);
243                mLayoutManager.requestSimpleAnimationsInNextLayout();
244                requestLayoutOnUIThread(mRecyclerView);
245                mLayoutManager.waitForLayout(2);
246                checkForMainThreadException();
247                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
248                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
249                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
250                // This is a LayoutManager problem if it asked for the view but didn't properly
251                // lay it out. It will move to disappearance
252                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
253                waitForAnimations(5);
254                assertTrue(recycledVHs.contains(target));
255            }
256        });
257    }
258
259    @Test
260    public void dontLayoutReusedViewWithPredictive() throws Throwable {
261        reuseHiddenViewTest(new ReuseTestCallback() {
262            @Override
263            public void postSetup(List<TestViewHolder> recycledList,
264                    final TestViewHolder target) throws Throwable {
265                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
266                        .getItemAnimator();
267                itemAnimator.reset();
268                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
269                    @Override
270                    void beforePreLayout(RecyclerView.Recycler recycler,
271                            AnimationLayoutManager lm, RecyclerView.State state) {
272                        mLayoutItemCount = 9;
273                        super.beforePreLayout(recycler, lm, state);
274                    }
275
276                    @Override
277                    void beforePostLayout(RecyclerView.Recycler recycler,
278                            AnimationLayoutManager layoutManager,
279                            RecyclerView.State state) {
280                        mLayoutItemCount = 7;
281                        super.beforePostLayout(recycler, layoutManager, state);
282                    }
283
284                    @Override
285                    void afterPostLayout(RecyclerView.Recycler recycler,
286                            AnimationLayoutManager layoutManager,
287                            RecyclerView.State state) {
288                        super.afterPostLayout(recycler, layoutManager, state);
289                        assertNull("test sanity. this view should not be re-laid out in post "
290                                + "layout", target.itemView.getParent());
291                    }
292                };
293                mLayoutManager.expectLayouts(2);
294                mTestAdapter.deleteAndNotify(1, 1);
295                mLayoutManager.waitForLayout(2);
296                checkForMainThreadException();
297                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
298                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
299                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
300                // This is a LayoutManager problem if it asked for the view but didn't properly
301                // lay it out. It will move to disappearance.
302                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
303                waitForAnimations(5);
304                assertTrue(recycledVHs.contains(target));
305            }
306        });
307    }
308
309    @Test
310    public void reuseHiddenViewWithoutPredictive() throws Throwable {
311        reuseHiddenViewTest(new ReuseTestCallback() {
312            @Override
313            public void postSetup(List<TestViewHolder> recycledList,
314                    TestViewHolder target) throws Throwable {
315                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
316                        .getItemAnimator();
317                itemAnimator.reset();
318                mLayoutManager.expectLayouts(1);
319                mLayoutManager.requestSimpleAnimationsInNextLayout();
320                mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
321                requestLayoutOnUIThread(mRecyclerView);
322                mLayoutManager.waitForLayout(2);
323                waitForAnimations(5);
324                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
325                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
326                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
327                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
328                assertFalse(recycledVHs.contains(target));
329            }
330        });
331    }
332
333    @Test
334    public void reuseHiddenViewWithoutAnimations() throws Throwable {
335        reuseHiddenViewTest(new ReuseTestCallback() {
336            @Override
337            public void postSetup(List<TestViewHolder> recycledList,
338                    TestViewHolder target) throws Throwable {
339                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
340                        .getItemAnimator();
341                itemAnimator.reset();
342                mLayoutManager.expectLayouts(1);
343                mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
344                requestLayoutOnUIThread(mRecyclerView);
345                mLayoutManager.waitForLayout(2);
346                waitForAnimations(5);
347                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
348                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
349                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
350                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
351                assertFalse(recycledVHs.contains(target));
352            }
353        });
354    }
355
356    @Test
357    public void reuseHiddenViewWithPredictive() throws Throwable {
358        reuseHiddenViewTest(new ReuseTestCallback() {
359            @Override
360            public void postSetup(List<TestViewHolder> recycledList,
361                    TestViewHolder target) throws Throwable {
362                // it should move to change scrap and then show up from there
363                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
364                        .getItemAnimator();
365                itemAnimator.reset();
366                mLayoutManager.expectLayouts(2);
367                mTestAdapter.deleteAndNotify(2, 1);
368                mLayoutManager.waitForLayout(2);
369                waitForAnimations(5);
370                // This LM does not layout the additional item so it does predictive wrong.
371                // We should still handle it and animate persistence for this item
372                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
373                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
374                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
375                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
376                assertTrue(itemAnimator.mMoveVHs.contains(target));
377                assertFalse(recycledVHs.contains(target));
378            }
379        });
380    }
381
382    @Test
383    public void reuseHiddenViewWithProperPredictive() throws Throwable {
384        reuseHiddenViewTest(new ReuseTestCallback() {
385            @Override
386            public void postSetup(List<TestViewHolder> recycledList,
387                    TestViewHolder target) throws Throwable {
388                // it should move to change scrap and then show up from there
389                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
390                        .getItemAnimator();
391                itemAnimator.reset();
392                mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
393                    @Override
394                    void beforePreLayout(RecyclerView.Recycler recycler,
395                            AnimationLayoutManager lm, RecyclerView.State state) {
396                        mLayoutItemCount = 9;
397                        super.beforePreLayout(recycler, lm, state);
398                    }
399
400                    @Override
401                    void afterPreLayout(RecyclerView.Recycler recycler,
402                            AnimationLayoutManager layoutManager,
403                            RecyclerView.State state) {
404                        mLayoutItemCount = 8;
405                        super.afterPreLayout(recycler, layoutManager, state);
406                    }
407                };
408
409                mLayoutManager.expectLayouts(2);
410                mTestAdapter.deleteAndNotify(2, 1);
411                mLayoutManager.waitForLayout(2);
412                waitForAnimations(5);
413                // This LM implements predictive animations properly by requesting target view
414                // in pre-layout.
415                assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
416                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
417                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
418                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
419                assertTrue(itemAnimator.mMoveVHs.contains(target));
420                assertFalse(recycledVHs.contains(target));
421            }
422        });
423    }
424
425    @Test
426    public void dontReuseHiddenViewOnInvalidate() throws Throwable {
427        reuseHiddenViewTest(new ReuseTestCallback() {
428            @Override
429            public void postSetup(List<TestViewHolder> recycledList,
430                    TestViewHolder target) throws Throwable {
431                // it should move to change scrap and then show up from there
432                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
433                        .getItemAnimator();
434                itemAnimator.reset();
435                mLayoutManager.expectLayouts(1);
436                mTestAdapter.dispatchDataSetChanged();
437                mLayoutManager.waitForLayout(2);
438                waitForAnimations(5);
439                assertFalse(mRecyclerView.getItemAnimator().isRunning());
440                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
441                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
442                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
443                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
444                assertTrue(recycledVHs.contains(target));
445            }
446        });
447    }
448
449    @Test
450    public void dontReuseOnTypeChange() throws Throwable {
451        reuseHiddenViewTest(new ReuseTestCallback() {
452            @Override
453            public void postSetup(List<TestViewHolder> recycledList,
454                    TestViewHolder target) throws Throwable {
455                // it should move to change scrap and then show up from there
456                LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
457                        .getItemAnimator();
458                itemAnimator.reset();
459                mLayoutManager.expectLayouts(1);
460                target.mBoundItem.mType += 2;
461                mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
462                mTestAdapter.changeAndNotify(target.getAdapterPosition(), 1);
463                requestLayoutOnUIThread(mRecyclerView);
464                mLayoutManager.waitForLayout(2);
465
466                assertTrue(itemAnimator.mChangeOldVHs.contains(target));
467                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
468                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
469                assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
470                assertTrue(mRecyclerView.mChildHelper.isHidden(target.itemView));
471                assertFalse(recycledVHs.contains(target));
472                waitForAnimations(5);
473                assertTrue(recycledVHs.contains(target));
474            }
475        });
476    }
477
478    interface ReuseTestCallback {
479
480        void postSetup(List<TestViewHolder> recycledList, TestViewHolder target) throws Throwable;
481    }
482
483    @Override
484    protected RecyclerView.ItemAnimator createItemAnimator() {
485        return new LoggingItemAnimator();
486    }
487
488    public void reuseHiddenViewTest(ReuseTestCallback callback) throws Throwable {
489        TestAdapter adapter = new TestAdapter(10) {
490            @Override
491            public void onViewRecycled(TestViewHolder holder) {
492                super.onViewRecycled(holder);
493                recycledVHs.add(holder);
494            }
495        };
496        setupBasic(10, 0, 10, adapter);
497        mRecyclerView.setItemViewCacheSize(0);
498        TestViewHolder target = (TestViewHolder) mRecyclerView.findViewHolderForAdapterPosition(9);
499        mRecyclerView.getItemAnimator().setAddDuration(1000);
500        mRecyclerView.getItemAnimator().setRemoveDuration(1000);
501        mRecyclerView.getItemAnimator().setChangeDuration(1000);
502        mRecyclerView.getItemAnimator().setMoveDuration(1000);
503        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
504        mLayoutManager.expectLayouts(2);
505        adapter.deleteAndNotify(2, 1);
506        mLayoutManager.waitForLayout(2);
507        // test sanity, make sure target is hidden now
508        assertTrue("test sanity", mRecyclerView.mChildHelper.isHidden(target.itemView));
509        callback.postSetup(recycledVHs, target);
510        // TODO TEST ITEM INVALIDATION OR TYPE CHANGE IN BETWEEN
511        // TODO TEST ITEM IS RECEIVED FROM RECYCLER BUT NOT RE-ADDED
512        // TODO TEST ITEM ANIMATOR IS CALLED TO GET NEW INFORMATION ABOUT LOCATION
513
514    }
515
516    @Test
517    public void detachBeforeAnimations() throws Throwable {
518        setupBasic(10, 0, 5);
519        final RecyclerView rv = mRecyclerView;
520        waitForAnimations(2);
521        final DefaultItemAnimator animator = new DefaultItemAnimator() {
522            @Override
523            public void runPendingAnimations() {
524                super.runPendingAnimations();
525            }
526        };
527        rv.setItemAnimator(animator);
528        mLayoutManager.expectLayouts(2);
529        mTestAdapter.deleteAndNotify(3, 4);
530        mLayoutManager.waitForLayout(2);
531        removeRecyclerView();
532        assertNull("test sanity check RV should be removed", rv.getParent());
533        assertEquals("no views should be hidden", 0, rv.mChildHelper.mHiddenViews.size());
534        assertFalse("there should not be any animations running", animator.isRunning());
535    }
536
537    @Test
538    public void moveDeleted() throws Throwable {
539        setupBasic(4, 0, 3);
540        waitForAnimations(2);
541        final View[] targetChild = new View[1];
542        final LoggingItemAnimator animator = new LoggingItemAnimator();
543        runTestOnUiThread(new Runnable() {
544            @Override
545            public void run() {
546                mRecyclerView.setItemAnimator(animator);
547                targetChild[0] = mRecyclerView.getChildAt(1);
548            }
549        });
550
551        assertNotNull("test sanity", targetChild);
552        mLayoutManager.expectLayouts(1);
553        runTestOnUiThread(new Runnable() {
554            @Override
555            public void run() {
556                mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
557                    @Override
558                    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
559                            RecyclerView.State state) {
560                        if (view == targetChild[0]) {
561                            outRect.set(10, 20, 30, 40);
562                        } else {
563                            outRect.set(0, 0, 0, 0);
564                        }
565                    }
566                });
567            }
568        });
569        mLayoutManager.waitForLayout(1);
570
571        // now delete that item.
572        mLayoutManager.expectLayouts(2);
573        RecyclerView.ViewHolder targetVH = mRecyclerView.getChildViewHolder(targetChild[0]);
574        targetChild[0] = null;
575        mTestAdapter.deleteAndNotify(1, 1);
576        mLayoutManager.waitForLayout(2);
577        assertFalse("if deleted view moves, it should not be in move animations",
578                animator.mMoveVHs.contains(targetVH));
579        assertEquals("only 1 item is deleted", 1, animator.mRemoveVHs.size());
580        assertTrue("the target view is removed", animator.mRemoveVHs.contains(targetVH
581        ));
582    }
583
584    private void runTestImportantForAccessibilityWhileDeteling(
585            final int boundImportantForAccessibility,
586            final int expectedImportantForAccessibility) throws Throwable {
587        // Adapter binding the item to the initial accessibility option.
588        // RecyclerView is expected to change it to 'expectedImportantForAccessibility'.
589        TestAdapter adapter = new TestAdapter(1) {
590            @Override
591            public void onBindViewHolder(TestViewHolder holder, int position) {
592                super.onBindViewHolder(holder, position);
593                ViewCompat.setImportantForAccessibility(
594                        holder.itemView, boundImportantForAccessibility);
595            }
596        };
597
598        // Set up with 1 item.
599        setupBasic(1, 0, 1, adapter);
600        waitForAnimations(2);
601        final View[] targetChild = new View[1];
602        final LoggingItemAnimator animator = new LoggingItemAnimator();
603        animator.setRemoveDuration(500);
604        runTestOnUiThread(new Runnable() {
605            @Override
606            public void run() {
607                mRecyclerView.setItemAnimator(animator);
608                targetChild[0] = mRecyclerView.getChildAt(0);
609                assertEquals(
610                        expectedImportantForAccessibility,
611                        ViewCompat.getImportantForAccessibility(targetChild[0]));
612            }
613        });
614
615        assertNotNull("test sanity", targetChild[0]);
616
617        // now delete that item.
618        mLayoutManager.expectLayouts(2);
619        mTestAdapter.deleteAndNotify(0, 1);
620
621        mLayoutManager.waitForLayout(2);
622
623        runTestOnUiThread(new Runnable() {
624            @Override
625            public void run() {
626                // The view is still a child of mRecyclerView, and is invisible for accessibility.
627                assertTrue(targetChild[0].getParent() == mRecyclerView);
628                assertEquals(
629                        ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
630                        ViewCompat.getImportantForAccessibility(targetChild[0]));
631            }
632        });
633
634        waitForAnimations(2);
635
636        // Delete animation is now complete.
637        runTestOnUiThread(new Runnable() {
638            @Override
639            public void run() {
640                // The view is in recycled state, and back to the expected accessibility.
641                assertTrue(targetChild[0].getParent() == null);
642                assertEquals(
643                        expectedImportantForAccessibility,
644                        ViewCompat.getImportantForAccessibility(targetChild[0]));
645            }
646        });
647
648        // Add 1 element, which should use same view.
649        mLayoutManager.expectLayouts(2);
650        mTestAdapter.addAndNotify(1);
651        mLayoutManager.waitForLayout(2);
652
653        runTestOnUiThread(new Runnable() {
654            @Override
655            public void run() {
656                // The view should be reused, and have the expected accessibility.
657                assertTrue(
658                        "the item must be reused", targetChild[0] == mRecyclerView.getChildAt(0));
659                assertEquals(
660                        expectedImportantForAccessibility,
661                        ViewCompat.getImportantForAccessibility(targetChild[0]));
662            }
663        });
664    }
665
666    @Test
667    public void importantForAccessibilityWhileDetelingAuto() throws Throwable {
668        runTestImportantForAccessibilityWhileDeteling(
669                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO,
670                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
671    }
672
673    @Test
674    public void importantForAccessibilityWhileDetelingNo() throws Throwable {
675        runTestImportantForAccessibilityWhileDeteling(
676                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO,
677                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
678    }
679
680    @Test
681    public void importantForAccessibilityWhileDetelingNoHideDescandants() throws Throwable {
682        runTestImportantForAccessibilityWhileDeteling(
683                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
684                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
685    }
686
687    @Test
688    public void importantForAccessibilityWhileDetelingYes() throws Throwable {
689        runTestImportantForAccessibilityWhileDeteling(
690                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
691                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
692    }
693
694    @Test
695    public void preLayoutPositionCleanup() throws Throwable {
696        setupBasic(4, 0, 4);
697        mLayoutManager.expectLayouts(2);
698        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
699            @Override
700            void beforePreLayout(RecyclerView.Recycler recycler,
701                    AnimationLayoutManager lm, RecyclerView.State state) {
702                mLayoutMin = 0;
703                mLayoutItemCount = 3;
704            }
705
706            @Override
707            void beforePostLayout(RecyclerView.Recycler recycler,
708                    AnimationLayoutManager layoutManager,
709                    RecyclerView.State state) {
710                mLayoutMin = 0;
711                mLayoutItemCount = 4;
712            }
713        };
714        mTestAdapter.addAndNotify(0, 1);
715        mLayoutManager.waitForLayout(2);
716
717
718    }
719
720    @Test
721    public void addRemoveSamePass() throws Throwable {
722        final List<RecyclerView.ViewHolder> mRecycledViews
723                = new ArrayList<RecyclerView.ViewHolder>();
724        TestAdapter adapter = new TestAdapter(50) {
725            @Override
726            public void onViewRecycled(TestViewHolder holder) {
727                super.onViewRecycled(holder);
728                mRecycledViews.add(holder);
729            }
730        };
731        adapter.setHasStableIds(true);
732        setupBasic(50, 3, 5, adapter);
733        mRecyclerView.setItemViewCacheSize(0);
734        final ArrayList<RecyclerView.ViewHolder> addVH
735                = new ArrayList<RecyclerView.ViewHolder>();
736        final ArrayList<RecyclerView.ViewHolder> removeVH
737                = new ArrayList<RecyclerView.ViewHolder>();
738
739        final ArrayList<RecyclerView.ViewHolder> moveVH
740                = new ArrayList<RecyclerView.ViewHolder>();
741
742        final View[] testView = new View[1];
743        mRecyclerView.setItemAnimator(new DefaultItemAnimator() {
744            @Override
745            public boolean animateAdd(RecyclerView.ViewHolder holder) {
746                addVH.add(holder);
747                return true;
748            }
749
750            @Override
751            public boolean animateRemove(RecyclerView.ViewHolder holder) {
752                removeVH.add(holder);
753                return true;
754            }
755
756            @Override
757            public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY,
758                    int toX, int toY) {
759                moveVH.add(holder);
760                return true;
761            }
762        });
763        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
764            @Override
765            void afterPreLayout(RecyclerView.Recycler recycler,
766                    AnimationLayoutManager layoutManager,
767                    RecyclerView.State state) {
768                super.afterPreLayout(recycler, layoutManager, state);
769                testView[0] = recycler.getViewForPosition(45);
770                testView[0].measure(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST),
771                        View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST));
772                testView[0].layout(10, 10, 10 + testView[0].getMeasuredWidth(),
773                        10 + testView[0].getMeasuredHeight());
774                layoutManager.addView(testView[0], 4);
775            }
776
777            @Override
778            void afterPostLayout(RecyclerView.Recycler recycler,
779                    AnimationLayoutManager layoutManager,
780                    RecyclerView.State state) {
781                super.afterPostLayout(recycler, layoutManager, state);
782                testView[0].layout(50, 50, 50 + testView[0].getMeasuredWidth(),
783                        50 + testView[0].getMeasuredHeight());
784                layoutManager.addDisappearingView(testView[0], 4);
785            }
786        };
787        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 3;
788        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 5;
789        mRecycledViews.clear();
790        mLayoutManager.expectLayouts(2);
791        mTestAdapter.deleteAndNotify(3, 1);
792        mLayoutManager.waitForLayout(2);
793
794        for (RecyclerView.ViewHolder vh : addVH) {
795            assertNotSame("add-remove item should not animate add", testView[0], vh.itemView);
796        }
797        for (RecyclerView.ViewHolder vh : moveVH) {
798            assertNotSame("add-remove item should not animate move", testView[0], vh.itemView);
799        }
800        for (RecyclerView.ViewHolder vh : removeVH) {
801            assertNotSame("add-remove item should not animate remove", testView[0], vh.itemView);
802        }
803        boolean found = false;
804        for (RecyclerView.ViewHolder vh : mRecycledViews) {
805            found |= vh.itemView == testView[0];
806        }
807        assertTrue("added-removed view should be recycled", found);
808    }
809
810    @Test
811    public void tmpRemoveMe() throws Throwable {
812        changeAnimTest(false, false, true, false);
813    }
814
815    @Test
816    public void changeAnimations() throws Throwable {
817        final boolean[] booleans = {true, false};
818        for (boolean supportsChange : booleans) {
819            for (boolean changeType : booleans) {
820                for (boolean hasStableIds : booleans) {
821                    for (boolean deleteSomeItems : booleans) {
822                        changeAnimTest(supportsChange, changeType, hasStableIds, deleteSomeItems);
823                    }
824                    removeRecyclerView();
825                }
826            }
827        }
828    }
829
830    public void changeAnimTest(final boolean supportsChangeAnim, final boolean changeType,
831            final boolean hasStableIds, final boolean deleteSomeItems) throws Throwable {
832        final int changedIndex = 3;
833        final int defaultType = 1;
834        final AtomicInteger changedIndexNewType = new AtomicInteger(defaultType);
835        final String logPrefix = "supportsChangeAnim:" + supportsChangeAnim +
836                ", change view type:" + changeType +
837                ", has stable ids:" + hasStableIds +
838                ", delete some items:" + deleteSomeItems;
839        TestAdapter testAdapter = new TestAdapter(10) {
840            @Override
841            public int getItemViewType(int position) {
842                return position == changedIndex ? changedIndexNewType.get() : defaultType;
843            }
844
845            @Override
846            public TestViewHolder onCreateViewHolder(ViewGroup parent,
847                    int viewType) {
848                TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
849                if (DEBUG) {
850                    Log.d(TAG, logPrefix + " onCreateVH" + vh.toString());
851                }
852                return vh;
853            }
854
855            @Override
856            public void onBindViewHolder(TestViewHolder holder,
857                    int position) {
858                super.onBindViewHolder(holder, position);
859                if (DEBUG) {
860                    Log.d(TAG, logPrefix + " onBind to " + position + "" + holder.toString());
861                }
862            }
863        };
864        testAdapter.setHasStableIds(hasStableIds);
865        setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
866        ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
867                supportsChangeAnim);
868
869        final RecyclerView.ViewHolder toBeChangedVH =
870                mRecyclerView.findViewHolderForLayoutPosition(changedIndex);
871        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
872            @Override
873            void afterPreLayout(RecyclerView.Recycler recycler,
874                    AnimationLayoutManager layoutManager,
875                    RecyclerView.State state) {
876                RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(
877                        changedIndex);
878                assertTrue(logPrefix + " changed view holder should have correct flag"
879                        , vh.isUpdated());
880            }
881
882            @Override
883            void afterPostLayout(RecyclerView.Recycler recycler,
884                    AnimationLayoutManager layoutManager, RecyclerView.State state) {
885                RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(
886                        changedIndex);
887                if (supportsChangeAnim) {
888                    assertNotSame(logPrefix + "a new VH should be given if change is supported",
889                            toBeChangedVH, vh);
890                } else if (!changeType && hasStableIds) {
891                    assertSame(logPrefix + "if change animations are not supported but we have "
892                            + "stable ids, same view holder should be returned", toBeChangedVH, vh);
893                }
894                super.beforePostLayout(recycler, layoutManager, state);
895            }
896        };
897        mLayoutManager.expectLayouts(1);
898        if (changeType) {
899            changedIndexNewType.set(defaultType + 1);
900        }
901        if (deleteSomeItems) {
902            runTestOnUiThread(new Runnable() {
903                @Override
904                public void run() {
905                    try {
906                        mTestAdapter.deleteAndNotify(changedIndex + 2, 1);
907                        mTestAdapter.notifyItemChanged(3);
908                    } catch (Throwable throwable) {
909                        throwable.printStackTrace();
910                    }
911
912                }
913            });
914        } else {
915            mTestAdapter.changeAndNotify(3, 1);
916        }
917
918        mLayoutManager.waitForLayout(2);
919    }
920
921    private void testChangeWithPayload(final boolean supportsChangeAnim,
922            final boolean canReUse, Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)
923            throws Throwable {
924        final List<Object> expectedPayloads = new ArrayList<Object>();
925        final int changedIndex = 3;
926        TestAdapter testAdapter = new TestAdapter(10) {
927            @Override
928            public int getItemViewType(int position) {
929                return 1;
930            }
931
932            @Override
933            public TestViewHolder onCreateViewHolder(ViewGroup parent,
934                    int viewType) {
935                TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
936                if (DEBUG) {
937                    Log.d(TAG, " onCreateVH" + vh.toString());
938                }
939                return vh;
940            }
941
942            @Override
943            public void onBindViewHolder(TestViewHolder holder,
944                    int position, List<Object> payloads) {
945                super.onBindViewHolder(holder, position);
946                if (DEBUG) {
947                    Log.d(TAG, " onBind to " + position + "" + holder.toString());
948                }
949                assertEquals(expectedPayloads, payloads);
950            }
951        };
952        testAdapter.setHasStableIds(false);
953        setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
954        mRecyclerView.setItemAnimator(new DefaultItemAnimator() {
955            @Override
956            public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
957                    @NonNull List<Object> payloads) {
958                return canReUse && super.canReuseUpdatedViewHolder(viewHolder, payloads);
959            }
960        });
961        ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
962                supportsChangeAnim);
963
964        int numTests = notifyPayloads.length;
965        for (int i = 0; i < numTests; i++) {
966            mLayoutManager.expectLayouts(1);
967            expectedPayloads.clear();
968            for (int j = 0; j < expectedPayloadsInOnBind[i].length; j++) {
969                expectedPayloads.add(expectedPayloadsInOnBind[i][j]);
970            }
971            final Object[] payloadsToSend = notifyPayloads[i];
972            runTestOnUiThread(new Runnable() {
973                @Override
974                public void run() {
975                    for (int j = 0; j < payloadsToSend.length; j++) {
976                        mTestAdapter.notifyItemChanged(changedIndex, payloadsToSend[j]);
977                    }
978                }
979            });
980            mLayoutManager.waitForLayout(2);
981            checkForMainThreadException();
982        }
983    }
984
985    @Test
986    public void crossFadingChangeAnimationWithPayload() throws Throwable {
987        // for crossfading change animation,  will receive EMPTY payload in onBindViewHolder
988        testChangeWithPayload(true, true,
989                new Object[][]{
990                        new Object[]{"abc"},
991                        new Object[]{"abc", null, "cdf"},
992                        new Object[]{"abc", null},
993                        new Object[]{null, "abc"},
994                        new Object[]{"abc", "cdf"}
995                },
996                new Object[][]{
997                        new Object[]{"abc"},
998                        new Object[0],
999                        new Object[0],
1000                        new Object[0],
1001                        new Object[]{"abc", "cdf"}
1002                });
1003    }
1004
1005    @Test
1006    public void crossFadingChangeAnimationWithPayloadWithoutReuse() throws Throwable {
1007        // for crossfading change animation,  will receive EMPTY payload in onBindViewHolder
1008        testChangeWithPayload(true, false,
1009                new Object[][]{
1010                        new Object[]{"abc"},
1011                        new Object[]{"abc", null, "cdf"},
1012                        new Object[]{"abc", null},
1013                        new Object[]{null, "abc"},
1014                        new Object[]{"abc", "cdf"}
1015                },
1016                new Object[][]{
1017                        new Object[0],
1018                        new Object[0],
1019                        new Object[0],
1020                        new Object[0],
1021                        new Object[0]
1022                });
1023    }
1024
1025    @Test
1026    public void noChangeAnimationWithPayload() throws Throwable {
1027        // for Change Animation disabled, payload should match the payloads unless
1028        // null payload is fired.
1029        testChangeWithPayload(false, true,
1030                new Object[][]{
1031                        new Object[]{"abc"},
1032                        new Object[]{"abc", null, "cdf"},
1033                        new Object[]{"abc", null},
1034                        new Object[]{null, "abc"},
1035                        new Object[]{"abc", "cdf"}
1036                },
1037                new Object[][]{
1038                        new Object[]{"abc"},
1039                        new Object[0],
1040                        new Object[0],
1041                        new Object[0],
1042                        new Object[]{"abc", "cdf"}
1043                });
1044    }
1045
1046    @Test
1047    public void recycleDuringAnimations() throws Throwable {
1048        final AtomicInteger childCount = new AtomicInteger(0);
1049        final TestAdapter adapter = new TestAdapter(1000) {
1050            @Override
1051            public TestViewHolder onCreateViewHolder(ViewGroup parent,
1052                    int viewType) {
1053                childCount.incrementAndGet();
1054                return super.onCreateViewHolder(parent, viewType);
1055            }
1056        };
1057        setupBasic(1000, 10, 20, adapter);
1058        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 10;
1059        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 20;
1060
1061        mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool() {
1062            @Override
1063            public void putRecycledView(RecyclerView.ViewHolder scrap) {
1064                super.putRecycledView(scrap);
1065                childCount.decrementAndGet();
1066            }
1067
1068            @Override
1069            public RecyclerView.ViewHolder getRecycledView(int viewType) {
1070                final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType);
1071                if (recycledView != null) {
1072                    childCount.incrementAndGet();
1073                }
1074                return recycledView;
1075            }
1076        });
1077
1078        // now keep adding children to trigger more children being created etc.
1079        for (int i = 0; i < 100; i++) {
1080            adapter.addAndNotify(15, 1);
1081            Thread.sleep(50);
1082        }
1083        getInstrumentation().waitForIdleSync();
1084        waitForAnimations(2);
1085        assertEquals("Children count should add up", childCount.get(),
1086                mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
1087    }
1088
1089    @Test
1090    public void notifyDataSetChanged() throws Throwable {
1091        setupBasic(10, 3, 4);
1092        int layoutCount = mLayoutManager.mTotalLayoutCount;
1093        mLayoutManager.expectLayouts(1);
1094        runTestOnUiThread(new Runnable() {
1095            @Override
1096            public void run() {
1097                try {
1098                    mTestAdapter.deleteAndNotify(4, 1);
1099                    mTestAdapter.dispatchDataSetChanged();
1100                } catch (Throwable throwable) {
1101                    throwable.printStackTrace();
1102                }
1103
1104            }
1105        });
1106        mLayoutManager.waitForLayout(2);
1107        getInstrumentation().waitForIdleSync();
1108        assertEquals("on notify data set changed, predictive animations should not run",
1109                layoutCount + 1, mLayoutManager.mTotalLayoutCount);
1110        mLayoutManager.expectLayouts(2);
1111        mTestAdapter.addAndNotify(4, 2);
1112        // make sure animations recover
1113        mLayoutManager.waitForLayout(2);
1114    }
1115
1116    @Test
1117    public void stableIdNotifyDataSetChanged() throws Throwable {
1118        final int itemCount = 20;
1119        List<Item> initialSet = new ArrayList<Item>();
1120        final TestAdapter adapter = new TestAdapter(itemCount) {
1121            @Override
1122            public long getItemId(int position) {
1123                return mItems.get(position).mId;
1124            }
1125        };
1126        adapter.setHasStableIds(true);
1127        initialSet.addAll(adapter.mItems);
1128        positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() {
1129                    @Override
1130                    void onRun(TestAdapter testAdapter) throws Throwable {
1131                        Item item5 = adapter.mItems.get(5);
1132                        Item item6 = adapter.mItems.get(6);
1133                        item5.mAdapterIndex = 6;
1134                        item6.mAdapterIndex = 5;
1135                        adapter.mItems.remove(5);
1136                        adapter.mItems.add(6, item5);
1137                        adapter.dispatchDataSetChanged();
1138                        //hacky, we support only 1 layout pass
1139                        mLayoutManager.layoutLatch.countDown();
1140                    }
1141                }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
1142                PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8),
1143                PositionConstraint.scrap(9, -1, 9));
1144        // now mix items.
1145    }
1146
1147
1148    @Test
1149    public void getItemForDeletedView() throws Throwable {
1150        getItemForDeletedViewTest(false);
1151        getItemForDeletedViewTest(true);
1152    }
1153
1154    public void getItemForDeletedViewTest(boolean stableIds) throws Throwable {
1155        final Set<Integer> itemViewTypeQueries = new HashSet<Integer>();
1156        final Set<Integer> itemIdQueries = new HashSet<Integer>();
1157        TestAdapter adapter = new TestAdapter(10) {
1158            @Override
1159            public int getItemViewType(int position) {
1160                itemViewTypeQueries.add(position);
1161                return super.getItemViewType(position);
1162            }
1163
1164            @Override
1165            public long getItemId(int position) {
1166                itemIdQueries.add(position);
1167                return mItems.get(position).mId;
1168            }
1169        };
1170        adapter.setHasStableIds(stableIds);
1171        setupBasic(10, 0, 10, adapter);
1172        assertEquals("getItemViewType for all items should be called", 10,
1173                itemViewTypeQueries.size());
1174        if (adapter.hasStableIds()) {
1175            assertEquals("getItemId should be called when adapter has stable ids", 10,
1176                    itemIdQueries.size());
1177        } else {
1178            assertEquals("getItemId should not be called when adapter does not have stable ids", 0,
1179                    itemIdQueries.size());
1180        }
1181        itemViewTypeQueries.clear();
1182        itemIdQueries.clear();
1183        mLayoutManager.expectLayouts(2);
1184        // delete last two
1185        final int deleteStart = 8;
1186        final int deleteCount = adapter.getItemCount() - deleteStart;
1187        adapter.deleteAndNotify(deleteStart, deleteCount);
1188        mLayoutManager.waitForLayout(2);
1189        for (int i = 0; i < deleteStart; i++) {
1190            assertTrue("getItemViewType for existing item " + i + " should be called",
1191                    itemViewTypeQueries.contains(i));
1192            if (adapter.hasStableIds()) {
1193                assertTrue("getItemId for existing item " + i
1194                                + " should be called when adapter has stable ids",
1195                        itemIdQueries.contains(i));
1196            }
1197        }
1198        for (int i = deleteStart; i < deleteStart + deleteCount; i++) {
1199            assertFalse("getItemViewType for deleted item " + i + " SHOULD NOT be called",
1200                    itemViewTypeQueries.contains(i));
1201            if (adapter.hasStableIds()) {
1202                assertFalse("getItemId for deleted item " + i + " SHOULD NOT be called",
1203                        itemIdQueries.contains(i));
1204            }
1205        }
1206    }
1207
1208    @Test
1209    public void deleteInvisibleMultiStep() throws Throwable {
1210        setupBasic(1000, 1, 7);
1211        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
1212        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
1213        mLayoutManager.expectLayouts(1);
1214        // try to trigger race conditions
1215        int targetItemCount = mTestAdapter.getItemCount();
1216        for (int i = 0; i < 100; i++) {
1217            mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});
1218            checkForMainThreadException();
1219            targetItemCount -= 2;
1220        }
1221        // wait until main thread runnables are consumed
1222        while (targetItemCount != mTestAdapter.getItemCount()) {
1223            Thread.sleep(100);
1224        }
1225        mLayoutManager.waitForLayout(2);
1226    }
1227
1228    @Test
1229    public void addManyMultiStep() throws Throwable {
1230        setupBasic(10, 1, 7);
1231        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
1232        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
1233        mLayoutManager.expectLayouts(1);
1234        // try to trigger race conditions
1235        int targetItemCount = mTestAdapter.getItemCount();
1236        for (int i = 0; i < 100; i++) {
1237            checkForMainThreadException();
1238            mTestAdapter.addAndNotify(0, 1);
1239            checkForMainThreadException();
1240            mTestAdapter.addAndNotify(7, 1);
1241            targetItemCount += 2;
1242        }
1243        checkForMainThreadException();
1244        // wait until main thread runnables are consumed
1245        while (targetItemCount != mTestAdapter.getItemCount()) {
1246            Thread.sleep(100);
1247            checkForMainThreadException();
1248        }
1249        mLayoutManager.waitForLayout(2);
1250    }
1251
1252    @Test
1253    public void basicDelete() throws Throwable {
1254        setupBasic(10);
1255        final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() {
1256            @Override
1257            public void postDispatchLayout() {
1258                // verify this only in first layout
1259                assertEquals("deleted views should still be children of RV",
1260                        mLayoutManager.getChildCount() + mDeletedViewCount
1261                        , mRecyclerView.getChildCount());
1262            }
1263
1264            @Override
1265            void afterPreLayout(RecyclerView.Recycler recycler,
1266                    AnimationLayoutManager layoutManager,
1267                    RecyclerView.State state) {
1268                super.afterPreLayout(recycler, layoutManager, state);
1269                mLayoutItemCount = 3;
1270                mLayoutMin = 0;
1271            }
1272        };
1273        callbacks.mLayoutItemCount = 10;
1274        callbacks.setExpectedItemCounts(10, 3);
1275        mLayoutManager.setOnLayoutCallbacks(callbacks);
1276
1277        mLayoutManager.expectLayouts(2);
1278        mTestAdapter.deleteAndNotify(0, 7);
1279        mLayoutManager.waitForLayout(2);
1280        callbacks.reset();// when animations end another layout will happen
1281    }
1282
1283
1284    @Test
1285    public void adapterChangeDuringScrolling() throws Throwable {
1286        setupBasic(10);
1287        final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
1288        final AtomicInteger onScrollItemCount = new AtomicInteger(0);
1289
1290        mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
1291            @Override
1292            void onLayoutChildren(RecyclerView.Recycler recycler,
1293                    AnimationLayoutManager lm, RecyclerView.State state) {
1294                onLayoutItemCount.set(state.getItemCount());
1295                super.onLayoutChildren(recycler, lm, state);
1296            }
1297
1298            @Override
1299            public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
1300                onScrollItemCount.set(state.getItemCount());
1301                super.onScroll(dx, recycler, state);
1302            }
1303        });
1304        runTestOnUiThread(new Runnable() {
1305            @Override
1306            public void run() {
1307                mTestAdapter.mItems.remove(5);
1308                mTestAdapter.notifyItemRangeRemoved(5, 1);
1309                mRecyclerView.scrollBy(0, 100);
1310                assertTrue("scrolling while there are pending adapter updates should "
1311                        + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
1312                assertEquals("scroll by should be called w/ updated adapter count",
1313                        mTestAdapter.mItems.size(), onScrollItemCount.get());
1314
1315            }
1316        });
1317    }
1318
1319    @Test
1320    public void notifyDataSetChangedDuringScroll() throws Throwable {
1321        setupBasic(10);
1322        final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
1323        final AtomicInteger onScrollItemCount = new AtomicInteger(0);
1324
1325        mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
1326            @Override
1327            void onLayoutChildren(RecyclerView.Recycler recycler,
1328                    AnimationLayoutManager lm, RecyclerView.State state) {
1329                onLayoutItemCount.set(state.getItemCount());
1330                super.onLayoutChildren(recycler, lm, state);
1331            }
1332
1333            @Override
1334            public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
1335                onScrollItemCount.set(state.getItemCount());
1336                super.onScroll(dx, recycler, state);
1337            }
1338        });
1339        runTestOnUiThread(new Runnable() {
1340            @Override
1341            public void run() {
1342                mTestAdapter.mItems.remove(5);
1343                mTestAdapter.notifyDataSetChanged();
1344                mRecyclerView.scrollBy(0, 100);
1345                assertTrue("scrolling while there are pending adapter updates should "
1346                        + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
1347                assertEquals("scroll by should be called w/ updated adapter count",
1348                        mTestAdapter.mItems.size(), onScrollItemCount.get());
1349
1350            }
1351        });
1352    }
1353
1354    @Test
1355    public void addInvisibleAndVisible() throws Throwable {
1356        setupBasic(10, 1, 7);
1357        mLayoutManager.expectLayouts(2);
1358        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
1359        mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{7, 1});// add a new item 0 // invisible
1360        mLayoutManager.waitForLayout(2);
1361    }
1362
1363    @Test
1364    public void addInvisible() throws Throwable {
1365        setupBasic(10, 1, 7);
1366        mLayoutManager.expectLayouts(1);
1367        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
1368        mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{8, 1});// add a new item 0
1369        mLayoutManager.waitForLayout(2);
1370    }
1371
1372    @Test
1373    public void basicAdd() throws Throwable {
1374        setupBasic(10);
1375        mLayoutManager.expectLayouts(2);
1376        setExpectedItemCounts(10, 13);
1377        mTestAdapter.addAndNotify(2, 3);
1378        mLayoutManager.waitForLayout(2);
1379    }
1380
1381    @Test
1382    public void appCancelAnimationInDetach() throws Throwable {
1383        final View[] addedView = new View[2];
1384        TestAdapter adapter = new TestAdapter(1) {
1385            @Override
1386            public void onViewDetachedFromWindow(TestViewHolder holder) {
1387                if ((addedView[0] == holder.itemView || addedView[1] == holder.itemView)
1388                        && ViewCompat.hasTransientState(holder.itemView)) {
1389                    ViewCompat.animate(holder.itemView).cancel();
1390                }
1391                super.onViewDetachedFromWindow(holder);
1392            }
1393        };
1394        // original 1 item
1395        setupBasic(1, 0, 1, adapter);
1396        mRecyclerView.getItemAnimator().setAddDuration(10000);
1397        mLayoutManager.expectLayouts(2);
1398        // add 2 items
1399        setExpectedItemCounts(1, 3);
1400        mTestAdapter.addAndNotify(0, 2);
1401        mLayoutManager.waitForLayout(2);
1402        checkForMainThreadException();
1403        // wait till "add animation" starts
1404        int limit = 200;
1405        while (addedView[0] == null || addedView[1] == null) {
1406            Thread.sleep(100);
1407            runTestOnUiThread(new Runnable() {
1408                @Override
1409                public void run() {
1410                    if (mRecyclerView.getChildCount() == 3) {
1411                        View view = mRecyclerView.getChildAt(0);
1412                        if (ViewCompat.hasTransientState(view)) {
1413                            addedView[0] = view;
1414                        }
1415                        view = mRecyclerView.getChildAt(1);
1416                        if (ViewCompat.hasTransientState(view)) {
1417                            addedView[1] = view;
1418                        }
1419                    }
1420                }
1421            });
1422            assertTrue("add should start on time", --limit > 0);
1423        }
1424
1425        // Layout from item2, exclude the current adding items
1426        mLayoutManager.expectLayouts(1);
1427        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
1428            @Override
1429            void beforePostLayout(RecyclerView.Recycler recycler,
1430                    AnimationLayoutManager layoutManager,
1431                    RecyclerView.State state) {
1432                mLayoutMin = 2;
1433                mLayoutItemCount = 1;
1434            }
1435        };
1436        requestLayoutOnUIThread(mRecyclerView);
1437        mLayoutManager.waitForLayout(2);
1438    }
1439
1440    @Test
1441    public void adapterChangeFrozen() throws Throwable {
1442        setupBasic(10, 1, 7);
1443        assertTrue(mRecyclerView.getChildCount() == 7);
1444
1445        mLayoutManager.expectLayouts(2);
1446        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
1447        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
1448        freezeLayout(true);
1449        mTestAdapter.addAndNotify(0, 1);
1450
1451        mLayoutManager.assertNoLayout("RV should keep old child during frozen", 2);
1452        assertEquals(7, mRecyclerView.getChildCount());
1453
1454        freezeLayout(false);
1455        mLayoutManager.waitForLayout(2);
1456        assertEquals("RV should get updated after waken from frozen",
1457                8, mRecyclerView.getChildCount());
1458    }
1459
1460    @Test
1461    public void removeScrapInvalidate() throws Throwable {
1462        setupBasic(10);
1463        TestRecyclerView testRecyclerView = getTestRecyclerView();
1464        mLayoutManager.expectLayouts(1);
1465        testRecyclerView.expectDraw(1);
1466        runTestOnUiThread(new Runnable() {
1467            @Override
1468            public void run() {
1469                mTestAdapter.mItems.clear();
1470                mTestAdapter.notifyDataSetChanged();
1471            }
1472        });
1473        mLayoutManager.waitForLayout(2);
1474        testRecyclerView.waitForDraw(2);
1475    }
1476
1477    @Test
1478    public void deleteVisibleAndInvisible() throws Throwable {
1479        setupBasic(11, 3, 5); //layout items  3 4 5 6 7
1480        mLayoutManager.expectLayouts(2);
1481        setLayoutRange(3, 5); //layout previously invisible child 10 from end of the list
1482        setExpectedItemCounts(9, 8);
1483        mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9
1484        mLayoutManager.waitForLayout(2);
1485    }
1486
1487    @Test
1488    public void findPositionOffset() throws Throwable {
1489        setupBasic(10);
1490        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
1491            @Override
1492            void beforePreLayout(RecyclerView.Recycler recycler,
1493                    AnimationLayoutManager lm, RecyclerView.State state) {
1494                super.beforePreLayout(recycler, lm, state);
1495                // [0,2,4]
1496                assertEquals("offset check", 0, mAdapterHelper.findPositionOffset(0));
1497                assertEquals("offset check", 1, mAdapterHelper.findPositionOffset(2));
1498                assertEquals("offset check", 2, mAdapterHelper.findPositionOffset(4));
1499            }
1500        };
1501        runTestOnUiThread(new Runnable() {
1502            @Override
1503            public void run() {
1504                try {
1505                    // delete 1
1506                    mTestAdapter.deleteAndNotify(1, 1);
1507                    // delete 3
1508                    mTestAdapter.deleteAndNotify(2, 1);
1509                } catch (Throwable throwable) {
1510                    throwable.printStackTrace();
1511                }
1512            }
1513        });
1514        mLayoutManager.waitForLayout(2);
1515    }
1516
1517    private void setLayoutRange(int start, int count) {
1518        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
1519        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
1520    }
1521
1522    private void setExpectedItemCounts(int preLayout, int postLayout) {
1523        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
1524    }
1525
1526    @Test
1527    public void deleteInvisible() throws Throwable {
1528        setupBasic(10, 1, 7);
1529        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
1530        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
1531        mLayoutManager.expectLayouts(1);
1532        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8);
1533        mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});// delete item id 0,8
1534        mLayoutManager.waitForLayout(2);
1535    }
1536
1537    private CollectPositionResult findByPos(RecyclerView recyclerView,
1538            RecyclerView.Recycler recycler, RecyclerView.State state, int position) {
1539        View view = recycler.getViewForPosition(position, true);
1540        RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
1541        if (vh.wasReturnedFromScrap()) {
1542            vh.clearReturnedFromScrapFlag(); //keep data consistent.
1543            return CollectPositionResult.fromScrap(vh);
1544        } else {
1545            return CollectPositionResult.fromAdapter(vh);
1546        }
1547    }
1548
1549    public Map<Integer, CollectPositionResult> collectPositions(RecyclerView recyclerView,
1550            RecyclerView.Recycler recycler, RecyclerView.State state, int... positions) {
1551        Map<Integer, CollectPositionResult> positionToAdapterMapping
1552                = new HashMap<Integer, CollectPositionResult>();
1553        for (int position : positions) {
1554            if (position < 0) {
1555                continue;
1556            }
1557            positionToAdapterMapping.put(position,
1558                    findByPos(recyclerView, recycler, state, position));
1559        }
1560        return positionToAdapterMapping;
1561    }
1562
1563    @Test
1564    public void addDelete2() throws Throwable {
1565        positionStatesTest(5, 0, 5, new AdapterOps() {
1566                    // 0 1 2 3 4
1567                    // 0 1 2 a b 3 4
1568                    // 0 1 b 3 4
1569                    // pre: 0 1 2 3 4
1570                    // pre w/ adap: 0 1 2 b 3 4
1571                    @Override
1572                    void onRun(TestAdapter adapter) throws Throwable {
1573                        adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
1574                    }
1575                }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
1576                PositionConstraint.scrap(3, 3, 3)
1577        );
1578    }
1579
1580    @Test
1581    public void addDelete1() throws Throwable {
1582        positionStatesTest(5, 0, 5, new AdapterOps() {
1583                    // 0 1 2 3 4
1584                    // 0 1 2 a b 3 4
1585                    // 0 2 a b 3 4
1586                    // 0 c d 2 a b 3 4
1587                    // 0 c d 2 a 4
1588                    // c d 2 a 4
1589                    // pre: 0 1 2 3 4
1590                    @Override
1591                    void onRun(TestAdapter adapter) throws Throwable {
1592                        adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
1593                                new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
1594                    }
1595                }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
1596                PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1),
1597                PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0),
1598                PositionConstraint.adapter(1), PositionConstraint.adapter(3)
1599        );
1600    }
1601
1602    @Test
1603    public void addSameIndexTwice() throws Throwable {
1604        positionStatesTest(12, 2, 7, new AdapterOps() {
1605                    @Override
1606                    void onRun(TestAdapter adapter) throws Throwable {
1607                        adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
1608                                new int[]{11, 1});
1609                    }
1610                }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
1611                PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7),
1612                PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12),
1613                PositionConstraint.scrap(8, 8, 13)
1614        );
1615    }
1616
1617    @Test
1618    public void deleteTwice() throws Throwable {
1619        positionStatesTest(12, 2, 7, new AdapterOps() {
1620                    @Override
1621                    void onRun(TestAdapter adapter) throws Throwable {
1622                        adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
1623                                new int[]{0, 1});// delete item ids 0,2,9,1
1624                    }
1625                }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
1626                PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2),
1627                PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5),
1628                PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7)
1629        );
1630    }
1631
1632
1633    public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
1634            int firstLayoutItemCount, AdapterOps adapterChanges,
1635            final PositionConstraint... constraints) throws Throwable {
1636        positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null,
1637                adapterChanges, constraints);
1638    }
1639
1640    public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
1641            int firstLayoutItemCount, TestAdapter adapter, AdapterOps adapterChanges,
1642            final PositionConstraint... constraints) throws Throwable {
1643        setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter);
1644        mLayoutManager.expectLayouts(2);
1645        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
1646            @Override
1647            void beforePreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
1648                    RecyclerView.State state) {
1649                super.beforePreLayout(recycler, lm, state);
1650                //harmless
1651                lm.detachAndScrapAttachedViews(recycler);
1652                final int[] ids = new int[constraints.length];
1653                for (int i = 0; i < constraints.length; i++) {
1654                    ids[i] = constraints[i].mPreLayoutPos;
1655                }
1656                Map<Integer, CollectPositionResult> positions
1657                        = collectPositions(lm.mRecyclerView, recycler, state, ids);
1658                StringBuilder positionLog = new StringBuilder("\nPosition logs:\n");
1659                for (Map.Entry<Integer, CollectPositionResult> entry : positions.entrySet()) {
1660                    positionLog.append(entry.getKey()).append(":").append(entry.getValue())
1661                            .append("\n");
1662                }
1663                for (PositionConstraint constraint : constraints) {
1664                    if (constraint.mPreLayoutPos != -1) {
1665                        constraint.validate(state, positions.get(constraint.mPreLayoutPos),
1666                                lm.getLog() + positionLog);
1667                    }
1668                }
1669            }
1670
1671            @Override
1672            void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
1673                    RecyclerView.State state) {
1674                super.beforePostLayout(recycler, lm, state);
1675                lm.detachAndScrapAttachedViews(recycler);
1676                final int[] ids = new int[constraints.length];
1677                for (int i = 0; i < constraints.length; i++) {
1678                    ids[i] = constraints[i].mPostLayoutPos;
1679                }
1680                Map<Integer, CollectPositionResult> positions
1681                        = collectPositions(lm.mRecyclerView, recycler, state, ids);
1682                StringBuilder positionLog = new StringBuilder("\nPosition logs:\n");
1683                for (Map.Entry<Integer, CollectPositionResult> entry : positions.entrySet()) {
1684                    positionLog.append(entry.getKey()).append(":")
1685                            .append(entry.getValue()).append("\n");
1686                }
1687                for (PositionConstraint constraint : constraints) {
1688                    if (constraint.mPostLayoutPos >= 0) {
1689                        constraint.validate(state, positions.get(constraint.mPostLayoutPos),
1690                                lm.getLog() + positionLog);
1691                    }
1692                }
1693            }
1694        };
1695        adapterChanges.run(mTestAdapter);
1696        mLayoutManager.waitForLayout(2);
1697        checkForMainThreadException();
1698        for (PositionConstraint constraint : constraints) {
1699            constraint.assertValidate();
1700        }
1701    }
1702
1703    @Test
1704    public void addThenRecycleRemovedView() throws Throwable {
1705        setupBasic(10);
1706        final AtomicInteger step = new AtomicInteger(0);
1707        final List<RecyclerView.ViewHolder> animateRemoveList
1708                = new ArrayList<RecyclerView.ViewHolder>();
1709        DefaultItemAnimator animator = new DefaultItemAnimator() {
1710            @Override
1711            public boolean animateRemove(RecyclerView.ViewHolder holder) {
1712                animateRemoveList.add(holder);
1713                return super.animateRemove(holder);
1714            }
1715        };
1716        mRecyclerView.setItemAnimator(animator);
1717        final List<RecyclerView.ViewHolder> pooledViews = new ArrayList<RecyclerView.ViewHolder>();
1718        mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool() {
1719            @Override
1720            public void putRecycledView(RecyclerView.ViewHolder scrap) {
1721                pooledViews.add(scrap);
1722                super.putRecycledView(scrap);
1723            }
1724        });
1725        final RecyclerView.ViewHolder[] targetVh = new RecyclerView.ViewHolder[1];
1726        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
1727            @Override
1728            void doLayout(RecyclerView.Recycler recycler,
1729                    AnimationLayoutManager lm, RecyclerView.State state) {
1730                switch (step.get()) {
1731                    case 1:
1732                        super.doLayout(recycler, lm, state);
1733                        if (state.isPreLayout()) {
1734                            View view = mLayoutManager.getChildAt(1);
1735                            RecyclerView.ViewHolder holder =
1736                                    mRecyclerView.getChildViewHolderInt(view);
1737                            targetVh[0] = holder;
1738                            assertTrue("test sanity", holder.isRemoved());
1739                            mLayoutManager.removeAndRecycleView(view, recycler);
1740                        }
1741                        break;
1742                }
1743            }
1744        };
1745        step.set(1);
1746        animateRemoveList.clear();
1747        mLayoutManager.expectLayouts(2);
1748        mTestAdapter.deleteAndNotify(1, 1);
1749        mLayoutManager.waitForLayout(2);
1750        assertTrue("test sanity, view should be recycled", pooledViews.contains(targetVh[0]));
1751        assertTrue("since LM force recycled a view, animate disappearance should not be called",
1752                animateRemoveList.isEmpty());
1753    }
1754}
1755