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 android.content.Context;
20import android.graphics.Canvas;
21import android.util.AttributeSet;
22import android.util.Log;
23import android.view.View;
24import android.view.ViewGroup;
25
26import java.util.ArrayList;
27import java.util.HashMap;
28import java.util.HashSet;
29import java.util.List;
30import java.util.Map;
31import java.util.Set;
32import java.util.concurrent.CountDownLatch;
33import java.util.concurrent.TimeUnit;
34import java.util.concurrent.atomic.AtomicInteger;
35
36public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest {
37
38    private static final boolean DEBUG = false;
39
40    private static final String TAG = "RecyclerViewAnimationsTest";
41
42    AnimationLayoutManager mLayoutManager;
43
44    TestAdapter mTestAdapter;
45
46    public RecyclerViewAnimationsTest() {
47        super(DEBUG);
48    }
49
50    @Override
51    protected void setUp() throws Exception {
52        super.setUp();
53    }
54
55    RecyclerView setupBasic(int itemCount) throws Throwable {
56        return setupBasic(itemCount, 0, itemCount);
57    }
58
59    RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount)
60            throws Throwable {
61        return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null);
62    }
63
64    RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount,
65            TestAdapter testAdapter)
66            throws Throwable {
67        final TestRecyclerView recyclerView = new TestRecyclerView(getActivity());
68        recyclerView.setHasFixedSize(true);
69        if (testAdapter == null) {
70            mTestAdapter = new TestAdapter(itemCount);
71        } else {
72            mTestAdapter = testAdapter;
73        }
74        recyclerView.setAdapter(mTestAdapter);
75        mLayoutManager = new AnimationLayoutManager();
76        recyclerView.setLayoutManager(mLayoutManager);
77        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex;
78        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount;
79
80        mLayoutManager.expectLayouts(1);
81        recyclerView.expectDraw(1);
82        setRecyclerView(recyclerView);
83        mLayoutManager.waitForLayout(2);
84        recyclerView.waitForDraw(1);
85        mLayoutManager.mOnLayoutCallbacks.reset();
86        getInstrumentation().waitForIdleSync();
87        assertEquals("extra layouts should not happen", 1, mLayoutManager.getTotalLayoutCount());
88        assertEquals("all expected children should be laid out", firstLayoutItemCount,
89                mLayoutManager.getChildCount());
90        return recyclerView;
91    }
92
93    public void testDetachBeforeAnimations() throws Throwable {
94        setupBasic(10, 0, 5);
95        final RecyclerView rv = mRecyclerView;
96        waitForAnimations(2);
97        final DefaultItemAnimator animator = new DefaultItemAnimator() {
98            @Override
99            public void runPendingAnimations() {
100                super.runPendingAnimations();
101            }
102        };
103        rv.setItemAnimator(animator);
104        mLayoutManager.expectLayouts(2);
105        mTestAdapter.deleteAndNotify(3, 4);
106        mLayoutManager.waitForLayout(2);
107        removeRecyclerView();
108        assertNull("test sanity check RV should be removed", rv.getParent());
109        assertEquals("no views should be hidden", 0, rv.mChildHelper.mHiddenViews.size());
110        assertFalse("there should not be any animations running", animator.isRunning());
111    }
112
113    public void testPreLayoutPositionCleanup() throws Throwable {
114        setupBasic(4, 0, 4);
115        mLayoutManager.expectLayouts(2);
116        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
117            @Override
118            void beforePreLayout(RecyclerView.Recycler recycler,
119                    AnimationLayoutManager lm, RecyclerView.State state) {
120                mLayoutMin = 0;
121                mLayoutItemCount = 3;
122            }
123
124            @Override
125            void beforePostLayout(RecyclerView.Recycler recycler,
126                    AnimationLayoutManager layoutManager,
127                    RecyclerView.State state) {
128                mLayoutMin = 0;
129                mLayoutItemCount = 4;
130            }
131        };
132        mTestAdapter.addAndNotify(0, 1);
133        mLayoutManager.waitForLayout(2);
134
135
136
137    }
138
139    public void testAddRemoveSamePass() throws Throwable {
140        final List<RecyclerView.ViewHolder> mRecycledViews
141                = new ArrayList<RecyclerView.ViewHolder>();
142        TestAdapter adapter = new TestAdapter(50) {
143            @Override
144            public void onViewRecycled(TestViewHolder holder) {
145                super.onViewRecycled(holder);
146                mRecycledViews.add(holder);
147            }
148        };
149        adapter.setHasStableIds(true);
150        setupBasic(50, 3, 5, adapter);
151        mRecyclerView.setItemViewCacheSize(0);
152        final ArrayList<RecyclerView.ViewHolder> addVH
153                = new ArrayList<RecyclerView.ViewHolder>();
154        final ArrayList<RecyclerView.ViewHolder> removeVH
155                = new ArrayList<RecyclerView.ViewHolder>();
156
157        final ArrayList<RecyclerView.ViewHolder> moveVH
158                = new ArrayList<RecyclerView.ViewHolder>();
159
160        final View[] testView = new View[1];
161        mRecyclerView.setItemAnimator(new DefaultItemAnimator() {
162            @Override
163            public boolean animateAdd(RecyclerView.ViewHolder holder) {
164                addVH.add(holder);
165                return true;
166            }
167
168            @Override
169            public boolean animateRemove(RecyclerView.ViewHolder holder) {
170                removeVH.add(holder);
171                return true;
172            }
173
174            @Override
175            public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY,
176                    int toX, int toY) {
177                moveVH.add(holder);
178                return true;
179            }
180        });
181        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
182            @Override
183            void afterPreLayout(RecyclerView.Recycler recycler,
184                    AnimationLayoutManager layoutManager,
185                    RecyclerView.State state) {
186                super.afterPreLayout(recycler, layoutManager, state);
187                testView[0] = recycler.getViewForPosition(45);
188                testView[0].measure(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST),
189                        View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST));
190                testView[0].layout(10, 10, 10 + testView[0].getMeasuredWidth(),
191                        10 + testView[0].getMeasuredHeight());
192                layoutManager.addView(testView[0], 4);
193            }
194
195            @Override
196            void afterPostLayout(RecyclerView.Recycler recycler,
197                    AnimationLayoutManager layoutManager,
198                    RecyclerView.State state) {
199                super.afterPostLayout(recycler, layoutManager, state);
200                testView[0].layout(50, 50, 50 + testView[0].getMeasuredWidth(),
201                        50 + testView[0].getMeasuredHeight());
202                layoutManager.addDisappearingView(testView[0], 4);
203            }
204        };
205        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 3;
206        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 5;
207        mRecycledViews.clear();
208        mLayoutManager.expectLayouts(2);
209        mTestAdapter.deleteAndNotify(3, 1);
210        mLayoutManager.waitForLayout(2);
211
212        for (RecyclerView.ViewHolder vh : addVH) {
213            assertNotSame("add-remove item should not animate add", testView[0], vh.itemView);
214        }
215        for (RecyclerView.ViewHolder vh : moveVH) {
216            assertNotSame("add-remove item should not animate move", testView[0], vh.itemView);
217        }
218        for (RecyclerView.ViewHolder vh : removeVH) {
219            assertNotSame("add-remove item should not animate remove", testView[0], vh.itemView);
220        }
221        boolean found = false;
222        for (RecyclerView.ViewHolder vh : mRecycledViews) {
223            found |= vh.itemView == testView[0];
224        }
225        assertTrue("added-removed view should be recycled", found);
226    }
227
228    public void testChangeAnimations()  throws Throwable {
229        final boolean[] booleans = {true, false};
230        for (boolean supportsChange : booleans) {
231            for (boolean changeType : booleans) {
232                for (boolean hasStableIds : booleans) {
233                    for (boolean deleteSomeItems : booleans) {
234                        changeAnimTest(supportsChange, changeType, hasStableIds, deleteSomeItems);
235                    }
236                    removeRecyclerView();
237                }
238            }
239        }
240    }
241    public void changeAnimTest(final boolean supportsChangeAnim, final boolean changeType,
242            final boolean hasStableIds, final boolean deleteSomeItems)  throws Throwable {
243        final int changedIndex = 3;
244        final int defaultType = 1;
245        final AtomicInteger changedIndexNewType = new AtomicInteger(defaultType);
246        final String logPrefix = "supportsChangeAnim:" + supportsChangeAnim +
247                ", change view type:" + changeType +
248                ", has stable ids:" + hasStableIds +
249                ", force predictive:" + deleteSomeItems;
250        TestAdapter testAdapter = new TestAdapter(10) {
251            @Override
252            public int getItemViewType(int position) {
253                return position == changedIndex ? changedIndexNewType.get() : defaultType;
254            }
255
256            @Override
257            public TestViewHolder onCreateViewHolder(ViewGroup parent,
258                    int viewType) {
259                TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
260                if (DEBUG) {
261                    Log.d(TAG, logPrefix + " onCreateVH" + vh.toString());
262                }
263                return vh;
264            }
265
266            @Override
267            public void onBindViewHolder(TestViewHolder holder,
268                    int position) {
269                super.onBindViewHolder(holder, position);
270                if (DEBUG) {
271                    Log.d(TAG, logPrefix + " onBind to " + position + "" + holder.toString());
272                }
273            }
274        };
275        testAdapter.setHasStableIds(hasStableIds);
276        setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
277        mRecyclerView.getItemAnimator().setSupportsChangeAnimations(supportsChangeAnim);
278
279        final RecyclerView.ViewHolder toBeChangedVH =
280                mRecyclerView.findViewHolderForLayoutPosition(changedIndex);
281        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
282            @Override
283            void afterPreLayout(RecyclerView.Recycler recycler,
284                    AnimationLayoutManager layoutManager,
285                    RecyclerView.State state) {
286                RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(
287                        changedIndex);
288                if (supportsChangeAnim) {
289                    assertTrue(logPrefix + " changed view holder should have correct flag"
290                            , vh.isChanged());
291                } else {
292                    assertFalse(logPrefix + " changed view holder should have correct flag"
293                            , vh.isChanged());
294                }
295            }
296
297            @Override
298            void afterPostLayout(RecyclerView.Recycler recycler,
299                    AnimationLayoutManager layoutManager, RecyclerView.State state) {
300                RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(
301                        changedIndex);
302                assertFalse(logPrefix + "VH should not be marked as changed", vh.isChanged());
303                if (supportsChangeAnim) {
304                    assertNotSame(logPrefix + "a new VH should be given if change is supported",
305                            toBeChangedVH, vh);
306                } else if (!changeType && hasStableIds) {
307                    assertSame(logPrefix + "if change animations are not supported but we have "
308                            + "stable ids, same view holder should be returned", toBeChangedVH, vh);
309                }
310                super.beforePostLayout(recycler, layoutManager, state);
311            }
312        };
313        mLayoutManager.expectLayouts(1);
314        if (changeType) {
315            changedIndexNewType.set(defaultType + 1);
316        }
317        if (deleteSomeItems) {
318            runTestOnUiThread(new Runnable() {
319                @Override
320                public void run() {
321                    try {
322                        mTestAdapter.deleteAndNotify(changedIndex + 2, 1);
323                        mTestAdapter.notifyItemChanged(3);
324                    } catch (Throwable throwable) {
325                        throwable.printStackTrace();
326                    }
327
328                }
329            });
330        } else {
331            mTestAdapter.notifyItemChanged(3);
332        }
333
334        mLayoutManager.waitForLayout(2);
335    }
336
337    public void testRecycleDuringAnimations() throws Throwable {
338        final AtomicInteger childCount = new AtomicInteger(0);
339        final TestAdapter adapter = new TestAdapter(1000) {
340            @Override
341            public TestViewHolder onCreateViewHolder(ViewGroup parent,
342                    int viewType) {
343                childCount.incrementAndGet();
344                return super.onCreateViewHolder(parent, viewType);
345            }
346        };
347        setupBasic(1000, 10, 20, adapter);
348        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 10;
349        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 20;
350
351        mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool() {
352            @Override
353            public void putRecycledView(RecyclerView.ViewHolder scrap) {
354                super.putRecycledView(scrap);
355                childCount.decrementAndGet();
356            }
357
358            @Override
359            public RecyclerView.ViewHolder getRecycledView(int viewType) {
360                final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType);
361                if (recycledView != null) {
362                    childCount.incrementAndGet();
363                }
364                return recycledView;
365            }
366        });
367
368        // now keep adding children to trigger more children being created etc.
369        for (int i = 0; i < 100; i ++) {
370            adapter.addAndNotify(15, 1);
371            Thread.sleep(50);
372        }
373        getInstrumentation().waitForIdleSync();
374        waitForAnimations(2);
375        assertEquals("Children count should add up", childCount.get(),
376                mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
377    }
378
379    public void testNotifyDataSetChanged() throws Throwable {
380        setupBasic(10, 3, 4);
381        int layoutCount = mLayoutManager.mTotalLayoutCount;
382        mLayoutManager.expectLayouts(1);
383        runTestOnUiThread(new Runnable() {
384            @Override
385            public void run() {
386                try {
387                    mTestAdapter.deleteAndNotify(4, 1);
388                    mTestAdapter.dispatchDataSetChanged();
389                } catch (Throwable throwable) {
390                    throwable.printStackTrace();
391                }
392
393            }
394        });
395        mLayoutManager.waitForLayout(2);
396        getInstrumentation().waitForIdleSync();
397        assertEquals("on notify data set changed, predictive animations should not run",
398                layoutCount + 1, mLayoutManager.mTotalLayoutCount);
399        mLayoutManager.expectLayouts(2);
400        mTestAdapter.addAndNotify(4, 2);
401        // make sure animations recover
402        mLayoutManager.waitForLayout(2);
403    }
404
405    public void testStableIdNotifyDataSetChanged() throws Throwable {
406        final int itemCount = 20;
407        List<Item> initialSet = new ArrayList<Item>();
408        final TestAdapter adapter = new TestAdapter(itemCount) {
409            @Override
410            public long getItemId(int position) {
411                return mItems.get(position).mId;
412            }
413        };
414        adapter.setHasStableIds(true);
415        initialSet.addAll(adapter.mItems);
416        positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() {
417            @Override
418            void onRun(TestAdapter testAdapter) throws Throwable {
419                Item item5 = adapter.mItems.get(5);
420                Item item6 = adapter.mItems.get(6);
421                item5.mAdapterIndex = 6;
422                item6.mAdapterIndex = 5;
423                adapter.mItems.remove(5);
424                adapter.mItems.add(6, item5);
425                adapter.dispatchDataSetChanged();
426                //hacky, we support only 1 layout pass
427                mLayoutManager.layoutLatch.countDown();
428            }
429        }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
430                PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8),
431                PositionConstraint.scrap(9, -1, 9));
432        // now mix items.
433    }
434
435
436    public void testGetItemForDeletedView() throws Throwable {
437        getItemForDeletedViewTest(false);
438        getItemForDeletedViewTest(true);
439    }
440
441    public void getItemForDeletedViewTest(boolean stableIds) throws Throwable {
442        final Set<Integer> itemViewTypeQueries = new HashSet<Integer>();
443        final Set<Integer> itemIdQueries = new HashSet<Integer>();
444        TestAdapter adapter = new TestAdapter(10) {
445            @Override
446            public int getItemViewType(int position) {
447                itemViewTypeQueries.add(position);
448                return super.getItemViewType(position);
449            }
450
451            @Override
452            public long getItemId(int position) {
453                itemIdQueries.add(position);
454                return mItems.get(position).mId;
455            }
456        };
457        adapter.setHasStableIds(stableIds);
458        setupBasic(10, 0, 10, adapter);
459        assertEquals("getItemViewType for all items should be called", 10,
460                itemViewTypeQueries.size());
461        if (adapter.hasStableIds()) {
462            assertEquals("getItemId should be called when adapter has stable ids", 10,
463                    itemIdQueries.size());
464        } else {
465            assertEquals("getItemId should not be called when adapter does not have stable ids", 0,
466                    itemIdQueries.size());
467        }
468        itemViewTypeQueries.clear();
469        itemIdQueries.clear();
470        mLayoutManager.expectLayouts(2);
471        // delete last two
472        final int deleteStart = 8;
473        final int deleteCount = adapter.getItemCount() - deleteStart;
474        adapter.deleteAndNotify(deleteStart, deleteCount);
475        mLayoutManager.waitForLayout(2);
476        for (int i = 0; i < deleteStart; i++) {
477            assertTrue("getItemViewType for existing item " + i + " should be called",
478                    itemViewTypeQueries.contains(i));
479            if (adapter.hasStableIds()) {
480                assertTrue("getItemId for existing item " + i
481                        + " should be called when adapter has stable ids",
482                        itemIdQueries.contains(i));
483            }
484        }
485        for (int i = deleteStart; i < deleteStart + deleteCount; i++) {
486            assertFalse("getItemViewType for deleted item " + i + " SHOULD NOT be called",
487                    itemViewTypeQueries.contains(i));
488            if (adapter.hasStableIds()) {
489                assertFalse("getItemId for deleted item " + i + " SHOULD NOT be called",
490                        itemIdQueries.contains(i));
491            }
492        }
493    }
494
495    public void testDeleteInvisibleMultiStep() throws Throwable {
496        setupBasic(1000, 1, 7);
497        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
498        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
499        mLayoutManager.expectLayouts(1);
500        // try to trigger race conditions
501        int targetItemCount = mTestAdapter.getItemCount();
502        for (int i = 0; i < 100; i++) {
503            mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});
504            checkForMainThreadException();
505            targetItemCount -= 2;
506        }
507        // wait until main thread runnables are consumed
508        while (targetItemCount != mTestAdapter.getItemCount()) {
509            Thread.sleep(100);
510        }
511        mLayoutManager.waitForLayout(2);
512    }
513
514    public void testAddManyMultiStep() throws Throwable {
515        setupBasic(10, 1, 7);
516        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
517        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
518        mLayoutManager.expectLayouts(1);
519        // try to trigger race conditions
520        int targetItemCount = mTestAdapter.getItemCount();
521        for (int i = 0; i < 100; i++) {
522            mTestAdapter.addAndNotify(0, 1);
523            mTestAdapter.addAndNotify(7, 1);
524            targetItemCount += 2;
525        }
526        // wait until main thread runnables are consumed
527        while (targetItemCount != mTestAdapter.getItemCount()) {
528            Thread.sleep(100);
529        }
530        mLayoutManager.waitForLayout(2);
531    }
532
533    public void testBasicDelete() throws Throwable {
534        setupBasic(10);
535        final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() {
536            @Override
537            public void postDispatchLayout() {
538                // verify this only in first layout
539                assertEquals("deleted views should still be children of RV",
540                        mLayoutManager.getChildCount() + mDeletedViewCount
541                        , mRecyclerView.getChildCount());
542            }
543
544            @Override
545            void afterPreLayout(RecyclerView.Recycler recycler,
546                    AnimationLayoutManager layoutManager,
547                    RecyclerView.State state) {
548                super.afterPreLayout(recycler, layoutManager, state);
549                mLayoutItemCount = 3;
550                mLayoutMin = 0;
551            }
552        };
553        callbacks.mLayoutItemCount = 10;
554        callbacks.setExpectedItemCounts(10, 3);
555        mLayoutManager.setOnLayoutCallbacks(callbacks);
556
557        mLayoutManager.expectLayouts(2);
558        mTestAdapter.deleteAndNotify(0, 7);
559        mLayoutManager.waitForLayout(2);
560        callbacks.reset();// when animations end another layout will happen
561    }
562
563
564    public void testAdapterChangeDuringScrolling() throws Throwable {
565        setupBasic(10);
566        final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
567        final AtomicInteger onScrollItemCount = new AtomicInteger(0);
568
569        mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
570            @Override
571            void onLayoutChildren(RecyclerView.Recycler recycler,
572                    AnimationLayoutManager lm, RecyclerView.State state) {
573                onLayoutItemCount.set(state.getItemCount());
574                super.onLayoutChildren(recycler, lm, state);
575            }
576
577            @Override
578            public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
579                onScrollItemCount.set(state.getItemCount());
580                super.onScroll(dx, recycler, state);
581            }
582        });
583        runTestOnUiThread(new Runnable() {
584            @Override
585            public void run() {
586                mTestAdapter.mItems.remove(5);
587                mTestAdapter.notifyItemRangeRemoved(5, 1);
588                mRecyclerView.scrollBy(0, 100);
589                assertTrue("scrolling while there are pending adapter updates should "
590                        + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
591                assertEquals("scroll by should be called w/ updated adapter count",
592                        mTestAdapter.mItems.size(), onScrollItemCount.get());
593
594            }
595        });
596    }
597
598    public void testNotifyDataSetChangedDuringScroll() throws Throwable {
599        setupBasic(10);
600        final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
601        final AtomicInteger onScrollItemCount = new AtomicInteger(0);
602
603        mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
604            @Override
605            void onLayoutChildren(RecyclerView.Recycler recycler,
606                    AnimationLayoutManager lm, RecyclerView.State state) {
607                onLayoutItemCount.set(state.getItemCount());
608                super.onLayoutChildren(recycler, lm, state);
609            }
610
611            @Override
612            public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
613                onScrollItemCount.set(state.getItemCount());
614                super.onScroll(dx, recycler, state);
615            }
616        });
617        runTestOnUiThread(new Runnable() {
618            @Override
619            public void run() {
620                mTestAdapter.mItems.remove(5);
621                mTestAdapter.notifyDataSetChanged();
622                mRecyclerView.scrollBy(0, 100);
623                assertTrue("scrolling while there are pending adapter updates should "
624                        + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
625                assertEquals("scroll by should be called w/ updated adapter count",
626                        mTestAdapter.mItems.size(), onScrollItemCount.get());
627
628            }
629        });
630    }
631
632    public void testAddInvisibleAndVisible() throws Throwable {
633        setupBasic(10, 1, 7);
634        mLayoutManager.expectLayouts(2);
635        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
636        mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{7, 1});// add a new item 0 // invisible
637        mLayoutManager.waitForLayout(2);
638    }
639
640    public void testAddInvisible() throws Throwable {
641        setupBasic(10, 1, 7);
642        mLayoutManager.expectLayouts(1);
643        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
644        mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{8, 1});// add a new item 0
645        mLayoutManager.waitForLayout(2);
646    }
647
648    public void testBasicAdd() throws Throwable {
649        setupBasic(10);
650        mLayoutManager.expectLayouts(2);
651        setExpectedItemCounts(10, 13);
652        mTestAdapter.addAndNotify(2, 3);
653        mLayoutManager.waitForLayout(2);
654    }
655
656    public TestRecyclerView getTestRecyclerView() {
657        return (TestRecyclerView) mRecyclerView;
658    }
659
660    public void testRemoveScrapInvalidate() throws Throwable {
661        setupBasic(10);
662        TestRecyclerView testRecyclerView = getTestRecyclerView();
663        mLayoutManager.expectLayouts(1);
664        testRecyclerView.expectDraw(1);
665        runTestOnUiThread(new Runnable() {
666            @Override
667            public void run() {
668                mTestAdapter.mItems.clear();
669                mTestAdapter.notifyDataSetChanged();
670            }
671        });
672        mLayoutManager.waitForLayout(2);
673        testRecyclerView.waitForDraw(2);
674    }
675
676    public void testDeleteVisibleAndInvisible() throws Throwable {
677        setupBasic(11, 3, 5); //layout items  3 4 5 6 7
678        mLayoutManager.expectLayouts(2);
679        setLayoutRange(3, 5); //layout previously invisible child 10 from end of the list
680        setExpectedItemCounts(9, 8);
681        mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9
682        mLayoutManager.waitForLayout(2);
683    }
684
685    public void testFindPositionOffset() throws Throwable {
686        setupBasic(10);
687        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
688            @Override
689            void beforePreLayout(RecyclerView.Recycler recycler,
690                    AnimationLayoutManager lm, RecyclerView.State state) {
691                super.beforePreLayout(recycler, lm, state);
692                // [0,2,4]
693                assertEquals("offset check", 0, mAdapterHelper.findPositionOffset(0));
694                assertEquals("offset check", 1, mAdapterHelper.findPositionOffset(2));
695                assertEquals("offset check", 2, mAdapterHelper.findPositionOffset(4));
696            }
697        };
698        runTestOnUiThread(new Runnable() {
699            @Override
700            public void run() {
701                // [0,1,2,3,4]
702                // delete 1
703                mTestAdapter.notifyItemRangeRemoved(1, 1);
704                // delete 3
705                mTestAdapter.notifyItemRangeRemoved(2, 1);
706            }
707        });
708        mLayoutManager.waitForLayout(2);
709    }
710
711    private void setLayoutRange(int start, int count) {
712        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
713        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
714    }
715
716    private void setExpectedItemCounts(int preLayout, int postLayout) {
717        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
718    }
719
720    public void testDeleteInvisible() throws Throwable {
721        setupBasic(10, 1, 7);
722        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
723        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
724        mLayoutManager.expectLayouts(1);
725        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8);
726        mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});// delete item id 0,8
727        mLayoutManager.waitForLayout(2);
728    }
729
730    private CollectPositionResult findByPos(RecyclerView recyclerView,
731            RecyclerView.Recycler recycler, RecyclerView.State state, int position) {
732        View view = recycler.getViewForPosition(position, true);
733        RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
734        if (vh.wasReturnedFromScrap()) {
735            vh.clearReturnedFromScrapFlag(); //keep data consistent.
736            return CollectPositionResult.fromScrap(vh);
737        } else {
738            return CollectPositionResult.fromAdapter(vh);
739        }
740    }
741
742    public Map<Integer, CollectPositionResult> collectPositions(RecyclerView recyclerView,
743            RecyclerView.Recycler recycler, RecyclerView.State state, int... positions) {
744        Map<Integer, CollectPositionResult> positionToAdapterMapping
745                = new HashMap<Integer, CollectPositionResult>();
746        for (int position : positions) {
747            if (position < 0) {
748                continue;
749            }
750            positionToAdapterMapping.put(position,
751                    findByPos(recyclerView, recycler, state, position));
752        }
753        return positionToAdapterMapping;
754    }
755
756    public void testAddDelete2() throws Throwable {
757        positionStatesTest(5, 0, 5, new AdapterOps() {
758            // 0 1 2 3 4
759            // 0 1 2 a b 3 4
760            // 0 1 b 3 4
761            // pre: 0 1 2 3 4
762            // pre w/ adap: 0 1 2 b 3 4
763            @Override
764            void onRun(TestAdapter adapter) throws Throwable {
765                adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
766            }
767        }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
768                PositionConstraint.scrap(3, 3, 3)
769        );
770    }
771
772    public void testAddDelete1() throws Throwable {
773        positionStatesTest(5, 0, 5, new AdapterOps() {
774            // 0 1 2 3 4
775            // 0 1 2 a b 3 4
776            // 0 2 a b 3 4
777            // 0 c d 2 a b 3 4
778            // 0 c d 2 a 4
779            // c d 2 a 4
780            // pre: 0 1 2 3 4
781            @Override
782            void onRun(TestAdapter adapter) throws Throwable {
783                adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
784                        new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
785            }
786        }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
787                PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1),
788                PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0),
789                PositionConstraint.adapter(1), PositionConstraint.adapter(3)
790        );
791    }
792
793    public void testAddSameIndexTwice() throws Throwable {
794        positionStatesTest(12, 2, 7, new AdapterOps() {
795            @Override
796            void onRun(TestAdapter adapter) throws Throwable {
797                adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
798                        new int[]{11, 1});
799            }
800        }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
801                PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7),
802                PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12),
803                PositionConstraint.scrap(8, 8, 13)
804        );
805    }
806
807    public void testDeleteTwice() throws Throwable {
808        positionStatesTest(12, 2, 7, new AdapterOps() {
809            @Override
810            void onRun(TestAdapter adapter) throws Throwable {
811                adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
812                        new int[]{0, 1});// delete item ids 0,2,9,1
813            }
814        }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
815                PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2),
816                PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5),
817                PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7)
818        );
819    }
820
821
822    public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
823            int firstLayoutItemCount, AdapterOps adapterChanges,
824            final PositionConstraint... constraints) throws Throwable {
825        positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null,
826                adapterChanges,  constraints);
827    }
828    public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
829            int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges,
830            final PositionConstraint... constraints) throws Throwable {
831        setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter);
832        mLayoutManager.expectLayouts(2);
833        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
834            @Override
835            void beforePreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
836                    RecyclerView.State state) {
837                super.beforePreLayout(recycler, lm, state);
838                //harmless
839                lm.detachAndScrapAttachedViews(recycler);
840                final int[] ids = new int[constraints.length];
841                for (int i = 0; i < constraints.length; i++) {
842                    ids[i] = constraints[i].mPreLayoutPos;
843                }
844                Map<Integer, CollectPositionResult> positions
845                        = collectPositions(lm.mRecyclerView, recycler, state, ids);
846                for (PositionConstraint constraint : constraints) {
847                    if (constraint.mPreLayoutPos != -1) {
848                        constraint.validate(state, positions.get(constraint.mPreLayoutPos),
849                                lm.getLog());
850                    }
851                }
852            }
853
854            @Override
855            void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
856                    RecyclerView.State state) {
857                super.beforePostLayout(recycler, lm, state);
858                lm.detachAndScrapAttachedViews(recycler);
859                final int[] ids = new int[constraints.length];
860                for (int i = 0; i < constraints.length; i++) {
861                    ids[i] = constraints[i].mPostLayoutPos;
862                }
863                Map<Integer, CollectPositionResult> positions
864                        = collectPositions(lm.mRecyclerView, recycler, state, ids);
865                for (PositionConstraint constraint : constraints) {
866                    if (constraint.mPostLayoutPos >= 0) {
867                        constraint.validate(state, positions.get(constraint.mPostLayoutPos),
868                                lm.getLog());
869                    }
870                }
871            }
872        };
873        adapterChanges.run(mTestAdapter);
874        mLayoutManager.waitForLayout(2);
875        checkForMainThreadException();
876        for (PositionConstraint constraint : constraints) {
877            constraint.assertValidate();
878        }
879    }
880
881    class AnimationLayoutManager extends TestLayoutManager {
882
883        private int mTotalLayoutCount = 0;
884        private String log;
885
886        OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() {
887        };
888
889
890
891        @Override
892        public boolean supportsPredictiveItemAnimations() {
893            return true;
894        }
895
896        public String getLog() {
897            return log;
898        }
899
900        private String prepareLog(RecyclerView.Recycler recycler, RecyclerView.State state, boolean done) {
901            StringBuilder builder = new StringBuilder();
902            builder.append("is pre layout:").append(state.isPreLayout()).append(", done:").append(done);
903            builder.append("\nViewHolders:\n");
904            for (RecyclerView.ViewHolder vh : ((TestRecyclerView)mRecyclerView).collectViewHolders()) {
905                builder.append(vh).append("\n");
906            }
907            builder.append("scrap:\n");
908            for (RecyclerView.ViewHolder vh : recycler.getScrapList()) {
909                builder.append(vh).append("\n");
910            }
911
912            if (state.isPreLayout() && !done) {
913                log = "\n" + builder.toString();
914            } else {
915                log += "\n" + builder.toString();
916            }
917            return log;
918        }
919
920        @Override
921        public void expectLayouts(int count) {
922            super.expectLayouts(count);
923            mOnLayoutCallbacks.mLayoutCount = 0;
924        }
925
926        public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) {
927            mOnLayoutCallbacks = onLayoutCallbacks;
928        }
929
930        @Override
931        public final void onLayoutChildren(RecyclerView.Recycler recycler,
932                RecyclerView.State state) {
933            try {
934                mTotalLayoutCount++;
935                prepareLog(recycler, state, false);
936                if (state.isPreLayout()) {
937                    validateOldPositions(recycler, state);
938                } else {
939                    validateClearedOldPositions(recycler, state);
940                }
941                mOnLayoutCallbacks.onLayoutChildren(recycler, this, state);
942                prepareLog(recycler, state, true);
943            } finally {
944                layoutLatch.countDown();
945            }
946        }
947
948        private void validateClearedOldPositions(RecyclerView.Recycler recycler,
949                RecyclerView.State state) {
950            if (getTestRecyclerView() == null) {
951                return;
952            }
953            for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
954                assertEquals("there should NOT be an old position in post layout",
955                        RecyclerView.NO_POSITION, viewHolder.mOldPosition);
956                assertEquals("there should NOT be a pre layout position in post layout",
957                        RecyclerView.NO_POSITION, viewHolder.mPreLayoutPosition);
958            }
959        }
960
961        private void validateOldPositions(RecyclerView.Recycler recycler,
962                RecyclerView.State state) {
963            if (getTestRecyclerView() == null) {
964                return;
965            }
966            for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
967                if (!viewHolder.isRemoved() && !viewHolder.isInvalid()) {
968                    assertTrue("there should be an old position in pre-layout",
969                            viewHolder.mOldPosition != RecyclerView.NO_POSITION);
970                }
971            }
972        }
973
974        public int getTotalLayoutCount() {
975            return mTotalLayoutCount;
976        }
977
978        @Override
979        public boolean canScrollVertically() {
980            return true;
981        }
982
983        @Override
984        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
985                RecyclerView.State state) {
986            mOnLayoutCallbacks.onScroll(dy, recycler, state);
987            return super.scrollVerticallyBy(dy, recycler, state);
988        }
989
990        public void onPostDispatchLayout() {
991            mOnLayoutCallbacks.postDispatchLayout();
992        }
993
994        @Override
995        public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable {
996            super.waitForLayout(timeout, timeUnit);
997            checkForMainThreadException();
998        }
999    }
1000
1001    abstract class OnLayoutCallbacks {
1002
1003        int mLayoutMin = Integer.MIN_VALUE;
1004
1005        int mLayoutItemCount = Integer.MAX_VALUE;
1006
1007        int expectedPreLayoutItemCount = -1;
1008
1009        int expectedPostLayoutItemCount = -1;
1010
1011        int mDeletedViewCount;
1012
1013        int mLayoutCount = 0;
1014
1015        void setExpectedItemCounts(int preLayout, int postLayout) {
1016            expectedPreLayoutItemCount = preLayout;
1017            expectedPostLayoutItemCount = postLayout;
1018        }
1019
1020        void reset() {
1021            mLayoutMin = Integer.MIN_VALUE;
1022            mLayoutItemCount = Integer.MAX_VALUE;
1023            expectedPreLayoutItemCount = -1;
1024            expectedPostLayoutItemCount = -1;
1025            mLayoutCount = 0;
1026        }
1027
1028        void beforePreLayout(RecyclerView.Recycler recycler,
1029                AnimationLayoutManager lm, RecyclerView.State state) {
1030            mDeletedViewCount = 0;
1031            for (int i = 0; i < lm.getChildCount(); i++) {
1032                View v = lm.getChildAt(i);
1033                if (lm.getLp(v).isItemRemoved()) {
1034                    mDeletedViewCount++;
1035                }
1036            }
1037        }
1038
1039        void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
1040                RecyclerView.State state) {
1041            if (DEBUG) {
1042                Log.d(TAG, "item count " + state.getItemCount());
1043            }
1044            lm.detachAndScrapAttachedViews(recycler);
1045            final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin;
1046            final int count = mLayoutItemCount
1047                    == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount;
1048            lm.layoutRange(recycler, start, start + count);
1049            assertEquals("correct # of children should be laid out",
1050                    count, lm.getChildCount());
1051            lm.assertVisibleItemPositions();
1052        }
1053
1054        private void assertNoPreLayoutPosition(RecyclerView.Recycler recycler) {
1055            for (RecyclerView.ViewHolder vh : recycler.mAttachedScrap) {
1056                assertPreLayoutPosition(vh);
1057            }
1058        }
1059
1060        private void assertNoPreLayoutPosition(RecyclerView.LayoutManager lm) {
1061            for (int i = 0; i < lm.getChildCount(); i ++) {
1062                final RecyclerView.ViewHolder vh = mRecyclerView
1063                        .getChildViewHolder(lm.getChildAt(i));
1064                assertPreLayoutPosition(vh);
1065            }
1066        }
1067
1068        private void assertPreLayoutPosition(RecyclerView.ViewHolder vh) {
1069            assertEquals("in post layout, there should not be a view holder w/ a pre "
1070                    + "layout position", RecyclerView.NO_POSITION, vh.mPreLayoutPosition);
1071            assertEquals("in post layout, there should not be a view holder w/ an old "
1072                    + "layout position", RecyclerView.NO_POSITION, vh.mOldPosition);
1073        }
1074
1075        void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
1076                RecyclerView.State state) {
1077
1078            if (state.isPreLayout()) {
1079                if (expectedPreLayoutItemCount != -1) {
1080                    assertEquals("on pre layout, state should return abstracted adapter size",
1081                            expectedPreLayoutItemCount, state.getItemCount());
1082                }
1083                beforePreLayout(recycler, lm, state);
1084            } else {
1085                if (expectedPostLayoutItemCount != -1) {
1086                    assertEquals("on post layout, state should return real adapter size",
1087                            expectedPostLayoutItemCount, state.getItemCount());
1088                }
1089                beforePostLayout(recycler, lm, state);
1090            }
1091            if (!state.isPreLayout()) {
1092                assertNoPreLayoutPosition(recycler);
1093            }
1094            doLayout(recycler, lm, state);
1095            if (state.isPreLayout()) {
1096                afterPreLayout(recycler, lm, state);
1097            } else {
1098                afterPostLayout(recycler, lm, state);
1099                assertNoPreLayoutPosition(lm);
1100            }
1101            mLayoutCount++;
1102        }
1103
1104        void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
1105                RecyclerView.State state) {
1106        }
1107
1108        void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
1109                RecyclerView.State state) {
1110        }
1111
1112        void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
1113                RecyclerView.State state) {
1114        }
1115
1116        void postDispatchLayout() {
1117        }
1118
1119        public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
1120
1121        }
1122    }
1123
1124    class TestRecyclerView extends RecyclerView {
1125
1126        CountDownLatch drawLatch;
1127
1128        public TestRecyclerView(Context context) {
1129            super(context);
1130        }
1131
1132        public TestRecyclerView(Context context, AttributeSet attrs) {
1133            super(context, attrs);
1134        }
1135
1136        public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) {
1137            super(context, attrs, defStyle);
1138        }
1139
1140        @Override
1141        void initAdapterManager() {
1142            super.initAdapterManager();
1143            mAdapterHelper.mOnItemProcessedCallback = new Runnable() {
1144                @Override
1145                public void run() {
1146                    validatePostUpdateOp();
1147                }
1148            };
1149        }
1150
1151        public void expectDraw(int count) {
1152            drawLatch = new CountDownLatch(count);
1153        }
1154
1155        public void waitForDraw(long timeout) throws Throwable {
1156            drawLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS);
1157            assertEquals("all expected draws should happen at the expected time frame",
1158                    0, drawLatch.getCount());
1159        }
1160
1161        List<ViewHolder> collectViewHolders() {
1162            List<ViewHolder> holders = new ArrayList<ViewHolder>();
1163            final int childCount = getChildCount();
1164            for (int i = 0; i < childCount; i++) {
1165                ViewHolder holder = getChildViewHolderInt(getChildAt(i));
1166                if (holder != null) {
1167                    holders.add(holder);
1168                }
1169            }
1170            return holders;
1171        }
1172
1173
1174        private void validateViewHolderPositions() {
1175            final Set<Integer> existingOffsets = new HashSet<Integer>();
1176            int childCount = getChildCount();
1177            StringBuilder log = new StringBuilder();
1178            for (int i = 0; i < childCount; i++) {
1179                ViewHolder vh = getChildViewHolderInt(getChildAt(i));
1180                TestViewHolder tvh = (TestViewHolder) vh;
1181                log.append(tvh.mBoundItem).append(vh)
1182                        .append(" hidden:")
1183                        .append(mChildHelper.mHiddenViews.contains(vh.itemView))
1184                        .append("\n");
1185            }
1186            for (int i = 0; i < childCount; i++) {
1187                ViewHolder vh = getChildViewHolderInt(getChildAt(i));
1188                if (vh.isInvalid()) {
1189                    continue;
1190                }
1191                if (vh.getLayoutPosition() < 0) {
1192                    LayoutManager lm = getLayoutManager();
1193                    for (int j = 0; j < lm.getChildCount(); j ++) {
1194                        assertNotSame("removed view holder should not be in LM's child list",
1195                                vh.itemView, lm.getChildAt(j));
1196                    }
1197                } else if (!mChildHelper.mHiddenViews.contains(vh.itemView)) {
1198                    if (!existingOffsets.add(vh.getLayoutPosition())) {
1199                        throw new IllegalStateException("view holder position conflict for "
1200                                + "existing views " + vh + "\n" + log);
1201                    }
1202                }
1203            }
1204        }
1205
1206        void validatePostUpdateOp() {
1207            try {
1208                validateViewHolderPositions();
1209                if (super.mState.isPreLayout()) {
1210                    validatePreLayoutSequence((AnimationLayoutManager) getLayoutManager());
1211                }
1212                validateAdapterPosition((AnimationLayoutManager) getLayoutManager());
1213            } catch (Throwable t) {
1214                postExceptionToInstrumentation(t);
1215            }
1216        }
1217
1218
1219
1220        private void validateAdapterPosition(AnimationLayoutManager lm) {
1221            for (ViewHolder vh : collectViewHolders()) {
1222                if (!vh.isRemoved() && vh.mPreLayoutPosition >= 0) {
1223                    assertEquals("adapter position calculations should match view holder "
1224                            + "pre layout:" + mState.isPreLayout()
1225                            + " positions\n" + vh + "\n" + lm.getLog(),
1226                            mAdapterHelper.findPositionOffset(vh.mPreLayoutPosition), vh.mPosition);
1227                }
1228            }
1229        }
1230
1231        // ensures pre layout positions are continuous block. This is not necessarily a case
1232        // but valid in test RV
1233        private void validatePreLayoutSequence(AnimationLayoutManager lm) {
1234            Set<Integer> preLayoutPositions = new HashSet<Integer>();
1235            for (ViewHolder vh : collectViewHolders()) {
1236                assertTrue("pre layout positions should be distinct " + lm.getLog(),
1237                        preLayoutPositions.add(vh.mPreLayoutPosition));
1238            }
1239            int minPos = Integer.MAX_VALUE;
1240            for (Integer pos : preLayoutPositions) {
1241                if (pos < minPos) {
1242                    minPos = pos;
1243                }
1244            }
1245            for (int i = 1; i < preLayoutPositions.size(); i++) {
1246                assertNotNull("next position should exist " + lm.getLog(),
1247                        preLayoutPositions.contains(minPos + i));
1248            }
1249        }
1250
1251        @Override
1252        protected void dispatchDraw(Canvas canvas) {
1253            super.dispatchDraw(canvas);
1254            if (drawLatch != null) {
1255                drawLatch.countDown();
1256            }
1257        }
1258
1259        @Override
1260        void dispatchLayout() {
1261            try {
1262                super.dispatchLayout();
1263                if (getLayoutManager() instanceof AnimationLayoutManager) {
1264                    ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout();
1265                }
1266            } catch (Throwable t) {
1267                postExceptionToInstrumentation(t);
1268            }
1269
1270        }
1271
1272
1273    }
1274
1275    abstract class AdapterOps {
1276
1277        final public void run(TestAdapter adapter) throws Throwable {
1278            onRun(adapter);
1279        }
1280
1281        abstract void onRun(TestAdapter testAdapter) throws Throwable;
1282    }
1283
1284    static class CollectPositionResult {
1285
1286        // true if found in scrap
1287        public RecyclerView.ViewHolder scrapResult;
1288
1289        public RecyclerView.ViewHolder adapterResult;
1290
1291        static CollectPositionResult fromScrap(RecyclerView.ViewHolder viewHolder) {
1292            CollectPositionResult cpr = new CollectPositionResult();
1293            cpr.scrapResult = viewHolder;
1294            return cpr;
1295        }
1296
1297        static CollectPositionResult fromAdapter(RecyclerView.ViewHolder viewHolder) {
1298            CollectPositionResult cpr = new CollectPositionResult();
1299            cpr.adapterResult = viewHolder;
1300            return cpr;
1301        }
1302    }
1303
1304    static class PositionConstraint {
1305
1306        public static enum Type {
1307            scrap,
1308            adapter,
1309            adapterScrap /*first pass adapter, second pass scrap*/
1310        }
1311
1312        Type mType;
1313
1314        int mOldPos; // if VH
1315
1316        int mPreLayoutPos;
1317
1318        int mPostLayoutPos;
1319
1320        int mValidateCount = 0;
1321
1322        public static PositionConstraint scrap(int oldPos, int preLayoutPos, int postLayoutPos) {
1323            PositionConstraint constraint = new PositionConstraint();
1324            constraint.mType = Type.scrap;
1325            constraint.mOldPos = oldPos;
1326            constraint.mPreLayoutPos = preLayoutPos;
1327            constraint.mPostLayoutPos = postLayoutPos;
1328            return constraint;
1329        }
1330
1331        public static PositionConstraint adapterScrap(int preLayoutPos, int position) {
1332            PositionConstraint constraint = new PositionConstraint();
1333            constraint.mType = Type.adapterScrap;
1334            constraint.mOldPos = RecyclerView.NO_POSITION;
1335            constraint.mPreLayoutPos = preLayoutPos;
1336            constraint.mPostLayoutPos = position;// adapter pos does not change
1337            return constraint;
1338        }
1339
1340        public static PositionConstraint adapter(int position) {
1341            PositionConstraint constraint = new PositionConstraint();
1342            constraint.mType = Type.adapter;
1343            constraint.mPreLayoutPos = RecyclerView.NO_POSITION;
1344            constraint.mOldPos = RecyclerView.NO_POSITION;
1345            constraint.mPostLayoutPos = position;// adapter pos does not change
1346            return constraint;
1347        }
1348
1349        public void assertValidate() {
1350            int expectedValidate = 0;
1351            if (mPreLayoutPos >= 0) {
1352                expectedValidate ++;
1353            }
1354            if (mPostLayoutPos >= 0) {
1355                expectedValidate ++;
1356            }
1357            assertEquals("should run all validates", expectedValidate, mValidateCount);
1358        }
1359
1360        @Override
1361        public String toString() {
1362            return "Cons{" +
1363                    "t=" + mType.name() +
1364                    ", old=" + mOldPos +
1365                    ", pre=" + mPreLayoutPos +
1366                    ", post=" + mPostLayoutPos +
1367                    '}';
1368        }
1369
1370        public void validate(RecyclerView.State state, CollectPositionResult result, String log) {
1371            mValidateCount ++;
1372            assertNotNull(this + ": result should not be null\n" + log, result);
1373            RecyclerView.ViewHolder viewHolder;
1374            if (mType == Type.scrap || (mType == Type.adapterScrap && !state.isPreLayout())) {
1375                assertNotNull(this + ": result should come from scrap\n" + log, result.scrapResult);
1376                viewHolder = result.scrapResult;
1377            } else {
1378                assertNotNull(this + ": result should come from adapter\n"  + log,
1379                        result.adapterResult);
1380                assertEquals(this + ": old position should be none when it came from adapter\n" + log,
1381                        RecyclerView.NO_POSITION, result.adapterResult.getOldPosition());
1382                viewHolder = result.adapterResult;
1383            }
1384            if (state.isPreLayout()) {
1385                assertEquals(this + ": pre-layout position should match\n" + log, mPreLayoutPos,
1386                        viewHolder.mPreLayoutPosition == -1 ? viewHolder.mPosition :
1387                        viewHolder.mPreLayoutPosition);
1388                assertEquals(this + ": pre-layout getPosition should match\n" + log, mPreLayoutPos,
1389                        viewHolder.getLayoutPosition());
1390                if (mType == Type.scrap) {
1391                    assertEquals(this + ": old position should match\n" + log, mOldPos,
1392                            result.scrapResult.getOldPosition());
1393                }
1394            } else if (mType == Type.adapter || mType == Type.adapterScrap || !result.scrapResult
1395                    .isRemoved()) {
1396                assertEquals(this + ": post-layout position should match\n" + log + "\n\n"
1397                        + viewHolder, mPostLayoutPos, viewHolder.getLayoutPosition());
1398            }
1399        }
1400    }
1401}
1402