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