RecyclerViewLayoutTest.java revision ee30f03253207f694cc46063b0c8c7cb91d24a6e
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
17
18package android.support.v7.widget;
19
20import android.graphics.PointF;
21import android.support.v4.view.ViewCompat;
22import android.util.Log;
23import android.view.View;
24import android.view.ViewGroup;
25import android.widget.TextView;
26
27import java.util.ArrayList;
28import java.util.HashMap;
29import java.util.List;
30import java.util.Map;
31import java.util.concurrent.CountDownLatch;
32import java.util.concurrent.TimeUnit;
33import java.util.concurrent.atomic.AtomicBoolean;
34import java.util.concurrent.atomic.AtomicInteger;
35
36public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest {
37
38    private static final boolean DEBUG = false;
39
40    private static final String TAG = "RecyclerViewLayoutTest";
41
42    public RecyclerViewLayoutTest() {
43        super(DEBUG);
44    }
45
46    public void testAccessRecyclerOnOnMeasure() throws Throwable {
47        accessRecyclerOnOnMeasureTest(false);
48        removeRecyclerView();
49        accessRecyclerOnOnMeasureTest(true);
50    }
51
52    public void testSmoothScrollWithRemovedItems() throws Throwable {
53        smoothScrollTest(false);
54        removeRecyclerView();
55        smoothScrollTest(true);
56    }
57
58    public void smoothScrollTest(final boolean removeItem) throws Throwable {
59        final LinearSmoothScroller[] lss = new LinearSmoothScroller[1];
60        final CountDownLatch calledOnStart = new CountDownLatch(1);
61        final CountDownLatch calledOnStop = new CountDownLatch(1);
62        final int visibleChildCount = 10;
63        TestLayoutManager lm = new TestLayoutManager() {
64            int start = 0;
65
66            @Override
67            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
68                super.onLayoutChildren(recycler, state);
69                layoutRange(recycler, start, visibleChildCount);
70                layoutLatch.countDown();
71            }
72
73            @Override
74            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
75                    RecyclerView.State state) {
76                start++;
77                if (DEBUG) {
78                    Log.d(TAG, "on scroll, remove and recycling. start:" + start + ", cnt:"
79                            + visibleChildCount);
80                }
81                removeAndRecycleAllViews(recycler);
82                layoutRange(recycler, start, start + visibleChildCount);
83                return dy;
84            }
85
86            @Override
87            public boolean canScrollVertically() {
88                return true;
89            }
90
91            @Override
92            public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
93                    int position) {
94                LinearSmoothScroller linearSmoothScroller =
95                        new LinearSmoothScroller(recyclerView.getContext()) {
96                            @Override
97                            public PointF computeScrollVectorForPosition(int targetPosition) {
98                                return new PointF(0, 1);
99                            }
100
101                            @Override
102                            protected void onStart() {
103                                super.onStart();
104                                calledOnStart.countDown();
105                            }
106
107                            @Override
108                            protected void onStop() {
109                                super.onStop();
110                                calledOnStop.countDown();
111                            }
112                        };
113                linearSmoothScroller.setTargetPosition(position);
114                lss[0] = linearSmoothScroller;
115                startSmoothScroll(linearSmoothScroller);
116            }
117        };
118        final RecyclerView rv = new RecyclerView(getActivity());
119        TestAdapter testAdapter = new TestAdapter(500);
120        rv.setLayoutManager(lm);
121        rv.setAdapter(testAdapter);
122        lm.expectLayouts(1);
123        setRecyclerView(rv);
124        lm.waitForLayout(1);
125        // regular scroll
126        final int targetPosition = visibleChildCount * (removeItem ? 30 : 4);
127        runTestOnUiThread(new Runnable() {
128            @Override
129            public void run() {
130                rv.smoothScrollToPosition(targetPosition);
131            }
132        });
133        if (DEBUG) {
134            Log.d(TAG, "scrolling to target position " + targetPosition);
135        }
136        assertTrue("on start should be called very soon", calledOnStart.await(2, TimeUnit.SECONDS));
137        if (removeItem) {
138            final int newTarget = targetPosition - 10;
139            testAdapter.deleteAndNotify(newTarget + 1, testAdapter.getItemCount() - newTarget - 1);
140            runTestOnUiThread(new Runnable() {
141                @Override
142                public void run() {
143                    ViewCompat.postOnAnimationDelayed(rv, new Runnable() {
144                        @Override
145                        public void run() {
146                            try {
147                                assertEquals("scroll position should be updated to next available",
148                                        newTarget, lss[0].getTargetPosition());
149                            } catch (Throwable t) {
150                                postExceptionToInstrumentation(t);
151                            }
152                        }
153                    }, 200);
154                }
155            });
156            checkForMainThreadException();
157            assertTrue("on stop should be called", calledOnStop.await(30, TimeUnit.SECONDS));
158            checkForMainThreadException();
159            assertNotNull("should scroll to new target " + newTarget
160                    , rv.findViewHolderForPosition(newTarget));
161            if (DEBUG) {
162                Log.d(TAG, "on stop has been called on time");
163            }
164        } else {
165            assertTrue("on stop should be called eventually",
166                    calledOnStop.await(30, TimeUnit.SECONDS));
167            assertNotNull("scroll to position should succeed",
168                    rv.findViewHolderForPosition(targetPosition));
169        }
170        checkForMainThreadException();
171    }
172
173    public void accessRecyclerOnOnMeasureTest(final boolean enablePredictiveAnimations)
174            throws Throwable {
175        TestAdapter testAdapter = new TestAdapter(10);
176        final AtomicInteger expectedOnMeasureStateCount = new AtomicInteger(10);
177        TestLayoutManager lm = new TestLayoutManager() {
178            @Override
179            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
180                super.onLayoutChildren(recycler, state);
181                try {
182                    layoutRange(recycler, 0, state.getItemCount());
183                    layoutLatch.countDown();
184                } catch (Throwable t) {
185                    postExceptionToInstrumentation(t);
186                } finally {
187                    layoutLatch.countDown();
188                }
189            }
190
191            @Override
192            public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
193                    int widthSpec, int heightSpec) {
194                try {
195                    // make sure we access all views
196                    for (int i = 0; i < state.getItemCount(); i++) {
197                        View view = recycler.getViewForPosition(i);
198                        assertNotNull(view);
199                        assertEquals(i, getPosition(view));
200                    }
201                    assertEquals(state.toString(),
202                            expectedOnMeasureStateCount.get(), state.getItemCount());
203                } catch(Throwable t) {
204                    postExceptionToInstrumentation(t);
205                }
206                super.onMeasure(recycler, state, widthSpec, heightSpec);
207            }
208
209            @Override
210            public boolean supportsPredictiveItemAnimations() {
211                return enablePredictiveAnimations;
212            }
213        };
214        RecyclerView recyclerView = new RecyclerView(getActivity());
215        recyclerView.setLayoutManager(lm);
216        recyclerView.setAdapter(testAdapter);
217        recyclerView.setLayoutManager(lm);
218        lm.expectLayouts(1);
219        setRecyclerView(recyclerView);
220        lm.waitForLayout(2);
221        checkForMainThreadException();
222        lm.expectLayouts(1);
223        if (!enablePredictiveAnimations) {
224            expectedOnMeasureStateCount.set(15);
225        }
226        testAdapter.addAndNotify(4, 5);
227        lm.waitForLayout(2);
228        checkForMainThreadException();
229    }
230
231    public void testSetCompatibleAdapter() throws Throwable {
232        compatibleAdapterTest(true, true);
233        removeRecyclerView();
234        compatibleAdapterTest(false, true);
235        removeRecyclerView();
236        compatibleAdapterTest(true, false);
237        removeRecyclerView();
238        compatibleAdapterTest(false, false);
239        removeRecyclerView();
240    }
241
242    private void compatibleAdapterTest(boolean useCustomPool, boolean removeAndRecycleExistingViews)
243            throws Throwable {
244        TestAdapter testAdapter = new TestAdapter(10);
245        final AtomicInteger recycledViewCount = new AtomicInteger();
246        TestLayoutManager lm = new TestLayoutManager() {
247            @Override
248            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
249                super.onLayoutChildren(recycler, state);
250                try {
251                    layoutRange(recycler, 0, state.getItemCount());
252                    layoutLatch.countDown();
253                } catch (Throwable t) {
254                    postExceptionToInstrumentation(t);
255                } finally {
256                    layoutLatch.countDown();
257                }
258            }
259        };
260        RecyclerView recyclerView = new RecyclerView(getActivity());
261        recyclerView.setLayoutManager(lm);
262        recyclerView.setAdapter(testAdapter);
263        recyclerView.setLayoutManager(lm);
264        recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() {
265            @Override
266            public void onViewRecycled(RecyclerView.ViewHolder holder) {
267                recycledViewCount.incrementAndGet();
268            }
269        });
270        lm.expectLayouts(1);
271        setRecyclerView(recyclerView, !useCustomPool);
272        lm.waitForLayout(2);
273        checkForMainThreadException();
274        lm.expectLayouts(1);
275        swapAdapter(new TestAdapter(10), removeAndRecycleExistingViews);
276        lm.waitForLayout(2);
277        checkForMainThreadException();
278        if (removeAndRecycleExistingViews) {
279            assertTrue("Previous views should be recycled", recycledViewCount.get() > 0);
280        } else {
281            assertEquals("No views should be recycled if adapters are compatible and developer "
282                    + "did not request a recycle", 0, recycledViewCount.get());
283        }
284    }
285
286    public void testSetIncompatibleAdapter() throws Throwable {
287        incompatibleAdapterTest(true);
288        incompatibleAdapterTest(false);
289    }
290
291    public void incompatibleAdapterTest(boolean useCustomPool) throws Throwable {
292        TestAdapter testAdapter = new TestAdapter(10);
293        TestLayoutManager lm = new TestLayoutManager() {
294            @Override
295            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
296                super.onLayoutChildren(recycler, state);
297                try {
298                    layoutRange(recycler, 0, state.getItemCount());
299                    layoutLatch.countDown();
300                } catch (Throwable t) {
301                    postExceptionToInstrumentation(t);
302                } finally {
303                    layoutLatch.countDown();
304                }
305            }
306        };
307        RecyclerView recyclerView = new RecyclerView(getActivity());
308        recyclerView.setLayoutManager(lm);
309        recyclerView.setAdapter(testAdapter);
310        recyclerView.setLayoutManager(lm);
311        lm.expectLayouts(1);
312        setRecyclerView(recyclerView, !useCustomPool);
313        lm.waitForLayout(2);
314        checkForMainThreadException();
315        lm.expectLayouts(1);
316        setAdapter(new TestAdapter2(10));
317        lm.waitForLayout(2);
318        checkForMainThreadException();
319    }
320
321    public void testRecycleIgnored() throws Throwable {
322        final TestAdapter adapter = new TestAdapter(10);
323        final TestLayoutManager lm = new TestLayoutManager() {
324            @Override
325            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
326                layoutRange(recycler, 0, 5);
327                layoutLatch.countDown();
328            }
329        };
330        final RecyclerView recyclerView = new RecyclerView(getActivity());
331        recyclerView.setAdapter(adapter);
332        recyclerView.setLayoutManager(lm);
333        lm.expectLayouts(1);
334        setRecyclerView(recyclerView);
335        lm.waitForLayout(2);
336        runTestOnUiThread(new Runnable() {
337            @Override
338            public void run() {
339                View child1 = lm.findViewByPosition(0);
340                View child2 = lm.findViewByPosition(1);
341                lm.ignoreView(child1);
342                lm.ignoreView(child2);
343
344                lm.removeAndRecycleAllViews(recyclerView.mRecycler);
345                assertEquals("ignored child should not be recycled or removed", 2,
346                        lm.getChildCount());
347
348                Throwable[] throwables = new Throwable[1];
349                try {
350                    lm.removeAndRecycleView(child1, mRecyclerView.mRecycler);
351                } catch (Throwable t) {
352                    throwables[0] = t;
353                }
354                assertTrue("Trying to recycle an ignored view should throw IllegalArgException "
355                        , throwables[0] instanceof IllegalArgumentException);
356                lm.removeAllViews();
357                assertEquals("ignored child should be removed as well ", 0, lm.getChildCount());
358            }
359        });
360    }
361
362    public void testInvalidateAllDecorOffsets() throws Throwable {
363        final TestAdapter adapter = new TestAdapter(10);
364        final RecyclerView recyclerView = new RecyclerView(getActivity());
365        final AtomicBoolean invalidatedOffsets = new AtomicBoolean(true);
366        recyclerView.setAdapter(adapter);
367        final AtomicInteger layoutCount = new AtomicInteger(4);
368        final RecyclerView.ItemDecoration dummyItemDecoration = new RecyclerView.ItemDecoration() {
369        };
370        TestLayoutManager testLayoutManager = new TestLayoutManager() {
371            @Override
372            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
373                try {
374                    // test
375                    for (int i = 0; i < getChildCount(); i ++) {
376                        View child = getChildAt(i);
377                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
378                                child.getLayoutParams();
379                        assertEquals(
380                                "Decor insets validation for VH should have expected value.",
381                                invalidatedOffsets.get(), lp.mInsetsDirty);
382                    }
383                    for (RecyclerView.ViewHolder vh : mRecyclerView.mRecycler.mCachedViews) {
384                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
385                                vh.itemView.getLayoutParams();
386                        assertEquals(
387                                "Decor insets invalidation in cache for VH should have expected "
388                                        + "value.",
389                                invalidatedOffsets.get(), lp.mInsetsDirty);
390                    }
391                    detachAndScrapAttachedViews(recycler);
392                    layoutRange(recycler, 0, layoutCount.get());
393                } catch (Throwable t) {
394                    postExceptionToInstrumentation(t);
395                } finally {
396                    layoutLatch.countDown();
397                }
398            }
399
400            @Override
401            public boolean supportsPredictiveItemAnimations() {
402                return false;
403            }
404        };
405        // first layout
406        recyclerView.setItemViewCacheSize(5);
407        recyclerView.setLayoutManager(testLayoutManager);
408        testLayoutManager.expectLayouts(1);
409        setRecyclerView(recyclerView);
410        testLayoutManager.waitForLayout(2);
411        checkForMainThreadException();
412
413        // re-layout w/o any change
414        invalidatedOffsets.set(false);
415        testLayoutManager.expectLayouts(1);
416        requestLayoutOnUIThread(recyclerView);
417        testLayoutManager.waitForLayout(1);
418        checkForMainThreadException();
419
420        // invalidate w/o an item decorator
421        invalidateDecorOffsets(recyclerView);
422        testLayoutManager.expectLayouts(1);
423        invalidateDecorOffsets(recyclerView);
424        testLayoutManager.assertNoLayout("layout should not happen", 2);
425        checkForMainThreadException();
426
427        // set item decorator, should invalidate
428        invalidatedOffsets.set(true);
429        testLayoutManager.expectLayouts(1);
430        addItemDecoration(mRecyclerView, dummyItemDecoration);
431        testLayoutManager.waitForLayout(1);
432        checkForMainThreadException();
433
434        // re-layout w/o any change
435        invalidatedOffsets.set(false);
436        testLayoutManager.expectLayouts(1);
437        requestLayoutOnUIThread(recyclerView);
438        testLayoutManager.waitForLayout(1);
439        checkForMainThreadException();
440
441        // invalidate w/ item decorator
442        invalidatedOffsets.set(true);
443        invalidateDecorOffsets(recyclerView);
444        testLayoutManager.expectLayouts(1);
445        invalidateDecorOffsets(recyclerView);
446        testLayoutManager.waitForLayout(2);
447        checkForMainThreadException();
448
449        // trigger cache.
450        layoutCount.set(3);
451        invalidatedOffsets.set(false);
452        testLayoutManager.expectLayouts(1);
453        requestLayoutOnUIThread(mRecyclerView);
454        testLayoutManager.waitForLayout(1);
455        checkForMainThreadException();
456        assertEquals("a view should be cached", 1, mRecyclerView.mRecycler.mCachedViews.size());
457
458        layoutCount.set(5);
459        invalidatedOffsets.set(true);
460        testLayoutManager.expectLayouts(1);
461        invalidateDecorOffsets(recyclerView);
462        testLayoutManager.waitForLayout(1);
463        checkForMainThreadException();
464
465        // remove item decorator
466        invalidatedOffsets.set(true);
467        testLayoutManager.expectLayouts(1);
468        removeItemDecoration(mRecyclerView, dummyItemDecoration);
469        testLayoutManager.waitForLayout(1);
470        checkForMainThreadException();
471    }
472
473    public void addItemDecoration(final RecyclerView recyclerView, final
474            RecyclerView.ItemDecoration itemDecoration) throws Throwable {
475        runTestOnUiThread(new Runnable() {
476            @Override
477            public void run() {
478                recyclerView.addItemDecoration(itemDecoration);
479            }
480        });
481    }
482
483    public void removeItemDecoration(final RecyclerView recyclerView, final
484    RecyclerView.ItemDecoration itemDecoration) throws Throwable {
485        runTestOnUiThread(new Runnable() {
486            @Override
487            public void run() {
488                recyclerView.removeItemDecoration(itemDecoration);
489            }
490        });
491    }
492
493    public void invalidateDecorOffsets(final RecyclerView recyclerView) throws Throwable {
494        runTestOnUiThread(new Runnable() {
495            @Override
496            public void run() {
497                recyclerView.invalidateItemDecorations();
498            }
499        });
500    }
501
502    public void testInvalidateDecorOffsets() throws Throwable {
503        final TestAdapter adapter = new TestAdapter(10);
504        adapter.setHasStableIds(true);
505        final RecyclerView recyclerView = new RecyclerView(getActivity());
506        recyclerView.setAdapter(adapter);
507
508        final Map<Long, Boolean> changes = new HashMap<Long, Boolean>();
509
510        TestLayoutManager testLayoutManager = new TestLayoutManager() {
511            @Override
512            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
513                try {
514                    if (changes.size() > 0) {
515                        // test
516                        for (int i = 0; i < getChildCount(); i ++) {
517                            View child = getChildAt(i);
518                            RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
519                                    child.getLayoutParams();
520                            RecyclerView.ViewHolder vh = lp.mViewHolder;
521                            if (!changes.containsKey(vh.getItemId())) {
522                                continue; //nothing to test
523                            }
524                            assertEquals(
525                                    "Decord insets validation for VH should have expected value.",
526                                    changes.get(vh.getItemId()).booleanValue(),
527                                    lp.mInsetsDirty);
528                        }
529                    }
530                    detachAndScrapAttachedViews(recycler);
531                    layoutRange(recycler, 0, state.getItemCount());
532                } catch (Throwable t) {
533                    postExceptionToInstrumentation(t);
534                } finally {
535                    layoutLatch.countDown();
536                }
537            }
538
539            @Override
540            public boolean supportsPredictiveItemAnimations() {
541                return false;
542            }
543        };
544        recyclerView.setLayoutManager(testLayoutManager);
545        testLayoutManager.expectLayouts(1);
546        setRecyclerView(recyclerView);
547        testLayoutManager.waitForLayout(2);
548        int itemAddedTo = 5;
549        for (int i = 0; i < itemAddedTo; i++) {
550            changes.put(mRecyclerView.findViewHolderForPosition(i).getItemId(), false);
551        }
552        for (int i = itemAddedTo; i < mRecyclerView.getChildCount(); i++) {
553            changes.put(mRecyclerView.findViewHolderForPosition(i).getItemId(), true);
554        }
555        testLayoutManager.expectLayouts(1);
556        adapter.addAndNotify(5, 1);
557        testLayoutManager.waitForLayout(2);
558        checkForMainThreadException();
559
560        changes.clear();
561        int[] changedItems = new int[]{3, 5, 6};
562        for (int i = 0; i < adapter.getItemCount(); i ++) {
563            changes.put(mRecyclerView.findViewHolderForPosition(i).getItemId(), false);
564        }
565        for (int i = 0; i < changedItems.length; i ++) {
566            changes.put(mRecyclerView.findViewHolderForPosition(changedItems[i]).getItemId(), true);
567        }
568        testLayoutManager.expectLayouts(1);
569        adapter.changePositionsAndNotify(changedItems);
570        testLayoutManager.waitForLayout(2);
571        checkForMainThreadException();
572
573        for (int i = 0; i < adapter.getItemCount(); i ++) {
574            changes.put(mRecyclerView.findViewHolderForPosition(i).getItemId(), true);
575        }
576        testLayoutManager.expectLayouts(1);
577        adapter.dispatchDataSetChanged();
578        testLayoutManager.waitForLayout(2);
579        checkForMainThreadException();
580    }
581
582    public void testMovingViaStableIds() throws Throwable {
583        stableIdsMoveTest(true);
584        removeRecyclerView();
585        stableIdsMoveTest(false);
586        removeRecyclerView();
587    }
588
589    public void stableIdsMoveTest(final boolean supportsPredictive) throws Throwable {
590        final TestAdapter testAdapter = new TestAdapter(10);
591        testAdapter.setHasStableIds(true);
592        final AtomicBoolean test = new AtomicBoolean(false);
593        final int movedViewFromIndex = 3;
594        final int movedViewToIndex = 6;
595        final View[] movedView = new View[1];
596        TestLayoutManager lm = new TestLayoutManager() {
597            @Override
598            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
599                detachAndScrapAttachedViews(recycler);
600                try {
601                    if (test.get()) {
602                        if (state.isPreLayout()) {
603                            View view = recycler.getViewForPosition(movedViewFromIndex, true);
604                            assertSame("In pre layout, should be able to get moved view w/ old "
605                                    + "position", movedView[0], view);
606                            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
607                            assertTrue("it should come from scrap", holder.wasReturnedFromScrap());
608                            // clear scrap flag
609                            holder.clearReturnedFromScrapFlag();
610                        } else {
611                            View view = recycler.getViewForPosition(movedViewToIndex, true);
612                            assertSame("In post layout, should be able to get moved view w/ new "
613                                    + "position", movedView[0], view);
614                            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
615                            assertTrue("it should come from scrap", holder.wasReturnedFromScrap());
616                            // clear scrap flag
617                            holder.clearReturnedFromScrapFlag();
618                        }
619                    }
620                    layoutRange(recycler, 0, state.getItemCount());
621                } catch (Throwable t) {
622                    postExceptionToInstrumentation(t);
623                } finally {
624                    layoutLatch.countDown();
625                }
626
627
628            }
629
630            @Override
631            public boolean supportsPredictiveItemAnimations() {
632                return supportsPredictive;
633            }
634        };
635        RecyclerView recyclerView = new RecyclerView(this.getActivity());
636        recyclerView.setAdapter(testAdapter);
637        recyclerView.setLayoutManager(lm);
638        lm.expectLayouts(1);
639        setRecyclerView(recyclerView);
640        lm.waitForLayout(1);
641
642        movedView[0] = recyclerView.getChildAt(movedViewFromIndex);
643        test.set(true);
644        lm.expectLayouts(supportsPredictive ? 2 : 1);
645        runTestOnUiThread(new Runnable() {
646            @Override
647            public void run() {
648                Item item = testAdapter.mItems.remove(movedViewFromIndex);
649                testAdapter.mItems.add(movedViewToIndex, item);
650                testAdapter.notifyItemRemoved(movedViewFromIndex);
651                testAdapter.notifyItemInserted(movedViewToIndex);
652            }
653        });
654        lm.waitForLayout(2);
655        checkForMainThreadException();
656    }
657
658    public void testAdapterChangeDuringLayout() throws Throwable {
659        adapterChangeInMainThreadTest("notifyDataSetChanged", new Runnable() {
660            @Override
661            public void run() {
662                mRecyclerView.getAdapter().notifyDataSetChanged();
663            }
664        });
665
666        adapterChangeInMainThreadTest("notifyItemChanged", new Runnable() {
667            @Override
668            public void run() {
669                mRecyclerView.getAdapter().notifyItemChanged(2);
670            }
671        });
672
673        adapterChangeInMainThreadTest("notifyItemInserted", new Runnable() {
674            @Override
675            public void run() {
676                mRecyclerView.getAdapter().notifyItemInserted(2);
677            }
678        });
679        adapterChangeInMainThreadTest("notifyItemRemoved", new Runnable() {
680            @Override
681            public void run() {
682                mRecyclerView.getAdapter().notifyItemRemoved(2);
683            }
684        });
685    }
686
687    public void adapterChangeInMainThreadTest(String msg,
688            final Runnable onLayoutRunnable) throws Throwable {
689        final AtomicBoolean doneFirstLayout = new AtomicBoolean(false);
690        TestAdapter testAdapter = new TestAdapter(10);
691        TestLayoutManager lm = new TestLayoutManager() {
692            @Override
693            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
694                super.onLayoutChildren(recycler, state);
695                try {
696                    layoutRange(recycler, 0, state.getItemCount());
697                    if (doneFirstLayout.get()) {
698                        onLayoutRunnable.run();
699                    }
700                } catch (Throwable t) {
701                    postExceptionToInstrumentation(t);
702                } finally {
703                    layoutLatch.countDown();
704                }
705
706            }
707        };
708        RecyclerView recyclerView = new RecyclerView(getActivity());
709        recyclerView.setLayoutManager(lm);
710        recyclerView.setAdapter(testAdapter);
711        lm.expectLayouts(1);
712        setRecyclerView(recyclerView);
713        lm.waitForLayout(2);
714        doneFirstLayout.set(true);
715        lm.expectLayouts(1);
716        requestLayoutOnUIThread(recyclerView);
717        lm.waitForLayout(2);
718        removeRecyclerView();
719        assertTrue("Invalid data updates should be caught:" + msg,
720                mainThreadException instanceof IllegalStateException);
721    }
722
723    public void testAdapterChangeDuringScroll() throws Throwable {
724        for (int orientation : new int[]{OrientationHelper.HORIZONTAL,
725                OrientationHelper.VERTICAL}) {
726            adapterChangeDuringScrollTest("notifyDataSetChanged", orientation,
727                    new Runnable() {
728                        @Override
729                        public void run() {
730                            mRecyclerView.getAdapter().notifyDataSetChanged();
731                        }
732                    });
733            adapterChangeDuringScrollTest("notifyItemChanged", orientation, new Runnable() {
734                @Override
735                public void run() {
736                    mRecyclerView.getAdapter().notifyItemChanged(2);
737                }
738            });
739
740            adapterChangeDuringScrollTest("notifyItemInserted", orientation, new Runnable() {
741                @Override
742                public void run() {
743                    mRecyclerView.getAdapter().notifyItemInserted(2);
744                }
745            });
746            adapterChangeDuringScrollTest("notifyItemRemoved", orientation, new Runnable() {
747                @Override
748                public void run() {
749                    mRecyclerView.getAdapter().notifyItemRemoved(2);
750                }
751            });
752        }
753    }
754
755    public void adapterChangeDuringScrollTest(String msg, final int orientation,
756            final Runnable onScrollRunnable) throws Throwable {
757        TestAdapter testAdapter = new TestAdapter(100);
758        TestLayoutManager lm = new TestLayoutManager() {
759            @Override
760            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
761                super.onLayoutChildren(recycler, state);
762                try {
763                    layoutRange(recycler, 0, 10);
764                } catch (Throwable t) {
765                    postExceptionToInstrumentation(t);
766                } finally {
767                    layoutLatch.countDown();
768                }
769            }
770
771            @Override
772            public boolean canScrollVertically() {
773                return orientation == OrientationHelper.VERTICAL;
774            }
775
776            @Override
777            public boolean canScrollHorizontally() {
778                return orientation == OrientationHelper.HORIZONTAL;
779            }
780
781            public int mockScroll() {
782                try {
783                    onScrollRunnable.run();
784                } catch (Throwable t) {
785                    postExceptionToInstrumentation(t);
786                } finally {
787                    layoutLatch.countDown();
788                }
789                return 0;
790            }
791
792            @Override
793            public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
794                    RecyclerView.State state) {
795                return mockScroll();
796            }
797
798            @Override
799            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
800                    RecyclerView.State state) {
801                return mockScroll();
802            }
803        };
804        RecyclerView recyclerView = new RecyclerView(getActivity());
805        recyclerView.setLayoutManager(lm);
806        recyclerView.setAdapter(testAdapter);
807        lm.expectLayouts(1);
808        setRecyclerView(recyclerView);
809        lm.waitForLayout(2);
810        lm.expectLayouts(1);
811        scrollBy(200);
812        lm.waitForLayout(2);
813        removeRecyclerView();
814        assertTrue("Invalid data updates should be caught:" + msg,
815                mainThreadException instanceof IllegalStateException);
816    }
817
818    public void testRecycleOnDetach() throws Throwable {
819        final RecyclerView recyclerView = new RecyclerView(getActivity());
820        final TestAdapter testAdapter = new TestAdapter(10);
821        final AtomicBoolean didRunOnDetach = new AtomicBoolean(false);
822        final TestLayoutManager lm = new TestLayoutManager() {
823            @Override
824            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
825                super.onLayoutChildren(recycler, state);
826                layoutRange(recycler, 0, state.getItemCount() - 1);
827                layoutLatch.countDown();
828            }
829
830            @Override
831            public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
832                super.onDetachedFromWindow(view, recycler);
833                didRunOnDetach.set(true);
834                removeAndRecycleAllViews(recycler);
835            }
836        };
837        recyclerView.setAdapter(testAdapter);
838        recyclerView.setLayoutManager(lm);
839        lm.expectLayouts(1);
840        setRecyclerView(recyclerView);
841        lm.waitForLayout(2);
842        removeRecyclerView();
843        assertTrue("When recycler view is removed, detach should run", didRunOnDetach.get());
844        assertEquals("All children should be recycled", recyclerView.getChildCount(), 0);
845    }
846
847    public void testUpdatesWhileDetached() throws Throwable {
848        final RecyclerView recyclerView = new RecyclerView(getActivity());
849        final int initialAdapterSize = 20;
850        final TestAdapter adapter = new TestAdapter(initialAdapterSize);
851        final AtomicInteger layoutCount = new AtomicInteger(0);
852        TestLayoutManager lm = new TestLayoutManager() {
853            @Override
854            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
855                super.onLayoutChildren(recycler, state);
856                layoutRange(recycler, 0, 5);
857                layoutCount.incrementAndGet();
858                layoutLatch.countDown();
859            }
860        };
861        recyclerView.setAdapter(adapter);
862        recyclerView.setLayoutManager(lm);
863        recyclerView.setHasFixedSize(true);
864        lm.expectLayouts(1);
865        adapter.addAndNotify(4, 5);
866        lm.assertNoLayout("When RV is not attached, layout should not happen", 1);
867    }
868
869    public void testUpdatesAfterDetach() throws Throwable {
870        final RecyclerView recyclerView = new RecyclerView(getActivity());
871        final int initialAdapterSize = 20;
872        final TestAdapter adapter = new TestAdapter(initialAdapterSize);
873        final AtomicInteger layoutCount = new AtomicInteger(0);
874        TestLayoutManager lm = new TestLayoutManager() {
875            @Override
876            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
877                super.onLayoutChildren(recycler, state);
878                layoutRange(recycler, 0, 5);
879                layoutCount.incrementAndGet();
880                layoutLatch.countDown();
881            }
882        };
883        recyclerView.setAdapter(adapter);
884        recyclerView.setLayoutManager(lm);
885        lm.expectLayouts(1);
886        recyclerView.setHasFixedSize(true);
887        setRecyclerView(recyclerView);
888        lm.waitForLayout(2);
889        lm.expectLayouts(1);
890        final int prevLayoutCount = layoutCount.get();
891        runTestOnUiThread(new Runnable() {
892            @Override
893            public void run() {
894                try {
895                    adapter.addAndNotify(4, 5);
896                    removeRecyclerView();
897                } catch (Throwable throwable) {
898                    throwable.printStackTrace();
899                }
900            }
901        });
902
903        lm.assertNoLayout("When RV is not attached, layout should not happen", 1);
904        assertEquals("No extra layout should happen when detached", prevLayoutCount,
905                layoutCount.get());
906    }
907
908    public void testNotifyDataSetChangedWithStableIds() throws Throwable {
909        final int defaultViewType = 1;
910        final Map<Item, Integer> viewTypeMap = new HashMap<Item, Integer>();
911        final Map<Integer, Integer> oldPositionToNewPositionMapping =
912                new HashMap<Integer, Integer>();
913        final TestAdapter adapter = new TestAdapter(100) {
914            @Override
915            public int getItemViewType(int position) {
916                Integer type = viewTypeMap.get(mItems.get(position));
917                return type == null ? defaultViewType : type;
918            }
919
920            @Override
921            public long getItemId(int position) {
922                return mItems.get(position).mId;
923            }
924        };
925        adapter.setHasStableIds(true);
926        final ArrayList<Item> previousItems = new ArrayList<Item>();
927        previousItems.addAll(adapter.mItems);
928
929        final AtomicInteger layoutStart = new AtomicInteger(50);
930        final AtomicBoolean validate = new AtomicBoolean(false);
931        final int childCount = 10;
932        final TestLayoutManager lm = new TestLayoutManager() {
933            @Override
934            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
935                try {
936                    super.onLayoutChildren(recycler, state);
937                    if (validate.get()) {
938                        assertEquals("Cached views should be kept", 5, recycler
939                                .mCachedViews.size());
940                        for (RecyclerView.ViewHolder vh : recycler.mCachedViews) {
941                            TestViewHolder tvh = (TestViewHolder) vh;
942                            assertTrue("view holder should be marked for update",
943                                    tvh.needsUpdate());
944                            assertTrue("view holder should be marked as invalid", tvh.isInvalid());
945                        }
946                    }
947                    detachAndScrapAttachedViews(recycler);
948                    if (validate.get()) {
949                        assertEquals("cache size should stay the same", 5,
950                                recycler.mCachedViews.size());
951                        assertEquals("all views should be scrapped", childCount,
952                                recycler.getScrapList().size());
953                        for (RecyclerView.ViewHolder vh : recycler.getScrapList()) {
954                            // TODO create test case for type change
955                            TestViewHolder tvh = (TestViewHolder) vh;
956                            assertTrue("view holder should be marked for update",
957                                    tvh.needsUpdate());
958                            assertTrue("view holder should be marked as invalid", tvh.isInvalid());
959                        }
960                    }
961                    layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount);
962                    if (validate.get()) {
963                        for (int i = 0; i < getChildCount(); i++) {
964                            View view = getChildAt(i);
965                            TestViewHolder tvh = (TestViewHolder) mRecyclerView
966                                    .getChildViewHolder(view);
967                            final int oldPos = previousItems.indexOf(tvh.mBindedItem);
968                            assertEquals("view holder's position should be correct",
969                                    oldPositionToNewPositionMapping.get(oldPos).intValue(),
970                                    tvh.getPosition());
971                            ;
972                        }
973                    }
974                } catch (Throwable t) {
975                    postExceptionToInstrumentation(t);
976                } finally {
977                    layoutLatch.countDown();
978                }
979            }
980        };
981        final RecyclerView recyclerView = new RecyclerView(getActivity());
982        recyclerView.setItemAnimator(null);
983        recyclerView.setAdapter(adapter);
984        recyclerView.setLayoutManager(lm);
985        recyclerView.setItemViewCacheSize(10);
986        lm.expectLayouts(1);
987        setRecyclerView(recyclerView);
988        lm.waitForLayout(2);
989        checkForMainThreadException();
990        getInstrumentation().waitForIdleSync();
991        layoutStart.set(layoutStart.get() + 5);//55
992        lm.expectLayouts(1);
993        requestLayoutOnUIThread(recyclerView);
994        lm.waitForLayout(2);
995        validate.set(true);
996        lm.expectLayouts(1);
997        runTestOnUiThread(new Runnable() {
998            @Override
999            public void run() {
1000                try {
1001                    adapter.moveItems(false,
1002                            new int[]{50, 56}, new int[]{51, 1}, new int[]{52, 2},
1003                            new int[]{53, 54}, new int[]{60, 61}, new int[]{62, 64},
1004                            new int[]{75, 58});
1005                    for (int i = 0; i < previousItems.size(); i++) {
1006                        Item item = previousItems.get(i);
1007                        oldPositionToNewPositionMapping.put(i, adapter.mItems.indexOf(item));
1008                    }
1009                    adapter.dispatchDataSetChanged();
1010                } catch (Throwable throwable) {
1011                    postExceptionToInstrumentation(throwable);
1012                }
1013            }
1014        });
1015        lm.waitForLayout(2);
1016        checkForMainThreadException();
1017    }
1018
1019    public void testFindViewById() throws Throwable {
1020        findViewByIdTest(false);
1021        removeRecyclerView();
1022        findViewByIdTest(true);
1023    }
1024
1025    public void findViewByIdTest(final boolean supportPredictive) throws Throwable {
1026        final RecyclerView recyclerView = new RecyclerView(getActivity());
1027        final int initialAdapterSize = 20;
1028        final TestAdapter adapter = new TestAdapter(initialAdapterSize);
1029        final int deleteStart = 6;
1030        final int deleteCount = 5;
1031        recyclerView.setAdapter(adapter);
1032        final AtomicBoolean assertPositions = new AtomicBoolean(false);
1033        TestLayoutManager lm = new TestLayoutManager() {
1034            @Override
1035            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1036                super.onLayoutChildren(recycler, state);
1037                if (assertPositions.get()) {
1038                    if (state.isPreLayout()) {
1039                        for (int i = 0; i < deleteStart; i++) {
1040                            View view = findViewByPosition(i);
1041                            assertNotNull("find view by position for existing items should work "
1042                                    + "fine", view);
1043                            assertFalse("view should not be marked as removed",
1044                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
1045                                            .isItemRemoved());
1046                        }
1047                        for (int i = 0; i < deleteCount; i++) {
1048                            View view = findViewByPosition(i + deleteStart);
1049                            assertNotNull("find view by position should work fine for removed "
1050                                    + "views in pre-layout", view);
1051                            assertTrue("view should be marked as removed",
1052                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
1053                                            .isItemRemoved());
1054                        }
1055                        for (int i = deleteStart + deleteCount; i < 20; i++) {
1056                            View view = findViewByPosition(i);
1057                            assertNotNull(view);
1058                            assertFalse("view should not be marked as removed",
1059                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
1060                                            .isItemRemoved());
1061                        }
1062                    } else {
1063                        for (int i = 0; i < initialAdapterSize - deleteCount; i++) {
1064                            View view = findViewByPosition(i);
1065                            assertNotNull("find view by position for existing item " + i +
1066                                    " should work fine. child count:" + getChildCount(), view);
1067                            TestViewHolder viewHolder =
1068                                    (TestViewHolder) mRecyclerView.getChildViewHolder(view);
1069                            assertSame("should be the correct item " + viewHolder
1070                                    , viewHolder.mBindedItem,
1071                                    adapter.mItems.get(viewHolder.mPosition));
1072                            assertFalse("view should not be marked as removed",
1073                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
1074                                            .isItemRemoved());
1075                        }
1076                    }
1077                }
1078                detachAndScrapAttachedViews(recycler);
1079                layoutRange(recycler, state.getItemCount() - 1, -1);
1080                layoutLatch.countDown();
1081            }
1082
1083            @Override
1084            public boolean supportsPredictiveItemAnimations() {
1085                return supportPredictive;
1086            }
1087        };
1088        recyclerView.setLayoutManager(lm);
1089        lm.expectLayouts(1);
1090        setRecyclerView(recyclerView);
1091        lm.waitForLayout(2);
1092        getInstrumentation().waitForIdleSync();
1093
1094        assertPositions.set(true);
1095        lm.expectLayouts(supportPredictive ? 2 : 1);
1096        adapter.deleteAndNotify(new int[]{deleteStart, deleteCount - 1}, new int[]{deleteStart, 1});
1097        lm.waitForLayout(2);
1098    }
1099
1100    public void testTypeForCache() throws Throwable {
1101        final AtomicInteger viewType = new AtomicInteger(1);
1102        final TestAdapter adapter = new TestAdapter(100) {
1103            @Override
1104            public int getItemViewType(int position) {
1105                return viewType.get();
1106            }
1107
1108            @Override
1109            public long getItemId(int position) {
1110                return mItems.get(position).mId;
1111            }
1112        };
1113        adapter.setHasStableIds(true);
1114        final AtomicInteger layoutStart = new AtomicInteger(2);
1115        final int childCount = 10;
1116        final TestLayoutManager lm = new TestLayoutManager() {
1117            @Override
1118            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1119                super.onLayoutChildren(recycler, state);
1120                detachAndScrapAttachedViews(recycler);
1121                layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount);
1122                layoutLatch.countDown();
1123            }
1124        };
1125        final RecyclerView recyclerView = new RecyclerView(getActivity());
1126        recyclerView.setItemAnimator(null);
1127        recyclerView.setAdapter(adapter);
1128        recyclerView.setLayoutManager(lm);
1129        recyclerView.setItemViewCacheSize(10);
1130        lm.expectLayouts(1);
1131        setRecyclerView(recyclerView);
1132        lm.waitForLayout(2);
1133        getInstrumentation().waitForIdleSync();
1134        layoutStart.set(4); // trigger a cache for 3,4
1135        lm.expectLayouts(1);
1136        requestLayoutOnUIThread(recyclerView);
1137        lm.waitForLayout(2);
1138        //
1139        viewType.incrementAndGet();
1140        layoutStart.set(2); // go back to bring views from cache
1141        lm.expectLayouts(1);
1142        adapter.mItems.remove(1);
1143        adapter.dispatchDataSetChanged();
1144        lm.waitForLayout(2);
1145        runTestOnUiThread(new Runnable() {
1146            @Override
1147            public void run() {
1148                for (int i = 2; i < 4; i++) {
1149                    RecyclerView.ViewHolder vh = recyclerView.findViewHolderForPosition(i);
1150                    assertEquals("View holder's type should match latest type", viewType.get(),
1151                            vh.getItemViewType());
1152                }
1153            }
1154        });
1155    }
1156
1157    public void testTypeForExistingViews() throws Throwable {
1158        final AtomicInteger viewType = new AtomicInteger(1);
1159        final int invalidatedCount = 2;
1160        final int layoutStart = 2;
1161        final TestAdapter adapter = new TestAdapter(100) {
1162            @Override
1163            public int getItemViewType(int position) {
1164                return viewType.get();
1165            }
1166
1167            @Override
1168            public void onBindViewHolder(TestViewHolder holder,
1169                    int position) {
1170                super.onBindViewHolder(holder, position);
1171                if (position >= layoutStart && position < invalidatedCount + layoutStart) {
1172                    try {
1173                        assertEquals("holder type should match current view type at position " +
1174                                position, viewType.get(), holder.getItemViewType());
1175                    } catch (Throwable t) {
1176                        postExceptionToInstrumentation(t);
1177                    }
1178                }
1179            }
1180
1181            @Override
1182            public long getItemId(int position) {
1183                return mItems.get(position).mId;
1184            }
1185        };
1186        adapter.setHasStableIds(true);
1187
1188        final int childCount = 10;
1189        final TestLayoutManager lm = new TestLayoutManager() {
1190            @Override
1191            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1192                super.onLayoutChildren(recycler, state);
1193                detachAndScrapAttachedViews(recycler);
1194                layoutRange(recycler, layoutStart, layoutStart + childCount);
1195                layoutLatch.countDown();
1196            }
1197        };
1198        final RecyclerView recyclerView = new RecyclerView(getActivity());
1199        recyclerView.setAdapter(adapter);
1200        recyclerView.setLayoutManager(lm);
1201        lm.expectLayouts(1);
1202        setRecyclerView(recyclerView);
1203        lm.waitForLayout(2);
1204        getInstrumentation().waitForIdleSync();
1205        viewType.incrementAndGet();
1206        lm.expectLayouts(1);
1207        adapter.changeAndNotify(layoutStart, invalidatedCount);
1208        lm.waitForLayout(2);
1209        checkForMainThreadException();
1210    }
1211
1212
1213    public void testState() throws Throwable {
1214        final TestAdapter adapter = new TestAdapter(10);
1215        final RecyclerView recyclerView = new RecyclerView(getActivity());
1216        recyclerView.setAdapter(adapter);
1217        recyclerView.setItemAnimator(null);
1218        final AtomicInteger itemCount = new AtomicInteger();
1219        final AtomicBoolean structureChanged = new AtomicBoolean();
1220        TestLayoutManager testLayoutManager = new TestLayoutManager() {
1221            @Override
1222            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1223                detachAndScrapAttachedViews(recycler);
1224                layoutRange(recycler, 0, state.getItemCount());
1225                itemCount.set(state.getItemCount());
1226                structureChanged.set(state.didStructureChange());
1227                layoutLatch.countDown();
1228            }
1229        };
1230        recyclerView.setLayoutManager(testLayoutManager);
1231        testLayoutManager.expectLayouts(1);
1232        runTestOnUiThread(new Runnable() {
1233            @Override
1234            public void run() {
1235                getActivity().mContainer.addView(recyclerView);
1236            }
1237        });
1238        testLayoutManager.waitForLayout(2, TimeUnit.SECONDS);
1239
1240        assertEquals("item count in state should be correct", adapter.getItemCount()
1241                , itemCount.get());
1242        assertEquals("structure changed should be true for first layout", true,
1243                structureChanged.get());
1244        Thread.sleep(1000); //wait for other layouts.
1245        testLayoutManager.expectLayouts(1);
1246        runTestOnUiThread(new Runnable() {
1247            @Override
1248            public void run() {
1249                recyclerView.requestLayout();
1250            }
1251        });
1252        testLayoutManager.waitForLayout(2);
1253        assertEquals("in second layout,structure changed should be false", false,
1254                structureChanged.get());
1255        testLayoutManager.expectLayouts(1); //
1256        adapter.deleteAndNotify(3, 2);
1257        testLayoutManager.waitForLayout(2);
1258        assertEquals("when items are removed, item count in state should be updated",
1259                adapter.getItemCount(),
1260                itemCount.get());
1261        assertEquals("structure changed should be true when items are removed", true,
1262                structureChanged.get());
1263        testLayoutManager.expectLayouts(1);
1264        adapter.addAndNotify(2, 5);
1265        testLayoutManager.waitForLayout(2);
1266
1267        assertEquals("when items are added, item count in state should be updated",
1268                adapter.getItemCount(),
1269                itemCount.get());
1270        assertEquals("structure changed should be true when items are removed", true,
1271                structureChanged.get());
1272    }
1273
1274    private static class TestViewHolder2 extends RecyclerView.ViewHolder {
1275        public TestViewHolder2(View itemView) {
1276            super(itemView);
1277        }
1278    }
1279
1280    private static class TestAdapter2 extends RecyclerView.Adapter<TestViewHolder2> {
1281        List<Item> mItems;
1282
1283        private TestAdapter2(int count) {
1284            mItems = new ArrayList<Item>(count);
1285            for (int i = 0; i < count; i++) {
1286                mItems.add(new Item(i, "Item " + i));
1287            }
1288        }
1289
1290        @Override
1291        public TestViewHolder2 onCreateViewHolder(ViewGroup parent,
1292                int viewType) {
1293            return new TestViewHolder2(new TextView(parent.getContext()));
1294        }
1295
1296        @Override
1297        public void onBindViewHolder(TestViewHolder2 holder, int position) {
1298            final Item item = mItems.get(position);
1299            ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")");
1300        }
1301
1302        @Override
1303        public int getItemCount() {
1304            return mItems.size();
1305        }
1306    }
1307
1308}
1309