RecyclerViewAnimationsTest.java revision e0c347f627f8a78d3e5e3e5eaac9c3ae26208689
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;
24
25import java.util.ArrayList;
26import java.util.HashMap;
27import java.util.HashSet;
28import java.util.List;
29import java.util.Map;
30import java.util.Set;
31import java.util.concurrent.CountDownLatch;
32import java.util.concurrent.TimeUnit;
33import java.util.concurrent.atomic.AtomicInteger;
34
35public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest {
36
37    private static final boolean DEBUG = false;
38
39    private static final String TAG = "RecyclerViewAnimationsTest";
40
41    AnimationLayoutManager mLayoutManager;
42
43    TestAdapter mTestAdapter;
44
45    public RecyclerViewAnimationsTest() {
46        super(DEBUG);
47    }
48
49    @Override
50    protected void setUp() throws Exception {
51        super.setUp();
52    }
53
54    RecyclerView setupBasic(int itemCount) throws Throwable {
55        return setupBasic(itemCount, 0, itemCount);
56    }
57
58    RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount)
59            throws Throwable {
60        return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null);
61    }
62
63    RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount,
64            TestAdapter testAdapter)
65            throws Throwable {
66        final TestRecyclerView recyclerView = new TestRecyclerView(getActivity());
67        recyclerView.setHasFixedSize(true);
68        if (testAdapter == null) {
69            mTestAdapter = new TestAdapter(itemCount);
70        } else {
71            mTestAdapter = testAdapter;
72        }
73        recyclerView.setAdapter(mTestAdapter);
74        mLayoutManager = new AnimationLayoutManager();
75        recyclerView.setLayoutManager(mLayoutManager);
76        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex;
77        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount;
78
79        mLayoutManager.expectLayouts(1);
80        recyclerView.expectDraw(1);
81        setRecyclerView(recyclerView);
82        mLayoutManager.waitForLayout(2);
83        recyclerView.waitForDraw(1);
84        mLayoutManager.mOnLayoutCallbacks.reset();
85        getInstrumentation().waitForIdleSync();
86        assertEquals("extra layouts should not happen", 1, mLayoutManager.getTotalLayoutCount());
87        assertEquals("all expected children should be laid out", firstLayoutItemCount,
88                mLayoutManager.getChildCount());
89        return recyclerView;
90    }
91
92    public void testNotifyDataSetChanged() throws Throwable {
93        setupBasic(10, 3, 4);
94        int layoutCount = mLayoutManager.mTotalLayoutCount;
95        mLayoutManager.expectLayouts(1);
96        runTestOnUiThread(new Runnable() {
97            @Override
98            public void run() {
99                try {
100                    mTestAdapter.deleteAndNotify(4, 1);
101                    mTestAdapter.notifyChange();
102                } catch (Throwable throwable) {
103                    throwable.printStackTrace();
104                }
105
106            }
107        });
108        mLayoutManager.waitForLayout(2);
109        getInstrumentation().waitForIdleSync();
110        assertEquals("on notify data set changed, predictive animations should not run",
111                layoutCount + 1, mLayoutManager.mTotalLayoutCount);
112        mLayoutManager.expectLayouts(2);
113        mTestAdapter.addAndNotify(4, 2);
114        // make sure animations recover
115        mLayoutManager.waitForLayout(2);
116    }
117
118    public void testStableIdNotifyDataSetChanged() throws Throwable {
119        final int itemCount = 20;
120        List<Item> initialSet = new ArrayList<Item>();
121        final TestAdapter adapter = new TestAdapter(itemCount) {
122            @Override
123            public long getItemId(int position) {
124                return mItems.get(position).mId;
125            }
126        };
127        adapter.setHasStableIds(true);
128        initialSet.addAll(adapter.mItems);
129        positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() {
130            @Override
131            void onRun(TestAdapter testAdapter) throws Throwable {
132                Item item5 = adapter.mItems.get(5);
133                Item item6 = adapter.mItems.get(6);
134                item5.mAdapterIndex = 6;
135                item6.mAdapterIndex = 5;
136                adapter.mItems.remove(5);
137                adapter.mItems.add(6, item5);
138                adapter.notifyChange();
139                //hacky, we support only 1 layout pass
140                mLayoutManager.layoutLatch.countDown();
141            }
142        }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
143                PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8),
144                PositionConstraint.scrap(9, -1, 9));
145        // now mix items.
146    }
147
148
149    public void testGetItemForDeletedView() throws Throwable {
150        getItemForDeletedViewTest(false);
151        getItemForDeletedViewTest(true);
152    }
153
154    public void getItemForDeletedViewTest(boolean stableIds) throws Throwable {
155        final Set<Integer> itemViewTypeQueries = new HashSet<Integer>();
156        final Set<Integer> itemIdQueries = new HashSet<Integer>();
157        TestAdapter adapter = new TestAdapter(10) {
158            @Override
159            public int getItemViewType(int position) {
160                itemViewTypeQueries.add(position);
161                return super.getItemViewType(position);
162            }
163
164            @Override
165            public long getItemId(int position) {
166                itemIdQueries.add(position);
167                return mItems.get(position).mId;
168            }
169        };
170        adapter.setHasStableIds(stableIds);
171        setupBasic(10, 0, 10, adapter);
172        assertEquals("getItemViewType for all items should be called", 10,
173                itemViewTypeQueries.size());
174        if (adapter.hasStableIds()) {
175            assertEquals("getItemId should be called when adapter has stable ids", 10,
176                    itemIdQueries.size());
177        } else {
178            assertEquals("getItemId should not be called when adapter does not have stable ids", 0,
179                    itemIdQueries.size());
180        }
181        itemViewTypeQueries.clear();
182        itemIdQueries.clear();
183        mLayoutManager.expectLayouts(2);
184        // delete last two
185        final int deleteStart = 8;
186        final int deleteCount = adapter.getItemCount() - deleteStart;
187        adapter.deleteAndNotify(deleteStart, deleteCount);
188        mLayoutManager.waitForLayout(2);
189        for (int i = 0; i < deleteStart; i++) {
190            assertTrue("getItemViewType for existing item " + i + " should be called",
191                    itemViewTypeQueries.contains(i));
192            if (adapter.hasStableIds()) {
193                assertTrue("getItemId for existing item " + i
194                        + " should be called when adapter has stable ids",
195                        itemIdQueries.contains(i));
196            }
197        }
198        for (int i = deleteStart; i < deleteStart + deleteCount; i++) {
199            assertFalse("getItemViewType for deleted item " + i + " SHOULD NOT be called",
200                    itemViewTypeQueries.contains(i));
201            if (adapter.hasStableIds()) {
202                assertFalse("getItemId for deleted item " + i + " SHOULD NOT be called",
203                        itemIdQueries.contains(i));
204            }
205        }
206    }
207
208    public void testDeleteInvisibleMultiStep() throws Throwable {
209        setupBasic(1000, 1, 7);
210        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
211        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
212        mLayoutManager.expectLayouts(1);
213        // try to trigger race conditions
214        int targetItemCount = mTestAdapter.getItemCount();
215        for (int i = 0; i < 100; i++) {
216            mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});
217            targetItemCount -= 2;
218        }
219        // wait until main thread runnables are consumed
220        while (targetItemCount != mTestAdapter.getItemCount()) {
221            Thread.sleep(100);
222        }
223        mLayoutManager.waitForLayout(2);
224    }
225
226    public void testAddManyMultiStep() throws Throwable {
227        setupBasic(10, 1, 7);
228        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
229        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
230        mLayoutManager.expectLayouts(1);
231        // try to trigger race conditions
232        int targetItemCount = mTestAdapter.getItemCount();
233        for (int i = 0; i < 100; i++) {
234            mTestAdapter.addAndNotify(0, 1);
235            mTestAdapter.addAndNotify(7, 1);
236            targetItemCount += 2;
237        }
238        // wait until main thread runnables are consumed
239        while (targetItemCount != mTestAdapter.getItemCount()) {
240            Thread.sleep(100);
241        }
242        mLayoutManager.waitForLayout(2);
243    }
244
245    public void testBasicDelete() throws Throwable {
246        setupBasic(10);
247        final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() {
248            @Override
249            public void postDispatchLayout() {
250                // verify this only in first layout
251                assertEquals("deleted views should still be children of RV",
252                        mLayoutManager.getChildCount() + mDeletedViewCount
253                        , mRecyclerView.getChildCount());
254            }
255
256            @Override
257            void afterPreLayout(RecyclerView.Recycler recycler,
258                    AnimationLayoutManager layoutManager,
259                    RecyclerView.State state) {
260                super.afterPreLayout(recycler, layoutManager, state);
261                mLayoutItemCount = 3;
262                mLayoutMin = 0;
263            }
264        };
265        callbacks.mLayoutItemCount = 10;
266        callbacks.setExpectedItemCounts(10, 3);
267        mLayoutManager.setOnLayoutCallbacks(callbacks);
268
269        mLayoutManager.expectLayouts(2);
270        mTestAdapter.deleteAndNotify(0, 7);
271        mLayoutManager.waitForLayout(2);
272        callbacks.reset();// when animations end another layout will happen
273    }
274
275
276    public void testAdapterChangeDuringScrolling() throws Throwable {
277        setupBasic(10);
278        final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
279        final AtomicInteger onScrollItemCount = new AtomicInteger(0);
280
281        mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
282            @Override
283            void onLayoutChildren(RecyclerView.Recycler recycler,
284                    AnimationLayoutManager lm, RecyclerView.State state) {
285                onLayoutItemCount.set(state.getItemCount());
286                super.onLayoutChildren(recycler, lm, state);
287            }
288
289            @Override
290            public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
291                onScrollItemCount.set(state.getItemCount());
292                super.onScroll(dx, recycler, state);
293            }
294        });
295        runTestOnUiThread(new Runnable() {
296            @Override
297            public void run() {
298                mTestAdapter.mItems.remove(5);
299                mTestAdapter.notifyItemRangeRemoved(5, 1);
300                mRecyclerView.scrollBy(0, 100);
301                assertTrue("scrolling while there are pending adapter updates should "
302                        + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
303                assertEquals("scroll by should be called w/ updated adapter count",
304                        mTestAdapter.mItems.size(), onScrollItemCount.get());
305
306            }
307        });
308    }
309
310    public void testAddInvisibleAndVisible() throws Throwable {
311        setupBasic(10, 1, 7);
312        mLayoutManager.expectLayouts(2);
313        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
314        mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{7, 1});// add a new item 0 // invisible
315        mLayoutManager.waitForLayout(2);
316    }
317
318    public void testAddInvisible() throws Throwable {
319        setupBasic(10, 1, 7);
320        mLayoutManager.expectLayouts(1);
321        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
322        mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{8, 1});// add a new item 0
323        mLayoutManager.waitForLayout(2);
324    }
325
326    public void testBasicAdd() throws Throwable {
327        setupBasic(10);
328        mLayoutManager.expectLayouts(2);
329        setExpectedItemCounts(10, 13);
330        mTestAdapter.addAndNotify(2, 3);
331        mLayoutManager.waitForLayout(2);
332    }
333
334    public TestRecyclerView getTestRecyclerView() {
335        return (TestRecyclerView) mRecyclerView;
336    }
337
338    public void testRemoveScrapInvalidate() throws Throwable {
339        setupBasic(10);
340        TestRecyclerView testRecyclerView = getTestRecyclerView();
341        mLayoutManager.expectLayouts(1);
342        testRecyclerView.expectDraw(1);
343        runTestOnUiThread(new Runnable() {
344            @Override
345            public void run() {
346                mTestAdapter.mItems.clear();
347                mTestAdapter.notifyDataSetChanged();
348            }
349        });
350        mLayoutManager.waitForLayout(2);
351        testRecyclerView.waitForDraw(2);
352    }
353
354    public void testDeleteVisibleAndInvisible() throws Throwable {
355        setupBasic(11, 3, 5); //layout items  3 4 5 6 7
356        mLayoutManager.expectLayouts(2);
357        setLayoutRange(3, 5); //layout previously invisible child 10 from end of the list
358        setExpectedItemCounts(9, 8);
359        mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9
360        mLayoutManager.waitForLayout(2);
361    }
362
363    public void testFindPositionOffset() throws Throwable {
364        setupBasic(10);
365        runTestOnUiThread(new Runnable() {
366            @Override
367            public void run() {
368                // [0,1,2,3,4]
369                // delete 1
370                mTestAdapter.notifyItemRangeRemoved(1, 1);
371                // delete 3
372                mTestAdapter.notifyItemRangeRemoved(2, 1);
373                mAdapterHelper.preProcess();
374                // [0,2,4]
375                assertEquals("offset check", 0, mAdapterHelper.findPositionOffset(0));
376                assertEquals("offset check", 1, mAdapterHelper.findPositionOffset(2));
377                assertEquals("offset check", 2, mAdapterHelper.findPositionOffset(4));
378
379            }
380        });
381    }
382
383    private void setLayoutRange(int start, int count) {
384        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
385        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
386    }
387
388    private void setExpectedItemCounts(int preLayout, int postLayout) {
389        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
390    }
391
392    public void testDeleteInvisible() throws Throwable {
393        setupBasic(10, 1, 7);
394        mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
395        mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
396        mLayoutManager.expectLayouts(1);
397        mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8);
398        mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});// delete item id 0,8
399        mLayoutManager.waitForLayout(2);
400    }
401
402    private CollectPositionResult findByPos(RecyclerView recyclerView,
403            RecyclerView.Recycler recycler, RecyclerView.State state, int position) {
404        View view = recycler.getViewForPosition(position, true);
405        RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
406        if (vh.wasReturnedFromScrap()) {
407            vh.clearReturnedFromScrapFlag(); //keep data consistent.
408            return CollectPositionResult.fromScrap(vh);
409        } else {
410            return CollectPositionResult.fromAdapter(vh);
411        }
412    }
413
414    public Map<Integer, CollectPositionResult> collectPositions(RecyclerView recyclerView,
415            RecyclerView.Recycler recycler, RecyclerView.State state, int... positions) {
416        Map<Integer, CollectPositionResult> positionToAdapterMapping
417                = new HashMap<Integer, CollectPositionResult>();
418        for (int position : positions) {
419            if (position < 0) {
420                continue;
421            }
422            positionToAdapterMapping.put(position,
423                    findByPos(recyclerView, recycler, state, position));
424        }
425        return positionToAdapterMapping;
426    }
427
428    public void testAddDelete2() throws Throwable {
429        positionStatesTest(5, 0, 5, new AdapterOps() {
430            // 0 1 2 3 4
431            // 0 1 2 a b 3 4
432            // 0 1 b 3 4
433            // pre: 0 1 2 3 4
434            // pre w/ adap: 0 1 2 b 3 4
435            @Override
436            void onRun(TestAdapter adapter) throws Throwable {
437                adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
438            }
439        }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
440                PositionConstraint.scrap(3, 3, 3)
441        );
442    }
443
444    public void testAddDelete1() throws Throwable {
445        positionStatesTest(5, 0, 5, new AdapterOps() {
446            // 0 1 2 3 4
447            // 0 1 2 a b 3 4
448            // 0 2 a b 3 4
449            // 0 c d 2 a b 3 4
450            // 0 c d 2 a 4
451            // c d 2 a 4
452            // pre: 0 1 2 3 4
453            @Override
454            void onRun(TestAdapter adapter) throws Throwable {
455                adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
456                        new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
457            }
458        }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
459                PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1),
460                PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0),
461                PositionConstraint.adapter(1), PositionConstraint.adapter(3)
462        );
463    }
464
465    public void testAddSameIndexTwice() throws Throwable {
466        positionStatesTest(12, 2, 7, new AdapterOps() {
467            @Override
468            void onRun(TestAdapter adapter) throws Throwable {
469                adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
470                        new int[]{11, 1});
471            }
472        }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
473                PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7),
474                PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12),
475                PositionConstraint.scrap(8, 8, 13)
476        );
477    }
478
479    public void testDeleteTwice() throws Throwable {
480        positionStatesTest(12, 2, 7, new AdapterOps() {
481            @Override
482            void onRun(TestAdapter adapter) throws Throwable {
483                adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
484                        new int[]{0, 1});// delete item ids 0,2,9,1
485            }
486        }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
487                PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2),
488                PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5),
489                PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7)
490        );
491    }
492
493
494    public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
495            int firstLayoutItemCount, AdapterOps adapterChanges,
496            final PositionConstraint... constraints) throws Throwable {
497        positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null,
498                adapterChanges,  constraints);
499    }
500    public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
501            int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges,
502            final PositionConstraint... constraints) throws Throwable {
503        setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter);
504        mLayoutManager.expectLayouts(2);
505        mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
506            @Override
507            void beforePreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
508                    RecyclerView.State state) {
509                super.beforePreLayout(recycler, lm, state);
510                //harmless
511                lm.detachAndScrapAttachedViews(recycler);
512                final int[] ids = new int[constraints.length];
513                for (int i = 0; i < constraints.length; i++) {
514                    ids[i] = constraints[i].mPreLayoutPos;
515                }
516                Map<Integer, CollectPositionResult> positions
517                        = collectPositions(lm.mRecyclerView, recycler, state, ids);
518                for (PositionConstraint constraint : constraints) {
519                    if (constraint.mPreLayoutPos != -1) {
520                        constraint.validate(state, positions.get(constraint.mPreLayoutPos),
521                                lm.getLog());
522                    }
523                }
524            }
525
526            @Override
527            void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
528                    RecyclerView.State state) {
529                super.beforePostLayout(recycler, lm, state);
530                lm.detachAndScrapAttachedViews(recycler);
531                final int[] ids = new int[constraints.length];
532                for (int i = 0; i < constraints.length; i++) {
533                    ids[i] = constraints[i].mPostLayoutPos;
534                }
535                Map<Integer, CollectPositionResult> positions
536                        = collectPositions(lm.mRecyclerView, recycler, state, ids);
537                for (PositionConstraint constraint : constraints) {
538                    if (constraint.mPostLayoutPos >= 0) {
539                        constraint.validate(state, positions.get(constraint.mPostLayoutPos),
540                                lm.getLog());
541                    }
542                }
543            }
544        };
545        adapterChanges.run(mTestAdapter);
546        mLayoutManager.waitForLayout(2);
547        checkForMainThreadException();
548        for (PositionConstraint constraint : constraints) {
549            constraint.assertValidate();
550        }
551    }
552
553    class AnimationLayoutManager extends TestLayoutManager {
554
555        private int mTotalLayoutCount = 0;
556        private String log;
557
558        OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() {
559        };
560
561
562
563        @Override
564        public boolean supportsPredictiveItemAnimations() {
565            return true;
566        }
567
568        public String getLog() {
569            return log;
570        }
571
572        private String prepareLog(RecyclerView.Recycler recycler, RecyclerView.State state, boolean done) {
573            StringBuilder builder = new StringBuilder();
574            builder.append("is pre layout:").append(state.isPreLayout()).append(", done:").append(done);
575            builder.append("\nViewHolders:\n");
576            for (RecyclerView.ViewHolder vh : ((TestRecyclerView)mRecyclerView).collectViewHolders()) {
577                builder.append(vh).append("\n");
578            }
579            builder.append("scrap:\n");
580            for (RecyclerView.ViewHolder vh : recycler.getScrapList()) {
581                builder.append(vh).append("\n");
582            }
583
584            if (state.isPreLayout() && !done) {
585                log = "\n" + builder.toString();
586            } else {
587                log += "\n" + builder.toString();
588            }
589            return log;
590        }
591
592        @Override
593        public void expectLayouts(int count) {
594            super.expectLayouts(count);
595            mOnLayoutCallbacks.mLayoutCount = 0;
596        }
597
598        public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) {
599            mOnLayoutCallbacks = onLayoutCallbacks;
600        }
601
602        @Override
603        public final void onLayoutChildren(RecyclerView.Recycler recycler,
604                RecyclerView.State state) {
605            try {
606                mTotalLayoutCount++;
607                prepareLog(recycler, state, false);
608                if (state.isPreLayout()) {
609                    validateOldPositions(recycler, state);
610                } else {
611                    validateClearedOldPositions(recycler, state);
612                }
613                mOnLayoutCallbacks.onLayoutChildren(recycler, this, state);
614                prepareLog(recycler, state, true);
615            } finally {
616                layoutLatch.countDown();
617            }
618        }
619
620        private void validateClearedOldPositions(RecyclerView.Recycler recycler,
621                RecyclerView.State state) {
622            if (getTestRecyclerView() == null) {
623                return;
624            }
625            for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
626                assertEquals("there should NOT be an old position in post layout",
627                        RecyclerView.NO_POSITION, viewHolder.mOldPosition);
628                assertEquals("there should NOT be a pre layout position in post layout",
629                        RecyclerView.NO_POSITION, viewHolder.mPreLayoutPosition);
630            }
631        }
632
633        private void validateOldPositions(RecyclerView.Recycler recycler,
634                RecyclerView.State state) {
635            if (getTestRecyclerView() == null) {
636                return;
637            }
638            for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
639                if (!viewHolder.isRemoved() && !viewHolder.isInvalid()) {
640                    assertTrue("there should be an old position in pre-layout",
641                            viewHolder.mOldPosition != RecyclerView.NO_POSITION);
642                }
643            }
644        }
645
646        public int getTotalLayoutCount() {
647            return mTotalLayoutCount;
648        }
649
650        @Override
651        public boolean canScrollVertically() {
652            return true;
653        }
654
655        @Override
656        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
657                RecyclerView.State state) {
658            mOnLayoutCallbacks.onScroll(dy, recycler, state);
659            return super.scrollVerticallyBy(dy, recycler, state);
660        }
661
662        public void onPostDispatchLayout() {
663            mOnLayoutCallbacks.postDispatchLayout();
664        }
665
666        @Override
667        public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable {
668            super.waitForLayout(timeout, timeUnit);
669            checkForMainThreadException();
670        }
671    }
672
673    abstract class OnLayoutCallbacks {
674
675        int mLayoutMin = Integer.MIN_VALUE;
676
677        int mLayoutItemCount = Integer.MAX_VALUE;
678
679        int expectedPreLayoutItemCount = -1;
680
681        int expectedPostLayoutItemCount = -1;
682
683        int mDeletedViewCount;
684
685        int mLayoutCount = 0;
686
687        void setExpectedItemCounts(int preLayout, int postLayout) {
688            expectedPreLayoutItemCount = preLayout;
689            expectedPostLayoutItemCount = postLayout;
690        }
691
692        void reset() {
693            mLayoutMin = Integer.MIN_VALUE;
694            mLayoutItemCount = Integer.MAX_VALUE;
695            expectedPreLayoutItemCount = -1;
696            expectedPostLayoutItemCount = -1;
697            mLayoutCount = 0;
698        }
699
700        void beforePreLayout(RecyclerView.Recycler recycler,
701                AnimationLayoutManager lm, RecyclerView.State state) {
702            mDeletedViewCount = 0;
703            for (int i = 0; i < lm.getChildCount(); i++) {
704                View v = lm.getChildAt(i);
705                if (lm.getLp(v).isItemRemoved()) {
706                    mDeletedViewCount++;
707                }
708            }
709        }
710
711        void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
712                RecyclerView.State state) {
713            if (DEBUG) {
714                Log.d(TAG, "item count " + state.getItemCount());
715            }
716            lm.detachAndScrapAttachedViews(recycler);
717            final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin;
718            final int count = mLayoutItemCount
719                    == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount;
720            int skippedAdd = lm.layoutRange(recycler, start, start + count);
721            assertEquals("correct # of children should be laid out",
722                    count - skippedAdd, lm.getChildCount());
723            lm.assertVisibleItemPositions();
724        }
725
726        void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
727                RecyclerView.State state) {
728
729            if (state.isPreLayout()) {
730                if (expectedPreLayoutItemCount != -1) {
731                    assertEquals("on pre layout, state should return abstracted adapter size",
732                            expectedPreLayoutItemCount, state.getItemCount());
733                }
734                beforePreLayout(recycler, lm, state);
735            } else {
736                if (expectedPostLayoutItemCount != -1) {
737                    assertEquals("on post layout, state should return real adapter size",
738                            expectedPostLayoutItemCount, state.getItemCount());
739                }
740                beforePostLayout(recycler, lm, state);
741            }
742            doLayout(recycler, lm, state);
743            if (state.isPreLayout()) {
744                afterPreLayout(recycler, lm, state);
745            } else {
746                afterPostLayout(recycler, lm, state);
747            }
748            mLayoutCount++;
749        }
750
751        void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
752                RecyclerView.State state) {
753        }
754
755        void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
756                RecyclerView.State state) {
757        }
758
759        void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager,
760                RecyclerView.State state) {
761        }
762
763        void postDispatchLayout() {
764        }
765
766        public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
767
768        }
769    }
770
771    class TestRecyclerView extends RecyclerView {
772
773        CountDownLatch drawLatch;
774
775        public TestRecyclerView(Context context) {
776            super(context);
777        }
778
779        public TestRecyclerView(Context context, AttributeSet attrs) {
780            super(context, attrs);
781        }
782
783        public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) {
784            super(context, attrs, defStyle);
785        }
786
787        @Override
788        void initAdapterManager() {
789            super.initAdapterManager();
790            mAdapterHelper.mOnItemProcessedCallback = new Runnable() {
791                @Override
792                public void run() {
793                    validatePostUpdateOp();
794                }
795            };
796        }
797
798        public void expectDraw(int count) {
799            drawLatch = new CountDownLatch(count);
800        }
801
802        public void waitForDraw(long timeout) throws Throwable {
803            drawLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS);
804            assertEquals("all expected draws should happen at the expected time frame",
805                    0, drawLatch.getCount());
806        }
807
808        List<ViewHolder> collectViewHolders() {
809            List<ViewHolder> holders = new ArrayList<ViewHolder>();
810            final int childCount = getChildCount();
811            for (int i = 0; i < childCount; i++) {
812                ViewHolder holder = getChildViewHolderInt(getChildAt(i));
813                if (holder != null) {
814                    holders.add(holder);
815                }
816            }
817            return holders;
818        }
819
820
821        private void validateViewHolderPositions() {
822            final Set<Integer> existingOffsets = new HashSet<Integer>();
823            int childCount = getChildCount();
824            StringBuilder log = new StringBuilder();
825            for (int i = 0; i < childCount; i++) {
826                ViewHolder vh = getChildViewHolderInt(getChildAt(i));
827                TestViewHolder tvh = (TestViewHolder) vh;
828                log.append(tvh.mBindedItem).append(vh)
829                        .append(" hidden:")
830                        .append(mChildHelper.mHiddenViews.contains(vh.itemView))
831                        .append("\n");
832            }
833            for (int i = 0; i < childCount; i++) {
834                ViewHolder vh = getChildViewHolderInt(getChildAt(i));
835                if (vh.isInvalid()) {
836                    continue;
837                }
838                if (vh.getPosition() < 0) {
839                    LayoutManager lm = getLayoutManager();
840                    for (int j = 0; j < lm.getChildCount(); j ++) {
841                        assertNotSame("removed view holder should not be in LM's child list",
842                                vh.itemView, lm.getChildAt(j));
843                    }
844                } else if (!mChildHelper.mHiddenViews.contains(vh.itemView)) {
845                    if (!existingOffsets.add(vh.getPosition())) {
846                        throw new IllegalStateException("view holder position conflict for "
847                                + "existing views " + vh + "\n" + log);
848                    }
849                }
850            }
851        }
852
853        void validatePostUpdateOp() {
854            try {
855                validateViewHolderPositions();
856                if (super.mState.isPreLayout()) {
857                    validatePreLayoutSequence((AnimationLayoutManager) getLayoutManager());
858                }
859                validateAdapterPosition((AnimationLayoutManager) getLayoutManager());
860            } catch (Throwable t) {
861                postExceptionToInstrumentation(t);
862            }
863        }
864
865
866
867        private void validateAdapterPosition(AnimationLayoutManager lm) {
868            for (ViewHolder vh : collectViewHolders()) {
869                if (!vh.isRemoved() && vh.mPreLayoutPosition >= 0) {
870                    assertEquals("adapter position calculations should match view holder "
871                            + "pre layout:" + mState.isPreLayout()
872                            + " positions\n" + vh + "\n" + lm.getLog(),
873                            mAdapterHelper.findPositionOffset(vh.mPreLayoutPosition), vh.mPosition);
874                }
875            }
876        }
877
878        // ensures pre layout positions are continuous block. This is not necessarily a case
879        // but valid in test RV
880        private void validatePreLayoutSequence(AnimationLayoutManager lm) {
881            Set<Integer> preLayoutPositions = new HashSet<Integer>();
882            for (ViewHolder vh : collectViewHolders()) {
883                assertTrue("pre layout positions should be distinct " + lm.getLog(),
884                        preLayoutPositions.add(vh.mPreLayoutPosition));
885            }
886            int minPos = Integer.MAX_VALUE;
887            for (Integer pos : preLayoutPositions) {
888                if (pos < minPos) {
889                    minPos = pos;
890                }
891            }
892            for (int i = 1; i < preLayoutPositions.size(); i++) {
893                assertNotNull("next position should exist " + lm.getLog(),
894                        preLayoutPositions.contains(minPos + i));
895            }
896        }
897
898        @Override
899        protected void dispatchDraw(Canvas canvas) {
900            super.dispatchDraw(canvas);
901            if (drawLatch != null) {
902                drawLatch.countDown();
903            }
904        }
905
906        @Override
907        void dispatchLayout() {
908            try {
909                super.dispatchLayout();
910                if (getLayoutManager() instanceof AnimationLayoutManager) {
911                    ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout();
912                }
913            } catch (Throwable t) {
914                postExceptionToInstrumentation(t);
915            }
916
917        }
918
919
920    }
921
922    abstract class AdapterOps {
923
924        final public void run(TestAdapter adapter) throws Throwable {
925            onRun(adapter);
926        }
927
928        abstract void onRun(TestAdapter testAdapter) throws Throwable;
929    }
930
931    static class CollectPositionResult {
932
933        // true if found in scrap
934        public RecyclerView.ViewHolder scrapResult;
935
936        public RecyclerView.ViewHolder adapterResult;
937
938        static CollectPositionResult fromScrap(RecyclerView.ViewHolder viewHolder) {
939            CollectPositionResult cpr = new CollectPositionResult();
940            cpr.scrapResult = viewHolder;
941            return cpr;
942        }
943
944        static CollectPositionResult fromAdapter(RecyclerView.ViewHolder viewHolder) {
945            CollectPositionResult cpr = new CollectPositionResult();
946            cpr.adapterResult = viewHolder;
947            return cpr;
948        }
949    }
950
951    static class PositionConstraint {
952
953        public static enum Type {
954            scrap,
955            adapter,
956            adapterScrap /*first pass adapter, second pass scrap*/
957        }
958
959        Type mType;
960
961        int mOldPos; // if VH
962
963        int mPreLayoutPos;
964
965        int mPostLayoutPos;
966
967        int mValidateCount = 0;
968
969        public static PositionConstraint scrap(int oldPos, int preLayoutPos, int postLayoutPos) {
970            PositionConstraint constraint = new PositionConstraint();
971            constraint.mType = Type.scrap;
972            constraint.mOldPos = oldPos;
973            constraint.mPreLayoutPos = preLayoutPos;
974            constraint.mPostLayoutPos = postLayoutPos;
975            return constraint;
976        }
977
978        public static PositionConstraint adapterScrap(int preLayoutPos, int position) {
979            PositionConstraint constraint = new PositionConstraint();
980            constraint.mType = Type.adapterScrap;
981            constraint.mOldPos = RecyclerView.NO_POSITION;
982            constraint.mPreLayoutPos = preLayoutPos;
983            constraint.mPostLayoutPos = position;// adapter pos does not change
984            return constraint;
985        }
986
987        public static PositionConstraint adapter(int position) {
988            PositionConstraint constraint = new PositionConstraint();
989            constraint.mType = Type.adapter;
990            constraint.mPreLayoutPos = RecyclerView.NO_POSITION;
991            constraint.mOldPos = RecyclerView.NO_POSITION;
992            constraint.mPostLayoutPos = position;// adapter pos does not change
993            return constraint;
994        }
995
996        public void assertValidate() {
997            int expectedValidate = 0;
998            if (mPreLayoutPos >= 0) {
999                expectedValidate ++;
1000            }
1001            if (mPostLayoutPos >= 0) {
1002                expectedValidate ++;
1003            }
1004            assertEquals("should run all validates", expectedValidate, mValidateCount);
1005        }
1006
1007        @Override
1008        public String toString() {
1009            return "Cons{" +
1010                    "t=" + mType.name() +
1011                    ", old=" + mOldPos +
1012                    ", pre=" + mPreLayoutPos +
1013                    ", post=" + mPostLayoutPos +
1014                    '}';
1015        }
1016
1017        public void validate(RecyclerView.State state, CollectPositionResult result, String log) {
1018            mValidateCount ++;
1019            assertNotNull(this + ": result should not be null\n" + log, result);
1020            RecyclerView.ViewHolder viewHolder;
1021            if (mType == Type.scrap || (mType == Type.adapterScrap && !state.isPreLayout())) {
1022                assertNotNull(this + ": result should come from scrap\n" + log, result.scrapResult);
1023                viewHolder = result.scrapResult;
1024            } else {
1025                assertNotNull(this + ": result should come from adapter\n"  + log,
1026                        result.adapterResult);
1027                assertEquals(this + ": old position should be none when it came from adapter\n" + log,
1028                        RecyclerView.NO_POSITION, result.adapterResult.getOldPosition());
1029                viewHolder = result.adapterResult;
1030            }
1031            if (state.isPreLayout()) {
1032                assertEquals(this + ": pre-layout position should match\n" + log, mPreLayoutPos,
1033                        viewHolder.mPreLayoutPosition == -1 ? viewHolder.mPosition :
1034                        viewHolder.mPreLayoutPosition);
1035                assertEquals(this + ": pre-layout getPosition should match\n" + log, mPreLayoutPos,
1036                        viewHolder.getPosition());
1037                if (mType == Type.scrap) {
1038                    assertEquals(this + ": old position should match\n" + log, mOldPos,
1039                            result.scrapResult.getOldPosition());
1040                }
1041            } else if (mType == Type.adapter || mType == Type.adapterScrap || !result.scrapResult
1042                    .isRemoved()) {
1043                assertEquals(this + ": post-layout position should match\n" + log + "\n\n"
1044                        + viewHolder, mPostLayoutPos, viewHolder.getPosition());
1045            }
1046        }
1047    }
1048}
1049