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