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 static android.support.v7.widget.RecyclerView.NO_POSITION;
21import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING;
22import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
23import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING;
24import static android.support.v7.widget.RecyclerView.getChildViewHolderInt;
25
26import static org.hamcrest.CoreMatchers.is;
27import static org.hamcrest.CoreMatchers.not;
28import static org.hamcrest.CoreMatchers.sameInstance;
29import static org.junit.Assert.assertEquals;
30import static org.junit.Assert.assertFalse;
31import static org.junit.Assert.assertNotNull;
32import static org.junit.Assert.assertNotSame;
33import static org.junit.Assert.assertNull;
34import static org.junit.Assert.assertSame;
35import static org.junit.Assert.assertThat;
36import static org.junit.Assert.assertTrue;
37import static org.junit.Assert.fail;
38import static org.mockito.Mockito.any;
39import static org.mockito.Mockito.anyInt;
40import static org.mockito.Mockito.eq;
41import static org.mockito.Mockito.mock;
42import static org.mockito.Mockito.never;
43import static org.mockito.Mockito.spy;
44import static org.mockito.Mockito.times;
45import static org.mockito.Mockito.verify;
46
47import android.content.Context;
48import android.graphics.Color;
49import android.graphics.PointF;
50import android.graphics.Rect;
51import android.os.Build;
52import android.os.SystemClock;
53import android.support.annotation.Nullable;
54import android.support.test.filters.SdkSuppress;
55import android.support.test.runner.AndroidJUnit4;
56import android.support.v4.view.ViewCompat;
57import android.support.v7.util.TouchUtils;
58import android.test.suitebuilder.annotation.MediumTest;
59import android.util.AttributeSet;
60import android.util.Log;
61import android.view.Gravity;
62import android.view.MotionEvent;
63import android.view.View;
64import android.view.ViewConfiguration;
65import android.view.ViewGroup;
66import android.view.ViewTreeObserver;
67import android.widget.LinearLayout;
68import android.widget.TextView;
69
70import org.hamcrest.CoreMatchers;
71import org.hamcrest.MatcherAssert;
72import org.junit.Test;
73import org.junit.runner.RunWith;
74
75import java.util.ArrayList;
76import java.util.HashMap;
77import java.util.List;
78import java.util.Map;
79import java.util.concurrent.CountDownLatch;
80import java.util.concurrent.TimeUnit;
81import java.util.concurrent.atomic.AtomicBoolean;
82import java.util.concurrent.atomic.AtomicInteger;
83
84@RunWith(AndroidJUnit4.class)
85@MediumTest
86public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest {
87    private static final int FLAG_HORIZONTAL = 1;
88    private static final int FLAG_VERTICAL = 1 << 1;
89    private static final int FLAG_FLING = 1 << 2;
90
91    private static final boolean DEBUG = false;
92
93    private static final String TAG = "RecyclerViewLayoutTest";
94
95    public RecyclerViewLayoutTest() {
96        super(DEBUG);
97    }
98
99    @Test
100    public void detachAttachGetReadyWithoutChanges() throws Throwable {
101        detachAttachGetReady(false, false, false);
102    }
103
104    @Test
105    public void detachAttachGetReadyRequireLayout() throws Throwable {
106        detachAttachGetReady(true, false, false);
107    }
108
109    @Test
110    public void detachAttachGetReadyRemoveAdapter() throws Throwable {
111        detachAttachGetReady(false, true, false);
112    }
113
114    @Test
115    public void detachAttachGetReadyRemoveLayoutManager() throws Throwable {
116        detachAttachGetReady(false, false, true);
117    }
118
119    private void detachAttachGetReady(final boolean requestLayoutOnDetach,
120            final boolean removeAdapter, final boolean removeLayoutManager) throws Throwable {
121        final LinearLayout ll1 = new LinearLayout(getActivity());
122        final LinearLayout ll2 = new LinearLayout(getActivity());
123        final LinearLayout ll3 = new LinearLayout(getActivity());
124
125        final RecyclerView rv = new RecyclerView(getActivity());
126        ll1.addView(ll2);
127        ll2.addView(ll3);
128        ll3.addView(rv);
129        TestLayoutManager layoutManager = new TestLayoutManager() {
130            @Override
131            public void onLayoutCompleted(RecyclerView.State state) {
132                super.onLayoutCompleted(state);
133                layoutLatch.countDown();
134            }
135
136            @Override
137            public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
138                super.onDetachedFromWindow(view, recycler);
139                if (requestLayoutOnDetach) {
140                    view.requestLayout();
141                }
142            }
143        };
144        rv.setLayoutManager(layoutManager);
145        rv.setAdapter(new TestAdapter(10));
146        layoutManager.expectLayouts(1);
147        runTestOnUiThread(new Runnable() {
148            @Override
149            public void run() {
150                getActivity().getContainer().addView(ll1);
151            }
152        });
153        layoutManager.waitForLayout(2);
154        runTestOnUiThread(new Runnable() {
155            @Override
156            public void run() {
157                ll1.removeView(ll2);
158            }
159        });
160        getInstrumentation().waitForIdleSync();
161        if (removeLayoutManager) {
162            rv.setLayoutManager(null);
163            rv.setLayoutManager(layoutManager);
164        }
165        if (removeAdapter) {
166            rv.setAdapter(null);
167            rv.setAdapter(new TestAdapter(10));
168        }
169        final boolean requireLayout = requestLayoutOnDetach || removeAdapter || removeLayoutManager;
170        layoutManager.expectLayouts(1);
171        runTestOnUiThread(new Runnable() {
172            @Override
173            public void run() {
174                ll1.addView(ll2);
175                if (requireLayout) {
176                    assertTrue(rv.hasPendingAdapterUpdates());
177                    assertFalse(rv.mFirstLayoutComplete);
178                } else {
179                    assertFalse(rv.hasPendingAdapterUpdates());
180                    assertTrue(rv.mFirstLayoutComplete);
181                }
182            }
183        });
184        if (requireLayout) {
185            layoutManager.waitForLayout(2);
186        } else {
187            layoutManager.assertNoLayout("nothing is invalid, layout should not happen", 2);
188        }
189    }
190
191    @Test
192    public void detachRvAndLayoutManagerProperly() throws Throwable {
193        final RecyclerView rv = new RecyclerView(getActivity());
194        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true) {
195            @Override
196            public void onAttachedToWindow(RecyclerView view) {
197                super.onAttachedToWindow(view);
198                assertThat(view.mLayout, is((RecyclerView.LayoutManager) this));
199            }
200
201            @Override
202            public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
203                super.onDetachedFromWindow(view, recycler);
204                assertThat(view.mLayout, is((RecyclerView.LayoutManager) this));
205            }
206        };
207        final Runnable check = new Runnable() {
208            @Override
209            public void run() {
210                assertThat("bound between the RV and the LM should be disconnected at the"
211                        + " same time", rv.mLayout == lm, is(lm.mRecyclerView == rv));
212            }
213        };
214        final AtomicInteger detachCounter = new AtomicInteger(0);
215        rv.setAdapter(new TestAdapter(10) {
216            @Override
217            public void onBindViewHolder(TestViewHolder holder,
218                    int position) {
219                super.onBindViewHolder(holder, position);
220                holder.itemView.setFocusable(true);
221                holder.itemView.setFocusableInTouchMode(true);
222            }
223
224            @Override
225            public void onViewDetachedFromWindow(TestViewHolder holder) {
226                super.onViewDetachedFromWindow(holder);
227                detachCounter.incrementAndGet();
228                check.run();
229            }
230
231            @Override
232            public void onViewRecycled(TestViewHolder holder) {
233                super.onViewRecycled(holder);
234                check.run();
235            }
236        });
237        rv.setLayoutManager(lm);
238        lm.expectLayouts(1);
239        setRecyclerView(rv);
240        lm.waitForLayout(2);
241        assertThat("test sanity", rv.getChildCount(), is(10));
242
243        final TestLayoutManager replacement = new LayoutAllLayoutManager(true);
244        replacement.expectLayouts(1);
245        runTestOnUiThread(new Runnable() {
246            @Override
247            public void run() {
248                rv.setLayoutManager(replacement);
249            }
250        });
251        replacement.waitForLayout(2);
252        assertThat("test sanity", rv.getChildCount(), is(10));
253        assertThat("all initial views should be detached", detachCounter.get(), is(10));
254        checkForMainThreadException();
255    }
256
257    @Test
258    public void focusSearchWithOtherFocusables() throws Throwable {
259        final LinearLayout container = new LinearLayout(getActivity());
260        container.setOrientation(LinearLayout.VERTICAL);
261        RecyclerView rv = new RecyclerView(getActivity());
262        mRecyclerView = rv;
263        rv.setAdapter(new TestAdapter(10) {
264            @Override
265            public void onBindViewHolder(TestViewHolder holder,
266                    int position) {
267                super.onBindViewHolder(holder, position);
268                holder.itemView.setFocusableInTouchMode(true);
269                holder.itemView.setLayoutParams(
270                        new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
271                        ViewGroup.LayoutParams.WRAP_CONTENT));
272            }
273        });
274        TestLayoutManager tlm = new TestLayoutManager() {
275            @Override
276            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
277                detachAndScrapAttachedViews(recycler);
278                layoutRange(recycler, 0, 1);
279                layoutLatch.countDown();
280            }
281
282            @Nullable
283            @Override
284            public View onFocusSearchFailed(View focused, int direction,
285                    RecyclerView.Recycler recycler,
286                    RecyclerView.State state) {
287                assertEquals(View.FOCUS_FORWARD, direction);
288                assertEquals(1, getChildCount());
289                View child0 = getChildAt(0);
290                View view = recycler.getViewForPosition(1);
291                addView(view);
292                measureChild(view, 0, 0);
293                layoutDecorated(view, 0, child0.getBottom(), getDecoratedMeasuredWidth(view),
294                        child0.getBottom() + getDecoratedMeasuredHeight(view));
295                return view;
296            }
297        };
298        tlm.setAutoMeasureEnabled(true);
299        rv.setLayoutManager(tlm);
300        TextView viewAbove = new TextView(getActivity());
301        viewAbove.setText("view above");
302        viewAbove.setFocusableInTouchMode(true);
303        container.addView(viewAbove);
304        container.addView(rv);
305        TextView viewBelow = new TextView(getActivity());
306        viewBelow.setText("view below");
307        viewBelow.setFocusableInTouchMode(true);
308        container.addView(viewBelow);
309        tlm.expectLayouts(1);
310        runTestOnUiThread(new Runnable() {
311            @Override
312            public void run() {
313                getActivity().getContainer().addView(container);
314            }
315        });
316
317        tlm.waitForLayout(2);
318        requestFocus(viewAbove, true);
319        assertTrue(viewAbove.hasFocus());
320        View newFocused = focusSearch(viewAbove, View.FOCUS_FORWARD);
321        assertThat(newFocused, sameInstance(rv.getChildAt(0)));
322        newFocused = focusSearch(rv.getChildAt(0), View.FOCUS_FORWARD);
323        assertThat(newFocused, sameInstance(rv.getChildAt(1)));
324    }
325
326    @Test
327    public void boundingBoxNoTranslation() throws Throwable {
328        transformedBoundingBoxTest(new ViewRunnable() {
329            @Override
330            public void run(View view) throws RuntimeException {
331                view.layout(10, 10, 30, 50);
332                assertThat(getTransformedBoundingBox(view), is(new Rect(10, 10, 30, 50)));
333            }
334        });
335    }
336
337    @Test
338    public void boundingBoxTranslateX() throws Throwable {
339        transformedBoundingBoxTest(new ViewRunnable() {
340            @Override
341            public void run(View view) throws RuntimeException {
342                view.layout(10, 10, 30, 50);
343                ViewCompat.setTranslationX(view, 10);
344                assertThat(getTransformedBoundingBox(view), is(new Rect(20, 10, 40, 50)));
345            }
346        });
347    }
348
349    @Test
350    public void boundingBoxTranslateY() throws Throwable {
351        transformedBoundingBoxTest(new ViewRunnable() {
352            @Override
353            public void run(View view) throws RuntimeException {
354                view.layout(10, 10, 30, 50);
355                ViewCompat.setTranslationY(view, 10);
356                assertThat(getTransformedBoundingBox(view), is(new Rect(10, 20, 30, 60)));
357            }
358        });
359    }
360
361    @Test
362    public void boundingBoxScaleX() throws Throwable {
363        transformedBoundingBoxTest(new ViewRunnable() {
364            @Override
365            public void run(View view) throws RuntimeException {
366                view.layout(10, 10, 30, 50);
367                ViewCompat.setScaleX(view, 2);
368                assertThat(getTransformedBoundingBox(view), is(new Rect(0, 10, 40, 50)));
369            }
370        });
371    }
372
373    @Test
374    public void boundingBoxScaleY() throws Throwable {
375        transformedBoundingBoxTest(new ViewRunnable() {
376            @Override
377            public void run(View view) throws RuntimeException {
378                view.layout(10, 10, 30, 50);
379                ViewCompat.setScaleY(view, 2);
380                assertThat(getTransformedBoundingBox(view), is(new Rect(10, -10, 30, 70)));
381            }
382        });
383    }
384
385    @Test
386    public void boundingBoxRotated() throws Throwable {
387        transformedBoundingBoxTest(new ViewRunnable() {
388            @Override
389            public void run(View view) throws RuntimeException {
390                view.layout(10, 10, 30, 50);
391                ViewCompat.setRotation(view, 90);
392                assertThat(getTransformedBoundingBox(view), is(new Rect(0, 20, 40, 40)));
393            }
394        });
395    }
396
397    @Test
398    public void boundingBoxRotatedWithDecorOffsets() throws Throwable {
399        final RecyclerView recyclerView = new RecyclerView(getActivity());
400        final TestAdapter adapter = new TestAdapter(1);
401        recyclerView.setAdapter(adapter);
402        recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
403            @Override
404            public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
405                    RecyclerView.State state) {
406                outRect.set(1, 2, 3, 4);
407            }
408        });
409        TestLayoutManager layoutManager = new TestLayoutManager() {
410            @Override
411            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
412                detachAndScrapAttachedViews(recycler);
413                View view = recycler.getViewForPosition(0);
414                addView(view);
415                view.measure(
416                        View.MeasureSpec.makeMeasureSpec(20, View.MeasureSpec.EXACTLY),
417                        View.MeasureSpec.makeMeasureSpec(40, View.MeasureSpec.EXACTLY)
418                );
419                // trigger decor offsets calculation
420                calculateItemDecorationsForChild(view, new Rect());
421                view.layout(10, 10, 30, 50);
422                ViewCompat.setRotation(view, 90);
423                assertThat(RecyclerViewLayoutTest.this.getTransformedBoundingBox(view),
424                        is(new Rect(-4, 19, 42, 43)));
425
426                layoutLatch.countDown();
427            }
428        };
429        recyclerView.setLayoutManager(layoutManager);
430        layoutManager.expectLayouts(1);
431        setRecyclerView(recyclerView);
432        layoutManager.waitForLayout(2);
433        checkForMainThreadException();
434    }
435
436    private Rect getTransformedBoundingBox(View child) {
437        Rect rect = new Rect();
438        mRecyclerView.getLayoutManager().getTransformedBoundingBox(child, true, rect);
439        return rect;
440    }
441
442    public void transformedBoundingBoxTest(final ViewRunnable layout) throws Throwable {
443        final RecyclerView recyclerView = new RecyclerView(getActivity());
444        final TestAdapter adapter = new TestAdapter(1);
445        recyclerView.setAdapter(adapter);
446        TestLayoutManager layoutManager = new TestLayoutManager() {
447            @Override
448            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
449                detachAndScrapAttachedViews(recycler);
450                View view = recycler.getViewForPosition(0);
451                addView(view);
452                view.measure(
453                        View.MeasureSpec.makeMeasureSpec(20, View.MeasureSpec.EXACTLY),
454                        View.MeasureSpec.makeMeasureSpec(40, View.MeasureSpec.EXACTLY)
455                );
456                layout.run(view);
457                layoutLatch.countDown();
458            }
459        };
460        recyclerView.setLayoutManager(layoutManager);
461        layoutManager.expectLayouts(1);
462        setRecyclerView(recyclerView);
463        layoutManager.waitForLayout(2);
464        checkForMainThreadException();
465    }
466
467    @Test
468    public void flingFrozen() throws Throwable {
469        testScrollFrozen(true);
470    }
471
472    @Test
473    public void dragFrozen() throws Throwable {
474        testScrollFrozen(false);
475    }
476
477    @Test
478    public void requestRectOnScreenWithScrollOffset() throws Throwable {
479        final RecyclerView recyclerView = new RecyclerView(getActivity());
480        final LayoutAllLayoutManager tlm = spy(new LayoutAllLayoutManager());
481        final int scrollY = 50;
482        RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
483            @Override
484            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
485                View view = new View(parent.getContext());
486                view.setScrollY(scrollY);
487                return new RecyclerView.ViewHolder(view) {
488                };
489            }
490            @Override
491            public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {}
492            @Override
493            public int getItemCount() {
494                return 1;
495            }
496        };
497        recyclerView.setAdapter(adapter);
498        recyclerView.setLayoutManager(tlm);
499        tlm.expectLayouts(1);
500        setRecyclerView(recyclerView);
501        tlm.waitForLayout(1);
502        final View child = recyclerView.getChildAt(0);
503        assertThat(child.getScrollY(), CoreMatchers.is(scrollY));
504        runTestOnUiThread(new Runnable() {
505            @Override
506            public void run() {
507                recyclerView.requestChildRectangleOnScreen(child, new Rect(3, 4, 5, 6), true);
508                verify(tlm, times(1)).scrollVerticallyBy(eq(-46), any(RecyclerView.Recycler.class),
509                        any(RecyclerView.State.class));
510            }
511        });
512    }
513
514    @Test
515    public void reattachAndScrollCrash() throws Throwable {
516        final RecyclerView recyclerView = new RecyclerView(getActivity());
517        final TestLayoutManager tlm = new TestLayoutManager() {
518
519            @Override
520            public boolean canScrollVertically() {
521                return true;
522            }
523
524            @Override
525            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
526                layoutRange(recycler, 0, Math.min(state.getItemCount(), 10));
527            }
528
529            @Override
530            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
531                                          RecyclerView.State state) {
532                // Access views in the state (that might have been deleted).
533                for (int  i = 10; i < state.getItemCount(); i++) {
534                    recycler.getViewForPosition(i);
535                }
536                return dy;
537            }
538        };
539
540        final TestAdapter adapter = new TestAdapter(12);
541
542        recyclerView.setAdapter(adapter);
543        recyclerView.setLayoutManager(tlm);
544
545        setRecyclerView(recyclerView);
546
547        runTestOnUiThread(new Runnable() {
548            @Override
549            public void run() {
550                getActivity().getContainer().removeView(recyclerView);
551                getActivity().getContainer().addView(recyclerView);
552                try {
553                    adapter.deleteAndNotify(1, adapter.getItemCount() - 1);
554                } catch (Throwable throwable) {
555                    postExceptionToInstrumentation(throwable);
556                }
557                recyclerView.scrollBy(0, 10);
558            }
559        });
560    }
561
562    private void testScrollFrozen(boolean fling) throws Throwable {
563        RecyclerView recyclerView = new RecyclerView(getActivity());
564
565        final int horizontalScrollCount = 3;
566        final int verticalScrollCount = 3;
567        final int horizontalVelocity = 1000;
568        final int verticalVelocity = 1000;
569        final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount);
570        final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount);
571        TestLayoutManager tlm = new TestLayoutManager() {
572            @Override
573            public boolean canScrollHorizontally() {
574                return true;
575            }
576
577            @Override
578            public boolean canScrollVertically() {
579                return true;
580            }
581
582            @Override
583            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
584                layoutRange(recycler, 0, 10);
585                layoutLatch.countDown();
586            }
587
588            @Override
589            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
590                    RecyclerView.State state) {
591                if (verticalCounter.get() > 0) {
592                    verticalCounter.decrementAndGet();
593                    return dy;
594                }
595                return 0;
596            }
597
598            @Override
599            public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
600                    RecyclerView.State state) {
601                if (horizontalCounter.get() > 0) {
602                    horizontalCounter.decrementAndGet();
603                    return dx;
604                }
605                return 0;
606            }
607        };
608        TestAdapter adapter = new TestAdapter(100);
609        recyclerView.setAdapter(adapter);
610        recyclerView.setLayoutManager(tlm);
611        tlm.expectLayouts(1);
612        setRecyclerView(recyclerView);
613        tlm.waitForLayout(2);
614
615        freezeLayout(true);
616
617        if (fling) {
618            assertFalse("fling should be blocked", fling(horizontalVelocity, verticalVelocity));
619        } else { // drag
620            TouchUtils.dragViewTo(getInstrumentation(), recyclerView,
621                    Gravity.LEFT | Gravity.TOP,
622                    mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2);
623        }
624        assertEquals("rv's horizontal scroll cb must not run", horizontalScrollCount,
625                horizontalCounter.get());
626        assertEquals("rv's vertical scroll cb must not run", verticalScrollCount,
627                verticalCounter.get());
628
629        freezeLayout(false);
630
631        if (fling) {
632            assertTrue("fling should be started", fling(horizontalVelocity, verticalVelocity));
633        } else { // drag
634            TouchUtils.dragViewTo(getInstrumentation(), recyclerView,
635                    Gravity.LEFT | Gravity.TOP,
636                    mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2);
637        }
638        assertEquals("rv's horizontal scroll cb must finishes", 0, horizontalCounter.get());
639        assertEquals("rv's vertical scroll cb must finishes", 0, verticalCounter.get());
640    }
641
642    @Test
643    public void testFocusSearchAfterChangedData() throws Throwable {
644        final RecyclerView recyclerView = new RecyclerView(getActivity());
645        TestLayoutManager tlm = new TestLayoutManager() {
646            @Override
647            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
648                layoutRange(recycler, 0, 2);
649                layoutLatch.countDown();
650            }
651
652            @Nullable
653            @Override
654            public View onFocusSearchFailed(View focused, int direction,
655                                            RecyclerView.Recycler recycler,
656                                            RecyclerView.State state) {
657                try {
658                    recycler.getViewForPosition(state.getItemCount() - 1);
659                } catch (Throwable t) {
660                    postExceptionToInstrumentation(t);
661                }
662                return null;
663            }
664        };
665        recyclerView.setLayoutManager(tlm);
666        final TestAdapter adapter = new TestAdapter(10) {
667            @Override
668            public void onBindViewHolder(TestViewHolder holder, int position) {
669                super.onBindViewHolder(holder, position);
670                holder.itemView.setFocusable(false);
671                holder.itemView.setFocusableInTouchMode(false);
672            }
673        };
674        recyclerView.setAdapter(adapter);
675        tlm.expectLayouts(1);
676        setRecyclerView(recyclerView);
677        tlm.waitForLayout(1);
678        runTestOnUiThread(new Runnable() {
679            @Override
680            public void run() {
681                adapter.mItems.remove(9);
682                adapter.notifyItemRemoved(9);
683                recyclerView.focusSearch(recyclerView.getChildAt(1), View.FOCUS_DOWN);
684            }
685        });
686        checkForMainThreadException();
687    }
688
689    @Test
690    public void testFocusSearchWithRemovedFocusedItem() throws Throwable {
691        final RecyclerView recyclerView = new RecyclerView(getActivity());
692        recyclerView.setItemAnimator(null);
693        TestLayoutManager tlm = new LayoutAllLayoutManager();
694        recyclerView.setLayoutManager(tlm);
695        final TestAdapter adapter = new TestAdapter(10) {
696            @Override
697            public void onBindViewHolder(TestViewHolder holder, int position) {
698                super.onBindViewHolder(holder, position);
699                holder.itemView.setFocusable(true);
700                holder.itemView.setFocusableInTouchMode(true);
701            }
702        };
703        recyclerView.setAdapter(adapter);
704        tlm.expectLayouts(1);
705        setRecyclerView(recyclerView);
706        tlm.waitForLayout(1);
707        final RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(9);
708        requestFocus(toFocus.itemView, true);
709        assertThat("test sanity", toFocus.itemView.hasFocus(), is(true));
710        runTestOnUiThread(new Runnable() {
711            @Override
712            public void run() {
713                adapter.mItems.remove(9);
714                adapter.notifyItemRemoved(9);
715                recyclerView.focusSearch(toFocus.itemView, View.FOCUS_DOWN);
716            }
717        });
718        checkForMainThreadException();
719    }
720
721
722    @Test
723    public void  testFocusSearchFailFrozen() throws Throwable {
724        RecyclerView recyclerView = new RecyclerView(getActivity());
725        final CountDownLatch focusLatch = new CountDownLatch(1);
726        final AtomicInteger focusSearchCalled = new AtomicInteger(0);
727        TestLayoutManager tlm = new TestLayoutManager() {
728            @Override
729            public boolean canScrollHorizontally() {
730                return true;
731            }
732
733            @Override
734            public boolean canScrollVertically() {
735                return true;
736            }
737
738            @Override
739            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
740                layoutRange(recycler, 0, 10);
741                layoutLatch.countDown();
742            }
743
744            @Override
745            public View onFocusSearchFailed(View focused, int direction,
746                    RecyclerView.Recycler recycler, RecyclerView.State state) {
747                focusSearchCalled.addAndGet(1);
748                focusLatch.countDown();
749                return null;
750            }
751        };
752        TestAdapter adapter = new TestAdapter(100);
753        recyclerView.setAdapter(adapter);
754        recyclerView.setLayoutManager(tlm);
755        tlm.expectLayouts(1);
756        setRecyclerView(recyclerView);
757        tlm.waitForLayout(2);
758        final View c = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
759        assertTrue("test sanity", requestFocus(c, true));
760        assertTrue("test sanity", c.hasFocus());
761        freezeLayout(true);
762        focusSearch(recyclerView, c, View.FOCUS_DOWN);
763        assertEquals("onFocusSearchFailed should not be called when layout is frozen",
764                0, focusSearchCalled.get());
765        freezeLayout(false);
766        focusSearch(c, View.FOCUS_DOWN);
767        assertTrue(focusLatch.await(2, TimeUnit.SECONDS));
768        assertEquals(1, focusSearchCalled.get());
769    }
770
771    public View focusSearch(final ViewGroup parent, final View focused, final int direction)
772            throws Throwable {
773        final View[] result = new View[1];
774        runTestOnUiThread(new Runnable() {
775            @Override
776            public void run() {
777                result[0] = parent.focusSearch(focused, direction);
778            }
779        });
780        return result[0];
781    }
782
783    @Test
784    public void frozenAndChangeAdapter() throws Throwable {
785        RecyclerView recyclerView = new RecyclerView(getActivity());
786
787        final AtomicInteger focusSearchCalled = new AtomicInteger(0);
788        TestLayoutManager tlm = new TestLayoutManager() {
789            @Override
790            public boolean canScrollHorizontally() {
791                return true;
792            }
793
794            @Override
795            public boolean canScrollVertically() {
796                return true;
797            }
798
799            @Override
800            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
801                layoutRange(recycler, 0, 10);
802                layoutLatch.countDown();
803            }
804
805            @Override
806            public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
807                    RecyclerView.State state) {
808                focusSearchCalled.addAndGet(1);
809                return null;
810            }
811        };
812        TestAdapter adapter = new TestAdapter(100);
813        recyclerView.setAdapter(adapter);
814        recyclerView.setLayoutManager(tlm);
815        tlm.expectLayouts(1);
816        setRecyclerView(recyclerView);
817        tlm.waitForLayout(2);
818
819        freezeLayout(true);
820        TestAdapter adapter2 = new TestAdapter(1000);
821        setAdapter(adapter2);
822        assertFalse(recyclerView.isLayoutFrozen());
823        assertSame(adapter2, recyclerView.getAdapter());
824
825        freezeLayout(true);
826        TestAdapter adapter3 = new TestAdapter(1000);
827        swapAdapter(adapter3, true);
828        assertFalse(recyclerView.isLayoutFrozen());
829        assertSame(adapter3, recyclerView.getAdapter());
830    }
831
832    @Test
833    public void noLayoutIf0ItemsAreChanged() throws Throwable {
834        unnecessaryNotifyEvents(new AdapterRunnable() {
835            @Override
836            public void run(TestAdapter adapter) throws Throwable {
837                adapter.notifyItemRangeChanged(3, 0);
838            }
839        });
840    }
841
842    @Test
843    public void noLayoutIf0ItemsAreChangedWithPayload() throws Throwable {
844        unnecessaryNotifyEvents(new AdapterRunnable() {
845            @Override
846            public void run(TestAdapter adapter) throws Throwable {
847                adapter.notifyItemRangeChanged(0, 0, new Object());
848            }
849        });
850    }
851
852    @Test
853    public void noLayoutIf0ItemsAreAdded() throws Throwable {
854        unnecessaryNotifyEvents(new AdapterRunnable() {
855            @Override
856            public void run(TestAdapter adapter) throws Throwable {
857                adapter.notifyItemRangeInserted(3, 0);
858            }
859        });
860    }
861
862    @Test
863    public void noLayoutIf0ItemsAreRemoved() throws Throwable {
864        unnecessaryNotifyEvents(new AdapterRunnable() {
865            @Override
866            public void run(TestAdapter adapter) throws Throwable {
867                adapter.notifyItemRangeRemoved(3, 0);
868            }
869        });
870    }
871
872    @Test
873    public void noLayoutIfItemMovedIntoItsOwnPlace() throws Throwable {
874        unnecessaryNotifyEvents(new AdapterRunnable() {
875            @Override
876            public void run(TestAdapter adapter) throws Throwable {
877                adapter.notifyItemMoved(3, 3);
878            }
879        });
880    }
881
882    public void unnecessaryNotifyEvents(final AdapterRunnable action) throws Throwable {
883        final RecyclerView recyclerView = new RecyclerView(getActivity());
884        final TestAdapter adapter = new TestAdapter(5);
885        TestLayoutManager tlm = new TestLayoutManager() {
886            @Override
887            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
888                super.onLayoutChildren(recycler, state);
889                layoutLatch.countDown();
890            }
891        };
892        recyclerView.setLayoutManager(tlm);
893        recyclerView.setAdapter(adapter);
894        tlm.expectLayouts(1);
895        setRecyclerView(recyclerView);
896        tlm.waitForLayout(1);
897        // ready
898        tlm.expectLayouts(1);
899        runTestOnUiThread(new Runnable() {
900            @Override
901            public void run() {
902                try {
903                    action.run(adapter);
904                } catch (Throwable throwable) {
905                    postExceptionToInstrumentation(throwable);
906                }
907            }
908        });
909        tlm.assertNoLayout("dummy event should not trigger a layout", 1);
910        checkForMainThreadException();
911    }
912
913    @Test
914    public void scrollToPositionCallback() throws Throwable {
915        RecyclerView recyclerView = new RecyclerView(getActivity());
916        TestLayoutManager tlm = new TestLayoutManager() {
917            int scrollPos = RecyclerView.NO_POSITION;
918
919            @Override
920            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
921                layoutLatch.countDown();
922                if (scrollPos == RecyclerView.NO_POSITION) {
923                    layoutRange(recycler, 0, 10);
924                } else {
925                    layoutRange(recycler, scrollPos, scrollPos + 10);
926                }
927            }
928
929            @Override
930            public void scrollToPosition(int position) {
931                scrollPos = position;
932                requestLayout();
933            }
934        };
935        recyclerView.setLayoutManager(tlm);
936        TestAdapter adapter = new TestAdapter(100);
937        recyclerView.setAdapter(adapter);
938        final AtomicInteger rvCounter = new AtomicInteger(0);
939        final AtomicInteger viewGroupCounter = new AtomicInteger(0);
940
941        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
942            @Override
943            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
944                rvCounter.incrementAndGet();
945                super.onScrolled(recyclerView, dx, dy);
946            }
947        });
948
949        getRecyclerViewContainer().getViewTreeObserver().addOnScrollChangedListener(
950                new ViewTreeObserver.OnScrollChangedListener() {
951                    @Override
952                    public void onScrollChanged() {
953                        viewGroupCounter.incrementAndGet();
954                    }
955                });
956
957        tlm.expectLayouts(1);
958        setRecyclerView(recyclerView);
959        tlm.waitForLayout(2);
960
961        assertEquals("RV on scroll should be called for initialization", 1, rvCounter.get());
962        assertEquals("VTO on scroll should be called for initialization", 1,
963                viewGroupCounter.get());
964        tlm.expectLayouts(1);
965        freezeLayout(true);
966        scrollToPosition(3);
967        tlm.assertNoLayout("scrollToPosition should be ignored", 2);
968        freezeLayout(false);
969        scrollToPosition(3);
970        tlm.waitForLayout(2);
971        assertEquals("RV on scroll should be called", 2, rvCounter.get());
972        assertEquals("VTO on scroll should be called", 2, viewGroupCounter.get());
973        tlm.expectLayouts(1);
974        requestLayoutOnUIThread(recyclerView);
975        tlm.waitForLayout(2);
976        // wait for draw :/
977        Thread.sleep(1000);
978        assertEquals("on scroll should NOT be called", 2, rvCounter.get());
979        assertEquals("on scroll should NOT be called", 2, viewGroupCounter.get());
980    }
981
982    @Test
983    public void scrollCallbackFromEmptyToSome() throws Throwable {
984        scrollCallbackOnVisibleRangeChange(1, new int[]{0, 0}, new int[]{0, 1});
985    }
986
987    @Test
988    public void scrollCallbackOnVisibleRangeExpand() throws Throwable {
989        scrollCallbackOnVisibleRangeChange(10, new int[]{3, 5}, new int[]{3, 6});
990    }
991
992    @Test
993    public void scrollCallbackOnVisibleRangeShrink() throws Throwable {
994        scrollCallbackOnVisibleRangeChange(10, new int[]{3, 6}, new int[]{3, 5});
995    }
996
997    @Test
998    public void scrollCallbackOnVisibleRangeExpand2() throws Throwable {
999        scrollCallbackOnVisibleRangeChange(10, new int[]{3, 5}, new int[]{2, 5});
1000    }
1001
1002    @Test
1003    public void scrollCallbackOnVisibleRangeShrink2() throws Throwable {
1004        scrollCallbackOnVisibleRangeChange(10, new int[]{3, 6}, new int[]{2, 6});
1005    }
1006
1007    private void scrollCallbackOnVisibleRangeChange(int itemCount, final int[] beforeRange,
1008            final int[] afterRange) throws Throwable {
1009        RecyclerView recyclerView = new RecyclerView(getActivity()) {
1010            @Override
1011            void dispatchLayout() {
1012                super.dispatchLayout();
1013                ((TestLayoutManager) getLayoutManager()).layoutLatch.countDown();
1014            }
1015        };
1016        final AtomicBoolean beforeState = new AtomicBoolean(true);
1017        TestLayoutManager tlm = new TestLayoutManager() {
1018            @Override
1019            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1020                detachAndScrapAttachedViews(recycler);
1021                int[] range = beforeState.get() ? beforeRange : afterRange;
1022                layoutRange(recycler, range[0], range[1]);
1023            }
1024        };
1025        recyclerView.setLayoutManager(tlm);
1026        final TestAdapter adapter = new TestAdapter(itemCount);
1027        recyclerView.setAdapter(adapter);
1028        tlm.expectLayouts(1);
1029        setRecyclerView(recyclerView);
1030        tlm.waitForLayout(1);
1031
1032        RecyclerView.OnScrollListener mockListener = mock(RecyclerView.OnScrollListener.class);
1033        recyclerView.addOnScrollListener(mockListener);
1034        verify(mockListener, never()).onScrolled(any(RecyclerView.class), anyInt(), anyInt());
1035
1036        tlm.expectLayouts(1);
1037        beforeState.set(false);
1038        requestLayoutOnUIThread(recyclerView);
1039        tlm.waitForLayout(2);
1040        checkForMainThreadException();
1041        verify(mockListener).onScrolled(recyclerView, 0, 0);
1042    }
1043
1044    @Test
1045    public void addItemOnScroll() throws Throwable {
1046        RecyclerView recyclerView = new RecyclerView(getActivity());
1047        final AtomicInteger start = new AtomicInteger(0);
1048        TestLayoutManager tlm = new TestLayoutManager() {
1049            @Override
1050            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1051                layoutRange(recycler, start.get(), start.get() + 10);
1052                layoutLatch.countDown();
1053            }
1054        };
1055        recyclerView.setLayoutManager(tlm);
1056        final TestAdapter adapter = new TestAdapter(100);
1057        recyclerView.setAdapter(adapter);
1058        tlm.expectLayouts(1);
1059        setRecyclerView(recyclerView);
1060        tlm.waitForLayout(1);
1061        final Throwable[] error = new Throwable[1];
1062        final AtomicBoolean calledOnScroll = new AtomicBoolean(false);
1063        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
1064            @Override
1065            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
1066                super.onScrolled(recyclerView, dx, dy);
1067                calledOnScroll.set(true);
1068                try {
1069                    adapter.addAndNotify(5, 20);
1070                } catch (Throwable throwable) {
1071                    error[0] = throwable;
1072                }
1073            }
1074        });
1075        start.set(4);
1076        MatcherAssert.assertThat("test sanity", calledOnScroll.get(), CoreMatchers.is(false));
1077        tlm.expectLayouts(1);
1078        requestLayoutOnUIThread(recyclerView);
1079        tlm.waitForLayout(2);
1080        checkForMainThreadException();
1081        MatcherAssert.assertThat("test sanity", calledOnScroll.get(), CoreMatchers.is(true));
1082        MatcherAssert.assertThat(error[0], CoreMatchers.nullValue());
1083    }
1084
1085    @Test
1086    public void scrollInBothDirectionEqual() throws Throwable {
1087        scrollInBothDirection(3, 3, 1000, 1000);
1088    }
1089
1090    @Test
1091    public void scrollInBothDirectionMoreVertical() throws Throwable {
1092        scrollInBothDirection(2, 3, 1000, 1000);
1093    }
1094
1095    @Test
1096    public void scrollInBothDirectionMoreHorizontal() throws Throwable {
1097        scrollInBothDirection(3, 2, 1000, 1000);
1098    }
1099
1100    @Test
1101    public void scrollHorizontalOnly() throws Throwable {
1102        scrollInBothDirection(3, 0, 1000, 0);
1103    }
1104
1105    @Test
1106    public void scrollVerticalOnly() throws Throwable {
1107        scrollInBothDirection(0, 3, 0, 1000);
1108    }
1109
1110    @Test
1111    public void scrollInBothDirectionEqualReverse() throws Throwable {
1112        scrollInBothDirection(3, 3, -1000, -1000);
1113    }
1114
1115    @Test
1116    public void scrollInBothDirectionMoreVerticalReverse() throws Throwable {
1117        scrollInBothDirection(2, 3, -1000, -1000);
1118    }
1119
1120    @Test
1121    public void scrollInBothDirectionMoreHorizontalReverse() throws Throwable {
1122        scrollInBothDirection(3, 2, -1000, -1000);
1123    }
1124
1125    @Test
1126    public void scrollHorizontalOnlyReverse() throws Throwable {
1127        scrollInBothDirection(3, 0, -1000, 0);
1128    }
1129
1130    @Test
1131    public void scrollVerticalOnlyReverse() throws Throwable {
1132        scrollInBothDirection(0, 3, 0, -1000);
1133    }
1134
1135    public void scrollInBothDirection(int horizontalScrollCount, int verticalScrollCount,
1136            int horizontalVelocity, int verticalVelocity)
1137            throws Throwable {
1138        RecyclerView recyclerView = new RecyclerView(getActivity());
1139        final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount);
1140        final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount);
1141        TestLayoutManager tlm = new TestLayoutManager() {
1142            @Override
1143            public boolean canScrollHorizontally() {
1144                return true;
1145            }
1146
1147            @Override
1148            public boolean canScrollVertically() {
1149                return true;
1150            }
1151
1152            @Override
1153            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1154                layoutRange(recycler, 0, 10);
1155                layoutLatch.countDown();
1156            }
1157
1158            @Override
1159            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
1160                    RecyclerView.State state) {
1161                if (verticalCounter.get() > 0) {
1162                    verticalCounter.decrementAndGet();
1163                    return dy;
1164                }
1165                return 0;
1166            }
1167
1168            @Override
1169            public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
1170                    RecyclerView.State state) {
1171                if (horizontalCounter.get() > 0) {
1172                    horizontalCounter.decrementAndGet();
1173                    return dx;
1174                }
1175                return 0;
1176            }
1177        };
1178        TestAdapter adapter = new TestAdapter(100);
1179        recyclerView.setAdapter(adapter);
1180        recyclerView.setLayoutManager(tlm);
1181        tlm.expectLayouts(1);
1182        setRecyclerView(recyclerView);
1183        tlm.waitForLayout(2);
1184        assertTrue("test sanity, fling must run", fling(horizontalVelocity, verticalVelocity));
1185        assertEquals("rv's horizontal scroll cb must run " + horizontalScrollCount + " times'", 0,
1186                horizontalCounter.get());
1187        assertEquals("rv's vertical scroll cb must run " + verticalScrollCount + " times'", 0,
1188                verticalCounter.get());
1189    }
1190
1191    @Test
1192    public void dragHorizontal() throws Throwable {
1193        scrollInOtherOrientationTest(FLAG_HORIZONTAL);
1194    }
1195
1196    @Test
1197    public void dragVertical() throws Throwable {
1198        scrollInOtherOrientationTest(FLAG_VERTICAL);
1199    }
1200
1201    @Test
1202    public void flingHorizontal() throws Throwable {
1203        scrollInOtherOrientationTest(FLAG_HORIZONTAL | FLAG_FLING);
1204    }
1205
1206    @Test
1207    public void flingVertical() throws Throwable {
1208        scrollInOtherOrientationTest(FLAG_VERTICAL | FLAG_FLING);
1209    }
1210
1211    @Test
1212    public void nestedDragVertical() throws Throwable {
1213        TestedFrameLayout tfl = getActivity().getContainer();
1214        tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
1215        tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
1216        scrollInOtherOrientationTest(FLAG_VERTICAL, 0);
1217    }
1218
1219    @Test
1220    public void nestedDragHorizontal() throws Throwable {
1221        TestedFrameLayout tfl = getActivity().getContainer();
1222        tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
1223        tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
1224        scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0);
1225    }
1226
1227    @Test
1228    public void nestedDragHorizontalCallsStopNestedScroll() throws Throwable {
1229        TestedFrameLayout tfl = getActivity().getContainer();
1230        tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
1231        tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
1232        scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0);
1233        assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled());
1234    }
1235
1236    @Test
1237    public void nestedDragVerticalCallsStopNestedScroll() throws Throwable {
1238        TestedFrameLayout tfl = getActivity().getContainer();
1239        tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
1240        tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
1241        scrollInOtherOrientationTest(FLAG_VERTICAL, 0);
1242        assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled());
1243    }
1244
1245    private void scrollInOtherOrientationTest(int flags)
1246            throws Throwable {
1247        scrollInOtherOrientationTest(flags, flags);
1248    }
1249
1250    private void scrollInOtherOrientationTest(final int flags, int expectedFlags) throws Throwable {
1251        RecyclerView recyclerView = new RecyclerView(getActivity());
1252        final AtomicBoolean scrolledHorizontal = new AtomicBoolean(false);
1253        final AtomicBoolean scrolledVertical = new AtomicBoolean(false);
1254
1255        final TestLayoutManager tlm = new TestLayoutManager() {
1256            @Override
1257            public boolean canScrollHorizontally() {
1258                return (flags & FLAG_HORIZONTAL) != 0;
1259            }
1260
1261            @Override
1262            public boolean canScrollVertically() {
1263                return (flags & FLAG_VERTICAL) != 0;
1264            }
1265
1266            @Override
1267            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1268                layoutRange(recycler, 0, 10);
1269                layoutLatch.countDown();
1270            }
1271
1272            @Override
1273            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
1274                    RecyclerView.State state) {
1275                scrolledVertical.set(true);
1276                return super.scrollVerticallyBy(dy, recycler, state);
1277            }
1278
1279            @Override
1280            public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
1281                    RecyclerView.State state) {
1282                scrolledHorizontal.set(true);
1283                return super.scrollHorizontallyBy(dx, recycler, state);
1284            }
1285        };
1286        TestAdapter adapter = new TestAdapter(100);
1287        recyclerView.setAdapter(adapter);
1288        recyclerView.setLayoutManager(tlm);
1289        tlm.expectLayouts(1);
1290        setRecyclerView(recyclerView);
1291        tlm.waitForLayout(2);
1292        if ( (flags & FLAG_FLING) != 0 ) {
1293            int flingVelocity = (mRecyclerView.getMaxFlingVelocity() +
1294                    mRecyclerView.getMinFlingVelocity()) / 2;
1295            assertEquals("fling started", (expectedFlags & FLAG_FLING) != 0,
1296                    fling(flingVelocity, flingVelocity));
1297        } else { // drag
1298            TouchUtils.dragViewTo(getInstrumentation(), recyclerView, Gravity.LEFT | Gravity.TOP,
1299                    mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2);
1300        }
1301        assertEquals("horizontally scrolled: " + tlm.mScrollHorizontallyAmount,
1302                (expectedFlags & FLAG_HORIZONTAL) != 0, scrolledHorizontal.get());
1303        assertEquals("vertically scrolled: " + tlm.mScrollVerticallyAmount,
1304                (expectedFlags & FLAG_VERTICAL) != 0, scrolledVertical.get());
1305    }
1306
1307    private boolean fling(final int velocityX, final int velocityY) throws Throwable {
1308        final AtomicBoolean didStart = new AtomicBoolean(false);
1309        runTestOnUiThread(new Runnable() {
1310            @Override
1311            public void run() {
1312                boolean result = mRecyclerView.fling(velocityX, velocityY);
1313                didStart.set(result);
1314            }
1315        });
1316        if (!didStart.get()) {
1317            return false;
1318        }
1319        waitForIdleScroll(mRecyclerView);
1320        return true;
1321    }
1322
1323    private void assertPendingUpdatesAndLayoutTest(final AdapterRunnable runnable) throws Throwable {
1324        RecyclerView recyclerView = new RecyclerView(getActivity());
1325        TestLayoutManager layoutManager = new DumbLayoutManager();
1326        final TestAdapter testAdapter = new TestAdapter(10);
1327        setupBasic(recyclerView, layoutManager, testAdapter, false);
1328        layoutManager.expectLayouts(1);
1329        runTestOnUiThread(new Runnable() {
1330            @Override
1331            public void run() {
1332                try {
1333                    runnable.run(testAdapter);
1334                } catch (Throwable throwable) {
1335                    fail("runnable has thrown an exception");
1336                }
1337                assertTrue(mRecyclerView.hasPendingAdapterUpdates());
1338            }
1339        });
1340        layoutManager.waitForLayout(1);
1341        assertFalse(mRecyclerView.hasPendingAdapterUpdates());
1342        checkForMainThreadException();
1343    }
1344
1345    private void setupBasic(RecyclerView recyclerView, TestLayoutManager tlm,
1346            TestAdapter adapter, boolean waitForFirstLayout) throws Throwable {
1347        recyclerView.setLayoutManager(tlm);
1348        recyclerView.setAdapter(adapter);
1349        if (waitForFirstLayout) {
1350            tlm.expectLayouts(1);
1351            setRecyclerView(recyclerView);
1352            tlm.waitForLayout(1);
1353        } else {
1354            setRecyclerView(recyclerView);
1355        }
1356    }
1357
1358    @Test
1359    public void hasPendingUpdatesBeforeFirstLayout() throws Throwable {
1360        RecyclerView recyclerView = new RecyclerView(getActivity());
1361        TestLayoutManager layoutManager = new DumbLayoutManager();
1362        TestAdapter testAdapter = new TestAdapter(10);
1363        setupBasic(recyclerView, layoutManager, testAdapter, false);
1364        assertTrue(mRecyclerView.hasPendingAdapterUpdates());
1365    }
1366
1367    @Test
1368    public void noPendingUpdatesAfterLayout() throws Throwable {
1369        RecyclerView recyclerView = new RecyclerView(getActivity());
1370        TestLayoutManager layoutManager = new DumbLayoutManager();
1371        TestAdapter testAdapter = new TestAdapter(10);
1372        setupBasic(recyclerView, layoutManager, testAdapter, true);
1373        assertFalse(mRecyclerView.hasPendingAdapterUpdates());
1374    }
1375
1376    @Test
1377    public void hasPendingUpdatesAfterItemIsRemoved() throws Throwable {
1378        assertPendingUpdatesAndLayoutTest(new AdapterRunnable() {
1379            @Override
1380            public void run(TestAdapter testAdapter) throws Throwable {
1381                testAdapter.deleteAndNotify(1, 1);
1382            }
1383        });
1384    }
1385    @Test
1386    public void hasPendingUpdatesAfterItemIsInserted() throws Throwable {
1387        assertPendingUpdatesAndLayoutTest(new AdapterRunnable() {
1388            @Override
1389            public void run(TestAdapter testAdapter) throws Throwable {
1390                testAdapter.addAndNotify(2, 1);
1391            }
1392        });
1393    }
1394    @Test
1395    public void hasPendingUpdatesAfterItemIsMoved() throws Throwable {
1396        assertPendingUpdatesAndLayoutTest(new AdapterRunnable() {
1397            @Override
1398            public void run(TestAdapter testAdapter) throws Throwable {
1399                testAdapter.moveItem(2, 3, true);
1400            }
1401        });
1402    }
1403    @Test
1404    public void hasPendingUpdatesAfterItemIsChanged() throws Throwable {
1405        assertPendingUpdatesAndLayoutTest(new AdapterRunnable() {
1406            @Override
1407            public void run(TestAdapter testAdapter) throws Throwable {
1408                testAdapter.changeAndNotify(2, 1);
1409            }
1410        });
1411    }
1412    @Test
1413    public void hasPendingUpdatesAfterDataSetIsChanged() throws Throwable {
1414        assertPendingUpdatesAndLayoutTest(new AdapterRunnable() {
1415            @Override
1416            public void run(TestAdapter testAdapter) {
1417                mRecyclerView.getAdapter().notifyDataSetChanged();
1418            }
1419        });
1420    }
1421
1422    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
1423    @Test
1424    public void transientStateRecycleViaAdapter() throws Throwable {
1425        transientStateRecycleTest(true, false);
1426    }
1427
1428    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
1429    @Test
1430    public void transientStateRecycleViaTransientStateCleanup() throws Throwable {
1431        transientStateRecycleTest(false, true);
1432    }
1433
1434    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
1435    @Test
1436    public void transientStateDontRecycle() throws Throwable {
1437        transientStateRecycleTest(false, false);
1438    }
1439
1440    public void transientStateRecycleTest(final boolean succeed, final boolean unsetTransientState)
1441            throws Throwable {
1442        final List<View> failedToRecycle = new ArrayList<>();
1443        final List<View> recycled = new ArrayList<>();
1444        TestAdapter testAdapter = new TestAdapter(10) {
1445            @Override
1446            public boolean onFailedToRecycleView(
1447                    TestViewHolder holder) {
1448                failedToRecycle.add(holder.itemView);
1449                if (unsetTransientState) {
1450                    setHasTransientState(holder.itemView, false);
1451                }
1452                return succeed;
1453            }
1454
1455            @Override
1456            public void onViewRecycled(TestViewHolder holder) {
1457                recycled.add(holder.itemView);
1458                super.onViewRecycled(holder);
1459            }
1460        };
1461        TestLayoutManager tlm = new TestLayoutManager() {
1462            @Override
1463            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1464                if (getChildCount() == 0) {
1465                    detachAndScrapAttachedViews(recycler);
1466                    layoutRange(recycler, 0, 5);
1467                } else {
1468                    removeAndRecycleAllViews(recycler);
1469                }
1470                if (layoutLatch != null) {
1471                    layoutLatch.countDown();
1472                }
1473            }
1474        };
1475        RecyclerView recyclerView = new RecyclerView(getActivity());
1476        recyclerView.setAdapter(testAdapter);
1477        recyclerView.setLayoutManager(tlm);
1478        recyclerView.setItemAnimator(null);
1479        setRecyclerView(recyclerView);
1480        getInstrumentation().waitForIdleSync();
1481        // make sure we have enough views after this position so that we'll receive the on recycled
1482        // callback
1483        View view = recyclerView.getChildAt(3);//this has to be greater than def cache size.
1484        setHasTransientState(view, true);
1485        tlm.expectLayouts(1);
1486        requestLayoutOnUIThread(recyclerView);
1487        tlm.waitForLayout(2);
1488
1489        assertTrue(failedToRecycle.contains(view));
1490        assertEquals(succeed || unsetTransientState, recycled.contains(view));
1491    }
1492
1493    @Test
1494    public void adapterPositionInvalidation() throws Throwable {
1495        final RecyclerView recyclerView = new RecyclerView(getActivity());
1496        final TestAdapter adapter = new TestAdapter(10);
1497        final TestLayoutManager tlm = new TestLayoutManager() {
1498            @Override
1499            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1500                layoutRange(recycler, 0, state.getItemCount());
1501                layoutLatch.countDown();
1502            }
1503        };
1504        recyclerView.setAdapter(adapter);
1505        recyclerView.setLayoutManager(tlm);
1506        tlm.expectLayouts(1);
1507        setRecyclerView(recyclerView);
1508        tlm.waitForLayout(1);
1509        runTestOnUiThread(new Runnable() {
1510            @Override
1511            public void run() {
1512                for (int i = 0; i < tlm.getChildCount(); i++) {
1513                    assertNotSame("adapter positions should not be undefined",
1514                            recyclerView.getChildAdapterPosition(tlm.getChildAt(i)),
1515                            RecyclerView.NO_POSITION);
1516                }
1517                adapter.notifyDataSetChanged();
1518                for (int i = 0; i < tlm.getChildCount(); i++) {
1519                    assertSame("adapter positions should be undefined",
1520                            recyclerView.getChildAdapterPosition(tlm.getChildAt(i)),
1521                            RecyclerView.NO_POSITION);
1522                }
1523            }
1524        });
1525    }
1526
1527    @Test
1528    public void adapterPositionsBasic() throws Throwable {
1529        adapterPositionsTest(null);
1530    }
1531
1532    @Test
1533    public void adapterPositionsRemoveItems() throws Throwable {
1534        adapterPositionsTest(new AdapterRunnable() {
1535            @Override
1536            public void run(TestAdapter adapter) throws Throwable {
1537                adapter.deleteAndNotify(3, 4);
1538            }
1539        });
1540    }
1541
1542    @Test
1543    public void adapterPositionsRemoveItemsBefore() throws Throwable {
1544        adapterPositionsTest(new AdapterRunnable() {
1545            @Override
1546            public void run(TestAdapter adapter) throws Throwable {
1547                adapter.deleteAndNotify(0, 1);
1548            }
1549        });
1550    }
1551
1552    @Test
1553    public void adapterPositionsAddItemsBefore() throws Throwable {
1554        adapterPositionsTest(new AdapterRunnable() {
1555            @Override
1556            public void run(TestAdapter adapter) throws Throwable {
1557                adapter.addAndNotify(0, 5);
1558            }
1559        });
1560    }
1561
1562    @Test
1563    public void adapterPositionsAddItemsInside() throws Throwable {
1564        adapterPositionsTest(new AdapterRunnable() {
1565            @Override
1566            public void run(TestAdapter adapter) throws Throwable {
1567                adapter.addAndNotify(3, 2);
1568            }
1569        });
1570    }
1571
1572    @Test
1573    public void adapterPositionsMoveItems() throws Throwable {
1574        adapterPositionsTest(new AdapterRunnable() {
1575            @Override
1576            public void run(TestAdapter adapter) throws Throwable {
1577                adapter.moveAndNotify(3, 5);
1578            }
1579        });
1580    }
1581
1582    @Test
1583    public void adapterPositionsNotifyDataSetChanged() throws Throwable {
1584        adapterPositionsTest(new AdapterRunnable() {
1585            @Override
1586            public void run(TestAdapter adapter) throws Throwable {
1587                adapter.mItems.clear();
1588                for (int i = 0; i < 20; i++) {
1589                    adapter.mItems.add(new Item(i, "added item"));
1590                }
1591                adapter.notifyDataSetChanged();
1592            }
1593        });
1594    }
1595
1596    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) // transientState is API 16
1597    @Test
1598    public void avoidLeakingRecyclerViewIfViewIsNotRecycled() throws Throwable {
1599        final AtomicBoolean failedToRecycle = new AtomicBoolean(false);
1600        final AtomicInteger recycledViewCount = new AtomicInteger(0);
1601        RecyclerView rv = new RecyclerView(getActivity());
1602        TestLayoutManager tlm = new TestLayoutManager() {
1603            @Override
1604            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1605                detachAndScrapAttachedViews(recycler);
1606                layoutRange(recycler, 0, state.getItemCount());
1607                layoutLatch.countDown();
1608            }
1609        };
1610        TestAdapter adapter = new TestAdapter(10) {
1611            @Override
1612            public boolean onFailedToRecycleView(
1613                    TestViewHolder holder) {
1614                failedToRecycle.set(true);
1615                return false;
1616            }
1617
1618            @Override
1619            public void onViewRecycled(TestViewHolder holder) {
1620                recycledViewCount.incrementAndGet();
1621                super.onViewRecycled(holder);
1622            }
1623        };
1624        rv.setAdapter(adapter);
1625        rv.setLayoutManager(tlm);
1626        tlm.expectLayouts(1);
1627        setRecyclerView(rv);
1628        tlm.waitForLayout(1);
1629        final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0));
1630        runTestOnUiThread(new Runnable() {
1631            @Override
1632            public void run() {
1633                ViewCompat.setHasTransientState(vh.itemView, true);
1634            }
1635        });
1636        tlm.expectLayouts(1);
1637        adapter.deleteAndNotify(0, 10);
1638        tlm.waitForLayout(2);
1639        final CountDownLatch animationsLatch = new CountDownLatch(1);
1640        rv.getItemAnimator().isRunning(
1641                new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
1642                    @Override
1643                    public void onAnimationsFinished() {
1644                        animationsLatch.countDown();
1645                    }
1646                });
1647        assertTrue(animationsLatch.await(2, TimeUnit.SECONDS));
1648        assertThat(recycledViewCount.get(), is(9));
1649        assertTrue(failedToRecycle.get());
1650        assertNull(vh.mOwnerRecyclerView);
1651        checkForMainThreadException();
1652    }
1653
1654    @Test
1655    public void avoidLeakingRecyclerViewViaViewHolder() throws Throwable {
1656        RecyclerView rv = new RecyclerView(getActivity());
1657        TestLayoutManager tlm = new TestLayoutManager() {
1658            @Override
1659            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1660                detachAndScrapAttachedViews(recycler);
1661                layoutRange(recycler, 0, state.getItemCount());
1662                layoutLatch.countDown();
1663            }
1664        };
1665        TestAdapter adapter = new TestAdapter(10);
1666        rv.setAdapter(adapter);
1667        rv.setLayoutManager(tlm);
1668        tlm.expectLayouts(1);
1669        setRecyclerView(rv);
1670        tlm.waitForLayout(1);
1671        final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0));
1672        tlm.expectLayouts(1);
1673        adapter.deleteAndNotify(0, 10);
1674        tlm.waitForLayout(2);
1675        final CountDownLatch animationsLatch = new CountDownLatch(1);
1676        rv.getItemAnimator().isRunning(
1677                new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
1678                    @Override
1679                    public void onAnimationsFinished() {
1680                        animationsLatch.countDown();
1681                    }
1682                });
1683        assertTrue(animationsLatch.await(2, TimeUnit.SECONDS));
1684        assertNull(vh.mOwnerRecyclerView);
1685        checkForMainThreadException();
1686    }
1687
1688    @Test
1689    public void duplicateAdapterPositionTest() throws Throwable {
1690        final TestAdapter testAdapter = new TestAdapter(10);
1691        final TestLayoutManager tlm = new TestLayoutManager() {
1692            @Override
1693            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1694                detachAndScrapAttachedViews(recycler);
1695                layoutRange(recycler, 0, state.getItemCount());
1696                if (!state.isPreLayout()) {
1697                    while (!recycler.getScrapList().isEmpty()) {
1698                        RecyclerView.ViewHolder viewHolder = recycler.getScrapList().get(0);
1699                        addDisappearingView(viewHolder.itemView, 0);
1700                    }
1701                }
1702                layoutLatch.countDown();
1703            }
1704
1705            @Override
1706            public boolean supportsPredictiveItemAnimations() {
1707                return true;
1708            }
1709        };
1710        final DefaultItemAnimator animator = new DefaultItemAnimator();
1711        animator.setSupportsChangeAnimations(true);
1712        animator.setChangeDuration(10000);
1713        testAdapter.setHasStableIds(true);
1714        final TestRecyclerView recyclerView = new TestRecyclerView(getActivity());
1715        recyclerView.setLayoutManager(tlm);
1716        recyclerView.setAdapter(testAdapter);
1717        recyclerView.setItemAnimator(animator);
1718
1719        tlm.expectLayouts(1);
1720        setRecyclerView(recyclerView);
1721        tlm.waitForLayout(2);
1722
1723        tlm.expectLayouts(2);
1724        testAdapter.mItems.get(2).mType += 2;
1725        final int itemId = testAdapter.mItems.get(2).mId;
1726        testAdapter.changeAndNotify(2, 1);
1727        tlm.waitForLayout(2);
1728
1729        runTestOnUiThread(new Runnable() {
1730            @Override
1731            public void run() {
1732                assertThat("test sanity", recyclerView.getChildCount(), CoreMatchers.is(11));
1733                // now mangle the order and run the test
1734                RecyclerView.ViewHolder hidden = null;
1735                RecyclerView.ViewHolder updated = null;
1736                for (int i = 0; i < recyclerView.getChildCount(); i ++) {
1737                    View view = recyclerView.getChildAt(i);
1738                    RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
1739                    if (vh.getAdapterPosition() == 2) {
1740                        if (mRecyclerView.mChildHelper.isHidden(view)) {
1741                            assertThat(hidden, CoreMatchers.nullValue());
1742                            hidden = vh;
1743                        } else {
1744                            assertThat(updated, CoreMatchers.nullValue());
1745                            updated = vh;
1746                        }
1747                    }
1748                }
1749                assertThat(hidden, CoreMatchers.notNullValue());
1750                assertThat(updated, CoreMatchers.notNullValue());
1751
1752                mRecyclerView.eatRequestLayout();
1753
1754                // first put the hidden child back
1755                int index1 = mRecyclerView.indexOfChild(hidden.itemView);
1756                int index2 = mRecyclerView.indexOfChild(updated.itemView);
1757                if (index1 < index2) {
1758                    // swap views
1759                    swapViewsAtIndices(recyclerView, index1, index2);
1760                }
1761                assertThat(tlm.findViewByPosition(2), CoreMatchers.sameInstance(updated.itemView));
1762
1763                assertThat(recyclerView.findViewHolderForAdapterPosition(2),
1764                        CoreMatchers.sameInstance(updated));
1765                assertThat(recyclerView.findViewHolderForLayoutPosition(2),
1766                        CoreMatchers.sameInstance(updated));
1767                assertThat(recyclerView.findViewHolderForItemId(itemId),
1768                        CoreMatchers.sameInstance(updated));
1769
1770                // now swap back
1771                swapViewsAtIndices(recyclerView, index1, index2);
1772
1773                assertThat(tlm.findViewByPosition(2), CoreMatchers.sameInstance(updated.itemView));
1774                assertThat(recyclerView.findViewHolderForAdapterPosition(2),
1775                        CoreMatchers.sameInstance(updated));
1776                assertThat(recyclerView.findViewHolderForLayoutPosition(2),
1777                        CoreMatchers.sameInstance(updated));
1778                assertThat(recyclerView.findViewHolderForItemId(itemId),
1779                        CoreMatchers.sameInstance(updated));
1780
1781                // now remove updated. re-assert fallback to the hidden one
1782                tlm.removeView(updated.itemView);
1783
1784                assertThat(tlm.findViewByPosition(2), CoreMatchers.nullValue());
1785                assertThat(recyclerView.findViewHolderForAdapterPosition(2),
1786                        CoreMatchers.sameInstance(hidden));
1787                assertThat(recyclerView.findViewHolderForLayoutPosition(2),
1788                        CoreMatchers.sameInstance(hidden));
1789                assertThat(recyclerView.findViewHolderForItemId(itemId),
1790                        CoreMatchers.sameInstance(hidden));
1791            }
1792        });
1793
1794    }
1795
1796    private void swapViewsAtIndices(TestRecyclerView recyclerView, int index1, int index2) {
1797        if (index1 == index2) {
1798            return;
1799        }
1800        if (index2 < index1) {
1801            int tmp = index1;
1802            index1 = index2;
1803            index2 = tmp;
1804        }
1805        final View v1 = recyclerView.getChildAt(index1);
1806        final View v2 = recyclerView.getChildAt(index2);
1807        boolean v1Hidden = recyclerView.mChildHelper.isHidden(v1);
1808        boolean v2Hidden = recyclerView.mChildHelper.isHidden(v2);
1809        // must un-hide before swap otherwise bucket indices will become invalid.
1810        if (v1Hidden) {
1811            mRecyclerView.mChildHelper.unhide(v1);
1812        }
1813        if (v2Hidden) {
1814            mRecyclerView.mChildHelper.unhide(v2);
1815        }
1816        recyclerView.detachViewFromParent(index2);
1817        recyclerView.attachViewToParent(v2, index1, v2.getLayoutParams());
1818        recyclerView.detachViewFromParent(index1 + 1);
1819        recyclerView.attachViewToParent(v1, index2, v1.getLayoutParams());
1820
1821        if (v1Hidden) {
1822            mRecyclerView.mChildHelper.hide(v1);
1823        }
1824        if (v2Hidden) {
1825            mRecyclerView.mChildHelper.hide(v2);
1826        }
1827    }
1828
1829    public void adapterPositionsTest(final AdapterRunnable adapterChanges) throws Throwable {
1830        final TestAdapter testAdapter = new TestAdapter(10);
1831        TestLayoutManager tlm = new TestLayoutManager() {
1832            @Override
1833            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1834                try {
1835                    layoutRange(recycler, Math.min(state.getItemCount(), 2)
1836                            , Math.min(state.getItemCount(), 7));
1837                    layoutLatch.countDown();
1838                } catch (Throwable t) {
1839                    postExceptionToInstrumentation(t);
1840                }
1841            }
1842        };
1843        final RecyclerView recyclerView = new RecyclerView(getActivity());
1844        recyclerView.setLayoutManager(tlm);
1845        recyclerView.setAdapter(testAdapter);
1846        tlm.expectLayouts(1);
1847        setRecyclerView(recyclerView);
1848        tlm.waitForLayout(1);
1849        runTestOnUiThread(new Runnable() {
1850            @Override
1851            public void run() {
1852                try {
1853                    final int count = recyclerView.getChildCount();
1854                    Map<View, Integer> layoutPositions = new HashMap<>();
1855                    assertTrue("test sanity", count > 0);
1856                    for (int i = 0; i < count; i++) {
1857                        View view = recyclerView.getChildAt(i);
1858                        TestViewHolder vh = (TestViewHolder) recyclerView.getChildViewHolder(view);
1859                        int index = testAdapter.mItems.indexOf(vh.mBoundItem);
1860                        assertEquals("should be able to find VH with adapter position " + index, vh,
1861                                recyclerView.findViewHolderForAdapterPosition(index));
1862                        assertEquals("get adapter position should return correct index", index,
1863                                vh.getAdapterPosition());
1864                        layoutPositions.put(view, vh.mPosition);
1865                    }
1866                    if (adapterChanges != null) {
1867                        adapterChanges.run(testAdapter);
1868                        for (int i = 0; i < count; i++) {
1869                            View view = recyclerView.getChildAt(i);
1870                            TestViewHolder vh = (TestViewHolder) recyclerView
1871                                    .getChildViewHolder(view);
1872                            int index = testAdapter.mItems.indexOf(vh.mBoundItem);
1873                            if (index >= 0) {
1874                                assertEquals("should be able to find VH with adapter position "
1875                                                + index, vh,
1876                                        recyclerView.findViewHolderForAdapterPosition(index));
1877                            }
1878                            assertSame("get adapter position should return correct index", index,
1879                                    vh.getAdapterPosition());
1880                            assertSame("should be able to find view with layout position",
1881                                    vh, mRecyclerView.findViewHolderForLayoutPosition(
1882                                            layoutPositions.get(view)));
1883                        }
1884
1885                    }
1886
1887                } catch (Throwable t) {
1888                    postExceptionToInstrumentation(t);
1889                }
1890            }
1891        });
1892        checkForMainThreadException();
1893    }
1894
1895    @Test
1896    public void scrollStateForSmoothScroll() throws Throwable {
1897        TestAdapter testAdapter = new TestAdapter(10);
1898        TestLayoutManager tlm = new TestLayoutManager();
1899        RecyclerView recyclerView = new RecyclerView(getActivity());
1900        recyclerView.setAdapter(testAdapter);
1901        recyclerView.setLayoutManager(tlm);
1902        setRecyclerView(recyclerView);
1903        getInstrumentation().waitForIdleSync();
1904        assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1905        final int[] stateCnts = new int[10];
1906        final CountDownLatch latch = new CountDownLatch(2);
1907        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
1908            @Override
1909            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
1910                stateCnts[newState] = stateCnts[newState] + 1;
1911                latch.countDown();
1912            }
1913        });
1914        runTestOnUiThread(new Runnable() {
1915            @Override
1916            public void run() {
1917                mRecyclerView.smoothScrollBy(0, 500);
1918            }
1919        });
1920        latch.await(5, TimeUnit.SECONDS);
1921        assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]);
1922        assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
1923        assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]);
1924    }
1925
1926    @Test
1927    public void scrollStateForSmoothScrollWithStop() throws Throwable {
1928        TestAdapter testAdapter = new TestAdapter(10);
1929        TestLayoutManager tlm = new TestLayoutManager();
1930        RecyclerView recyclerView = new RecyclerView(getActivity());
1931        recyclerView.setAdapter(testAdapter);
1932        recyclerView.setLayoutManager(tlm);
1933        setRecyclerView(recyclerView);
1934        getInstrumentation().waitForIdleSync();
1935        assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1936        final int[] stateCnts = new int[10];
1937        final CountDownLatch latch = new CountDownLatch(1);
1938        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
1939            @Override
1940            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
1941                stateCnts[newState] = stateCnts[newState] + 1;
1942                latch.countDown();
1943            }
1944        });
1945        runTestOnUiThread(new Runnable() {
1946            @Override
1947            public void run() {
1948                mRecyclerView.smoothScrollBy(0, 500);
1949            }
1950        });
1951        latch.await(5, TimeUnit.SECONDS);
1952        runTestOnUiThread(new Runnable() {
1953            @Override
1954            public void run() {
1955                mRecyclerView.stopScroll();
1956            }
1957        });
1958        assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1959        assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]);
1960        assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
1961        assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]);
1962    }
1963
1964    @Test
1965    public void scrollStateForFling() throws Throwable {
1966        TestAdapter testAdapter = new TestAdapter(10);
1967        TestLayoutManager tlm = new TestLayoutManager();
1968        RecyclerView recyclerView = new RecyclerView(getActivity());
1969        recyclerView.setAdapter(testAdapter);
1970        recyclerView.setLayoutManager(tlm);
1971        setRecyclerView(recyclerView);
1972        getInstrumentation().waitForIdleSync();
1973        assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1974        final int[] stateCnts = new int[10];
1975        final CountDownLatch latch = new CountDownLatch(2);
1976        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
1977            @Override
1978            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
1979                stateCnts[newState] = stateCnts[newState] + 1;
1980                latch.countDown();
1981            }
1982        });
1983        final ViewConfiguration vc = ViewConfiguration.get(getActivity());
1984        final float fling = vc.getScaledMinimumFlingVelocity()
1985                + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .1f;
1986        runTestOnUiThread(new Runnable() {
1987            @Override
1988            public void run() {
1989                mRecyclerView.fling(0, Math.round(fling));
1990            }
1991        });
1992        latch.await(5, TimeUnit.SECONDS);
1993        assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]);
1994        assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
1995        assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]);
1996    }
1997
1998    @Test
1999    public void scrollStateForFlingWithStop() throws Throwable {
2000        TestAdapter testAdapter = new TestAdapter(10);
2001        TestLayoutManager tlm = new TestLayoutManager();
2002        RecyclerView recyclerView = new RecyclerView(getActivity());
2003        recyclerView.setAdapter(testAdapter);
2004        recyclerView.setLayoutManager(tlm);
2005        setRecyclerView(recyclerView);
2006        getInstrumentation().waitForIdleSync();
2007        assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
2008        final int[] stateCnts = new int[10];
2009        final CountDownLatch latch = new CountDownLatch(1);
2010        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
2011            @Override
2012            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
2013                stateCnts[newState] = stateCnts[newState] + 1;
2014                latch.countDown();
2015            }
2016        });
2017        final ViewConfiguration vc = ViewConfiguration.get(getActivity());
2018        final float fling = vc.getScaledMinimumFlingVelocity()
2019                + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .8f;
2020        runTestOnUiThread(new Runnable() {
2021            @Override
2022            public void run() {
2023                mRecyclerView.fling(0, Math.round(fling));
2024            }
2025        });
2026        latch.await(5, TimeUnit.SECONDS);
2027        runTestOnUiThread(new Runnable() {
2028            @Override
2029            public void run() {
2030                mRecyclerView.stopScroll();
2031            }
2032        });
2033        assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
2034        assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]);
2035        assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
2036        assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]);
2037    }
2038
2039    @Test
2040    public void scrollStateDrag() throws Throwable {
2041        TestAdapter testAdapter = new TestAdapter(10);
2042        TestLayoutManager tlm = new TestLayoutManager();
2043        RecyclerView recyclerView = new RecyclerView(getActivity());
2044        recyclerView.setAdapter(testAdapter);
2045        recyclerView.setLayoutManager(tlm);
2046        setRecyclerView(recyclerView);
2047        getInstrumentation().waitForIdleSync();
2048        assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
2049        final int[] stateCnts = new int[10];
2050        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
2051            @Override
2052            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
2053                stateCnts[newState] = stateCnts[newState] + 1;
2054            }
2055        });
2056        drag(mRecyclerView, 0, 0, 0, 500, 5);
2057        assertEquals(0, stateCnts[SCROLL_STATE_SETTLING]);
2058        assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
2059        assertEquals(1, stateCnts[SCROLL_STATE_DRAGGING]);
2060    }
2061
2062    public void drag(ViewGroup view, float fromX, float toX, float fromY, float toY,
2063            int stepCount) throws Throwable {
2064        long downTime = SystemClock.uptimeMillis();
2065        long eventTime = SystemClock.uptimeMillis();
2066
2067        float y = fromY;
2068        float x = fromX;
2069
2070        float yStep = (toY - fromY) / stepCount;
2071        float xStep = (toX - fromX) / stepCount;
2072
2073        MotionEvent event = MotionEvent.obtain(downTime, eventTime,
2074                MotionEvent.ACTION_DOWN, x, y, 0);
2075        sendTouch(view, event);
2076        for (int i = 0; i < stepCount; ++i) {
2077            y += yStep;
2078            x += xStep;
2079            eventTime = SystemClock.uptimeMillis();
2080            event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0);
2081            sendTouch(view, event);
2082        }
2083
2084        eventTime = SystemClock.uptimeMillis();
2085        event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0);
2086        sendTouch(view, event);
2087        getInstrumentation().waitForIdleSync();
2088    }
2089
2090    private void sendTouch(final ViewGroup view, final MotionEvent event) throws Throwable {
2091        runTestOnUiThread(new Runnable() {
2092            @Override
2093            public void run() {
2094                if (view.onInterceptTouchEvent(event)) {
2095                    view.onTouchEvent(event);
2096                }
2097            }
2098        });
2099    }
2100
2101    @Test
2102    public void recycleScrap() throws Throwable {
2103        recycleScrapTest(false);
2104        removeRecyclerView();
2105        recycleScrapTest(true);
2106    }
2107
2108    public void recycleScrapTest(final boolean useRecycler) throws Throwable {
2109        TestAdapter testAdapter = new TestAdapter(10);
2110        final AtomicBoolean test = new AtomicBoolean(false);
2111        TestLayoutManager lm = new TestLayoutManager() {
2112            @Override
2113            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2114                ViewInfoStore infoStore = mRecyclerView.mViewInfoStore;
2115                if (test.get()) {
2116                    try {
2117                        detachAndScrapAttachedViews(recycler);
2118                        for (int i = recycler.getScrapList().size() - 1; i >= 0; i--) {
2119                            if (useRecycler) {
2120                                recycler.recycleView(recycler.getScrapList().get(i).itemView);
2121                            } else {
2122                                removeAndRecycleView(recycler.getScrapList().get(i).itemView,
2123                                        recycler);
2124                            }
2125                        }
2126                        if (infoStore.mOldChangedHolders != null) {
2127                            for (int i = infoStore.mOldChangedHolders.size() - 1; i >= 0; i--) {
2128                                if (useRecycler) {
2129                                    recycler.recycleView(
2130                                            infoStore.mOldChangedHolders.valueAt(i).itemView);
2131                                } else {
2132                                    removeAndRecycleView(
2133                                            infoStore.mOldChangedHolders.valueAt(i).itemView,
2134                                            recycler);
2135                                }
2136                            }
2137                        }
2138                        assertEquals("no scrap should be left over", 0, recycler.getScrapCount());
2139                        assertEquals("pre layout map should be empty", 0,
2140                                InfoStoreTrojan.sizeOfPreLayout(infoStore));
2141                        assertEquals("post layout map should be empty", 0,
2142                                InfoStoreTrojan.sizeOfPostLayout(infoStore));
2143                        if (infoStore.mOldChangedHolders != null) {
2144                            assertEquals("post old change map should be empty", 0,
2145                                    infoStore.mOldChangedHolders.size());
2146                        }
2147                    } catch (Throwable t) {
2148                        postExceptionToInstrumentation(t);
2149                    }
2150
2151                }
2152                layoutRange(recycler, 0, 5);
2153                layoutLatch.countDown();
2154                super.onLayoutChildren(recycler, state);
2155            }
2156        };
2157        RecyclerView recyclerView = new RecyclerView(getActivity());
2158        recyclerView.setAdapter(testAdapter);
2159        recyclerView.setLayoutManager(lm);
2160        ((SimpleItemAnimator)recyclerView.getItemAnimator()).setSupportsChangeAnimations(true);
2161        lm.expectLayouts(1);
2162        setRecyclerView(recyclerView);
2163        lm.waitForLayout(2);
2164        test.set(true);
2165        lm.expectLayouts(1);
2166        testAdapter.changeAndNotify(3, 1);
2167        lm.waitForLayout(2);
2168        checkForMainThreadException();
2169    }
2170
2171    @Test
2172    public void aAccessRecyclerOnOnMeasureWithPredictive() throws Throwable {
2173        accessRecyclerOnOnMeasureTest(true);
2174    }
2175
2176    @Test
2177    public void accessRecyclerOnOnMeasureWithoutPredictive() throws Throwable {
2178        accessRecyclerOnOnMeasureTest(false);
2179    }
2180
2181    @Test
2182    public void smoothScrollWithRemovedItemsAndRemoveItem() throws Throwable {
2183        smoothScrollTest(true);
2184    }
2185
2186    @Test
2187    public void smoothScrollWithRemovedItems() throws Throwable {
2188        smoothScrollTest(false);
2189    }
2190
2191    public void smoothScrollTest(final boolean removeItem) throws Throwable {
2192        final LinearSmoothScroller[] lss = new LinearSmoothScroller[1];
2193        final CountDownLatch calledOnStart = new CountDownLatch(1);
2194        final CountDownLatch calledOnStop = new CountDownLatch(1);
2195        final int visibleChildCount = 10;
2196        TestLayoutManager lm = new TestLayoutManager() {
2197            int start = 0;
2198
2199            @Override
2200            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2201                super.onLayoutChildren(recycler, state);
2202                layoutRange(recycler, start, visibleChildCount);
2203                layoutLatch.countDown();
2204            }
2205
2206            @Override
2207            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
2208                    RecyclerView.State state) {
2209                start++;
2210                if (DEBUG) {
2211                    Log.d(TAG, "on scroll, remove and recycling. start:" + start + ", cnt:"
2212                            + visibleChildCount);
2213                }
2214                removeAndRecycleAllViews(recycler);
2215                layoutRange(recycler, start,
2216                        Math.max(state.getItemCount(), start + visibleChildCount));
2217                return dy;
2218            }
2219
2220            @Override
2221            public boolean canScrollVertically() {
2222                return true;
2223            }
2224
2225            @Override
2226            public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
2227                    int position) {
2228                LinearSmoothScroller linearSmoothScroller =
2229                        new LinearSmoothScroller(recyclerView.getContext()) {
2230                            @Override
2231                            public PointF computeScrollVectorForPosition(int targetPosition) {
2232                                return new PointF(0, 1);
2233                            }
2234
2235                            @Override
2236                            protected void onStart() {
2237                                super.onStart();
2238                                calledOnStart.countDown();
2239                            }
2240
2241                            @Override
2242                            protected void onStop() {
2243                                super.onStop();
2244                                calledOnStop.countDown();
2245                            }
2246                        };
2247                linearSmoothScroller.setTargetPosition(position);
2248                lss[0] = linearSmoothScroller;
2249                startSmoothScroll(linearSmoothScroller);
2250            }
2251        };
2252        final RecyclerView rv = new RecyclerView(getActivity());
2253        TestAdapter testAdapter = new TestAdapter(500);
2254        rv.setLayoutManager(lm);
2255        rv.setAdapter(testAdapter);
2256        lm.expectLayouts(1);
2257        setRecyclerView(rv);
2258        lm.waitForLayout(1);
2259        // regular scroll
2260        final int targetPosition = visibleChildCount * (removeItem ? 30 : 4);
2261        runTestOnUiThread(new Runnable() {
2262            @Override
2263            public void run() {
2264                rv.smoothScrollToPosition(targetPosition);
2265            }
2266        });
2267        if (DEBUG) {
2268            Log.d(TAG, "scrolling to target position " + targetPosition);
2269        }
2270        assertTrue("on start should be called very soon", calledOnStart.await(2, TimeUnit.SECONDS));
2271        if (removeItem) {
2272            final int newTarget = targetPosition - 10;
2273            testAdapter.deleteAndNotify(newTarget + 1, testAdapter.getItemCount() - newTarget - 1);
2274            final CountDownLatch targetCheck = new CountDownLatch(1);
2275            runTestOnUiThread(new Runnable() {
2276                @Override
2277                public void run() {
2278                    ViewCompat.postOnAnimationDelayed(rv, new Runnable() {
2279                        @Override
2280                        public void run() {
2281                            try {
2282                                assertEquals("scroll position should be updated to next available",
2283                                        newTarget, lss[0].getTargetPosition());
2284                            } catch (Throwable t) {
2285                                postExceptionToInstrumentation(t);
2286                            }
2287                            targetCheck.countDown();
2288                        }
2289                    }, 50);
2290                }
2291            });
2292            assertTrue("target position should be checked on time ",
2293                    targetCheck.await(10, TimeUnit.SECONDS));
2294            checkForMainThreadException();
2295            assertTrue("on stop should be called", calledOnStop.await(30, TimeUnit.SECONDS));
2296            checkForMainThreadException();
2297            assertNotNull("should scroll to new target " + newTarget
2298                    , rv.findViewHolderForLayoutPosition(newTarget));
2299            if (DEBUG) {
2300                Log.d(TAG, "on stop has been called on time");
2301            }
2302        } else {
2303            assertTrue("on stop should be called eventually",
2304                    calledOnStop.await(30, TimeUnit.SECONDS));
2305            assertNotNull("scroll to position should succeed",
2306                    rv.findViewHolderForLayoutPosition(targetPosition));
2307        }
2308        checkForMainThreadException();
2309    }
2310
2311    @Test
2312    public void consecutiveSmoothScroll() throws Throwable {
2313        final AtomicInteger visibleChildCount = new AtomicInteger(10);
2314        final AtomicInteger totalScrolled = new AtomicInteger(0);
2315        final TestLayoutManager lm = new TestLayoutManager() {
2316            int start = 0;
2317
2318            @Override
2319            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2320                super.onLayoutChildren(recycler, state);
2321                layoutRange(recycler, start, visibleChildCount.get());
2322                layoutLatch.countDown();
2323            }
2324
2325            @Override
2326            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
2327                    RecyclerView.State state) {
2328                totalScrolled.set(totalScrolled.get() + dy);
2329                return dy;
2330            }
2331
2332            @Override
2333            public boolean canScrollVertically() {
2334                return true;
2335            }
2336        };
2337        final RecyclerView rv = new RecyclerView(getActivity());
2338        TestAdapter testAdapter = new TestAdapter(500);
2339        rv.setLayoutManager(lm);
2340        rv.setAdapter(testAdapter);
2341        lm.expectLayouts(1);
2342        setRecyclerView(rv);
2343        lm.waitForLayout(1);
2344        runTestOnUiThread(new Runnable() {
2345            @Override
2346            public void run() {
2347                rv.smoothScrollBy(0, 2000);
2348            }
2349        });
2350        Thread.sleep(250);
2351        final AtomicInteger scrollAmt = new AtomicInteger();
2352        runTestOnUiThread(new Runnable() {
2353            @Override
2354            public void run() {
2355                final int soFar = totalScrolled.get();
2356                scrollAmt.set(soFar);
2357                rv.smoothScrollBy(0, 5000 - soFar);
2358            }
2359        });
2360        while (rv.getScrollState() != SCROLL_STATE_IDLE) {
2361            Thread.sleep(100);
2362        }
2363        final int soFar = totalScrolled.get();
2364        assertEquals("second scroll should be competed properly", 5000, soFar);
2365    }
2366
2367    public void accessRecyclerOnOnMeasureTest(final boolean enablePredictiveAnimations)
2368            throws Throwable {
2369        TestAdapter testAdapter = new TestAdapter(10);
2370        final AtomicInteger expectedOnMeasureStateCount = new AtomicInteger(10);
2371        TestLayoutManager lm = new TestLayoutManager() {
2372            @Override
2373            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2374                super.onLayoutChildren(recycler, state);
2375                try {
2376                    layoutRange(recycler, 0, state.getItemCount());
2377                    layoutLatch.countDown();
2378                } catch (Throwable t) {
2379                    postExceptionToInstrumentation(t);
2380                } finally {
2381                    layoutLatch.countDown();
2382                }
2383            }
2384
2385            @Override
2386            public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
2387                    int widthSpec, int heightSpec) {
2388                try {
2389                    // make sure we access all views
2390                    for (int i = 0; i < state.getItemCount(); i++) {
2391                        View view = recycler.getViewForPosition(i);
2392                        assertNotNull(view);
2393                        assertEquals(i, getPosition(view));
2394                    }
2395                    if (!state.isPreLayout()) {
2396                        assertEquals(state.toString(),
2397                                expectedOnMeasureStateCount.get(), state.getItemCount());
2398                    }
2399                } catch (Throwable t) {
2400                    postExceptionToInstrumentation(t);
2401                }
2402                super.onMeasure(recycler, state, widthSpec, heightSpec);
2403            }
2404
2405            @Override
2406            public boolean supportsPredictiveItemAnimations() {
2407                return enablePredictiveAnimations;
2408            }
2409        };
2410        RecyclerView recyclerView = new RecyclerView(getActivity());
2411        recyclerView.setLayoutManager(lm);
2412        recyclerView.setAdapter(testAdapter);
2413        recyclerView.setLayoutManager(lm);
2414        lm.expectLayouts(1);
2415        setRecyclerView(recyclerView);
2416        lm.waitForLayout(2);
2417        checkForMainThreadException();
2418        lm.expectLayouts(1);
2419        if (!enablePredictiveAnimations) {
2420            expectedOnMeasureStateCount.set(15);
2421        }
2422        testAdapter.addAndNotify(4, 5);
2423        lm.waitForLayout(2);
2424        checkForMainThreadException();
2425    }
2426
2427    @Test
2428    public void setCompatibleAdapter() throws Throwable {
2429        compatibleAdapterTest(true, true);
2430        removeRecyclerView();
2431        compatibleAdapterTest(false, true);
2432        removeRecyclerView();
2433        compatibleAdapterTest(true, false);
2434        removeRecyclerView();
2435        compatibleAdapterTest(false, false);
2436        removeRecyclerView();
2437    }
2438
2439    private void compatibleAdapterTest(boolean useCustomPool, boolean removeAndRecycleExistingViews)
2440            throws Throwable {
2441        TestAdapter testAdapter = new TestAdapter(10);
2442        final AtomicInteger recycledViewCount = new AtomicInteger();
2443        TestLayoutManager lm = new TestLayoutManager() {
2444            @Override
2445            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2446                try {
2447                    layoutRange(recycler, 0, state.getItemCount());
2448                    layoutLatch.countDown();
2449                } catch (Throwable t) {
2450                    postExceptionToInstrumentation(t);
2451                } finally {
2452                    layoutLatch.countDown();
2453                }
2454            }
2455        };
2456        RecyclerView recyclerView = new RecyclerView(getActivity());
2457        recyclerView.setLayoutManager(lm);
2458        recyclerView.setAdapter(testAdapter);
2459        recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() {
2460            @Override
2461            public void onViewRecycled(RecyclerView.ViewHolder holder) {
2462                recycledViewCount.incrementAndGet();
2463            }
2464        });
2465        lm.expectLayouts(1);
2466        setRecyclerView(recyclerView, !useCustomPool);
2467        lm.waitForLayout(2);
2468        checkForMainThreadException();
2469        lm.expectLayouts(1);
2470        swapAdapter(new TestAdapter(10), removeAndRecycleExistingViews);
2471        lm.waitForLayout(2);
2472        checkForMainThreadException();
2473        if (removeAndRecycleExistingViews) {
2474            assertTrue("Previous views should be recycled", recycledViewCount.get() > 0);
2475        } else {
2476            assertEquals("No views should be recycled if adapters are compatible and developer "
2477                    + "did not request a recycle", 0, recycledViewCount.get());
2478        }
2479    }
2480
2481    @Test
2482    public void setIncompatibleAdapter() throws Throwable {
2483        incompatibleAdapterTest(true);
2484        incompatibleAdapterTest(false);
2485    }
2486
2487    public void incompatibleAdapterTest(boolean useCustomPool) throws Throwable {
2488        TestAdapter testAdapter = new TestAdapter(10);
2489        TestLayoutManager lm = new TestLayoutManager() {
2490            @Override
2491            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2492                super.onLayoutChildren(recycler, state);
2493                try {
2494                    layoutRange(recycler, 0, state.getItemCount());
2495                    layoutLatch.countDown();
2496                } catch (Throwable t) {
2497                    postExceptionToInstrumentation(t);
2498                } finally {
2499                    layoutLatch.countDown();
2500                }
2501            }
2502        };
2503        RecyclerView recyclerView = new RecyclerView(getActivity());
2504        recyclerView.setLayoutManager(lm);
2505        recyclerView.setAdapter(testAdapter);
2506        recyclerView.setLayoutManager(lm);
2507        lm.expectLayouts(1);
2508        setRecyclerView(recyclerView, !useCustomPool);
2509        lm.waitForLayout(2);
2510        checkForMainThreadException();
2511        lm.expectLayouts(1);
2512        setAdapter(new TestAdapter2(10));
2513        lm.waitForLayout(2);
2514        checkForMainThreadException();
2515    }
2516
2517    @Test
2518    public void recycleIgnored() throws Throwable {
2519        final TestAdapter adapter = new TestAdapter(10);
2520        final TestLayoutManager lm = new TestLayoutManager() {
2521            @Override
2522            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2523                layoutRange(recycler, 0, 5);
2524                layoutLatch.countDown();
2525            }
2526        };
2527        final RecyclerView recyclerView = new RecyclerView(getActivity());
2528        recyclerView.setAdapter(adapter);
2529        recyclerView.setLayoutManager(lm);
2530        lm.expectLayouts(1);
2531        setRecyclerView(recyclerView);
2532        lm.waitForLayout(2);
2533        runTestOnUiThread(new Runnable() {
2534            @Override
2535            public void run() {
2536                View child1 = lm.findViewByPosition(0);
2537                View child2 = lm.findViewByPosition(1);
2538                lm.ignoreView(child1);
2539                lm.ignoreView(child2);
2540
2541                lm.removeAndRecycleAllViews(recyclerView.mRecycler);
2542                assertEquals("ignored child should not be recycled or removed", 2,
2543                        lm.getChildCount());
2544
2545                Throwable[] throwables = new Throwable[1];
2546                try {
2547                    lm.removeAndRecycleView(child1, mRecyclerView.mRecycler);
2548                } catch (Throwable t) {
2549                    throwables[0] = t;
2550                }
2551                assertTrue("Trying to recycle an ignored view should throw IllegalArgException "
2552                        , throwables[0] instanceof IllegalArgumentException);
2553                lm.removeAllViews();
2554                assertEquals("ignored child should be removed as well ", 0, lm.getChildCount());
2555            }
2556        });
2557    }
2558
2559    @Test
2560    public void findIgnoredByPosition() throws Throwable {
2561        final TestAdapter adapter = new TestAdapter(10);
2562        final TestLayoutManager lm = new TestLayoutManager() {
2563            @Override
2564            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2565                detachAndScrapAttachedViews(recycler);
2566                layoutRange(recycler, 0, 5);
2567                layoutLatch.countDown();
2568            }
2569        };
2570        final RecyclerView recyclerView = new RecyclerView(getActivity());
2571        recyclerView.setAdapter(adapter);
2572        recyclerView.setLayoutManager(lm);
2573        lm.expectLayouts(1);
2574        setRecyclerView(recyclerView);
2575        lm.waitForLayout(2);
2576        Thread.sleep(5000);
2577        final int pos = 1;
2578        final View[] ignored = new View[1];
2579        runTestOnUiThread(new Runnable() {
2580            @Override
2581            public void run() {
2582                View child = lm.findViewByPosition(pos);
2583                lm.ignoreView(child);
2584                ignored[0] = child;
2585            }
2586        });
2587        assertNotNull("ignored child should not be null", ignored[0]);
2588        assertNull("find view by position should not return ignored child",
2589                lm.findViewByPosition(pos));
2590        lm.expectLayouts(1);
2591        requestLayoutOnUIThread(mRecyclerView);
2592        lm.waitForLayout(1);
2593        assertEquals("child count should be ", 6, lm.getChildCount());
2594        View replacement = lm.findViewByPosition(pos);
2595        assertNotNull("re-layout should replace ignored child w/ another one", replacement);
2596        assertNotSame("replacement should be a different view", replacement, ignored[0]);
2597    }
2598
2599    @Test
2600    public void itemDecorsWithPredictive() throws Throwable {
2601        LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
2602        lm.setSupportsPredictive(true);
2603        final Object changePayload = new Object();
2604        final TestAdapter adapter = new TestAdapter(10) {
2605            @Override
2606            public void onBindViewHolder(TestViewHolder holder,
2607                    int position, List<Object> payloads) {
2608                super.onBindViewHolder(holder, position);
2609                holder.setData(payloads.isEmpty() ? null : payloads.get(0));
2610            }
2611        };
2612        final Map<Integer, Object> preLayoutData = new HashMap<>();
2613        final Map<Integer, Object> postLayoutData = new HashMap<>();
2614
2615        final RecyclerView.ItemDecoration decoration = new RecyclerView.ItemDecoration() {
2616            @Override
2617            public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
2618                    RecyclerView.State state) {
2619                try {
2620                    TestViewHolder tvh = (TestViewHolder) parent.getChildViewHolder(view);
2621                    Object data = tvh.getData();
2622                    int adapterPos = tvh.getAdapterPosition();
2623                    assertThat(adapterPos, is(not(NO_POSITION)));
2624                    if (state.isPreLayout()) {
2625                        preLayoutData.put(adapterPos, data);
2626                    } else {
2627                        postLayoutData.put(adapterPos, data);
2628                    }
2629                } catch (Throwable t) {
2630                    postExceptionToInstrumentation(t);
2631                }
2632
2633            }
2634        };
2635        RecyclerView rv = new RecyclerView(getActivity());
2636        rv.addItemDecoration(decoration);
2637        rv.setAdapter(adapter);
2638        rv.setLayoutManager(lm);
2639        lm.expectLayouts(1);
2640        setRecyclerView(rv);
2641        lm.waitForLayout(2);
2642
2643        preLayoutData.clear();
2644        postLayoutData.clear();
2645        lm.expectLayouts(2);
2646        runTestOnUiThread(new Runnable() {
2647            @Override
2648            public void run() {
2649                adapter.notifyItemChanged(3, changePayload);
2650            }
2651        });
2652        lm.waitForLayout(2);
2653        assertThat(preLayoutData.containsKey(3), is(false));
2654        assertThat(postLayoutData.get(3), is(changePayload));
2655        assertThat(preLayoutData.size(), is(0));
2656        assertThat(postLayoutData.size(), is(1));
2657        checkForMainThreadException();
2658    }
2659
2660    @Test
2661    public void invalidateAllDecorOffsets() throws Throwable {
2662        final TestAdapter adapter = new TestAdapter(10);
2663        final RecyclerView recyclerView = new RecyclerView(getActivity());
2664        final AtomicBoolean invalidatedOffsets = new AtomicBoolean(true);
2665        recyclerView.setAdapter(adapter);
2666        final AtomicInteger layoutCount = new AtomicInteger(4);
2667        final RecyclerView.ItemDecoration dummyItemDecoration = new RecyclerView.ItemDecoration() {
2668        };
2669        TestLayoutManager testLayoutManager = new TestLayoutManager() {
2670            @Override
2671            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2672                try {
2673                    // test
2674                    for (int i = 0; i < getChildCount(); i++) {
2675                        View child = getChildAt(i);
2676                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
2677                                child.getLayoutParams();
2678                        assertEquals(
2679                                "Decor insets validation for VH should have expected value.",
2680                                invalidatedOffsets.get(), lp.mInsetsDirty);
2681                    }
2682                    for (RecyclerView.ViewHolder vh : mRecyclerView.mRecycler.mCachedViews) {
2683                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
2684                                vh.itemView.getLayoutParams();
2685                        assertEquals(
2686                                "Decor insets invalidation in cache for VH should have expected "
2687                                        + "value.",
2688                                invalidatedOffsets.get(), lp.mInsetsDirty);
2689                    }
2690                    detachAndScrapAttachedViews(recycler);
2691                    layoutRange(recycler, 0, layoutCount.get());
2692                } catch (Throwable t) {
2693                    postExceptionToInstrumentation(t);
2694                } finally {
2695                    layoutLatch.countDown();
2696                }
2697            }
2698
2699            @Override
2700            public boolean supportsPredictiveItemAnimations() {
2701                return false;
2702            }
2703        };
2704        // first layout
2705        recyclerView.setItemViewCacheSize(5);
2706        recyclerView.setLayoutManager(testLayoutManager);
2707        testLayoutManager.expectLayouts(1);
2708        setRecyclerView(recyclerView, true, false);
2709        testLayoutManager.waitForLayout(2);
2710        checkForMainThreadException();
2711
2712        // re-layout w/o any change
2713        invalidatedOffsets.set(false);
2714        testLayoutManager.expectLayouts(1);
2715        requestLayoutOnUIThread(recyclerView);
2716        testLayoutManager.waitForLayout(1);
2717        checkForMainThreadException();
2718
2719        // invalidate w/o an item decorator
2720
2721        invalidateDecorOffsets(recyclerView);
2722        testLayoutManager.expectLayouts(1);
2723        invalidateDecorOffsets(recyclerView);
2724        testLayoutManager.assertNoLayout("layout should not happen", 2);
2725        checkForMainThreadException();
2726
2727        // set item decorator, should invalidate
2728        invalidatedOffsets.set(true);
2729        testLayoutManager.expectLayouts(1);
2730        addItemDecoration(mRecyclerView, dummyItemDecoration);
2731        testLayoutManager.waitForLayout(1);
2732        checkForMainThreadException();
2733
2734        // re-layout w/o any change
2735        invalidatedOffsets.set(false);
2736        testLayoutManager.expectLayouts(1);
2737        requestLayoutOnUIThread(recyclerView);
2738        testLayoutManager.waitForLayout(1);
2739        checkForMainThreadException();
2740
2741        // invalidate w/ item decorator
2742        invalidatedOffsets.set(true);
2743        invalidateDecorOffsets(recyclerView);
2744        testLayoutManager.expectLayouts(1);
2745        invalidateDecorOffsets(recyclerView);
2746        testLayoutManager.waitForLayout(2);
2747        checkForMainThreadException();
2748
2749        // trigger cache.
2750        layoutCount.set(3);
2751        invalidatedOffsets.set(false);
2752        testLayoutManager.expectLayouts(1);
2753        requestLayoutOnUIThread(mRecyclerView);
2754        testLayoutManager.waitForLayout(1);
2755        checkForMainThreadException();
2756        assertEquals("a view should be cached", 1, mRecyclerView.mRecycler.mCachedViews.size());
2757
2758        layoutCount.set(5);
2759        invalidatedOffsets.set(true);
2760        testLayoutManager.expectLayouts(1);
2761        invalidateDecorOffsets(recyclerView);
2762        testLayoutManager.waitForLayout(1);
2763        checkForMainThreadException();
2764
2765        // remove item decorator
2766        invalidatedOffsets.set(true);
2767        testLayoutManager.expectLayouts(1);
2768        removeItemDecoration(mRecyclerView, dummyItemDecoration);
2769        testLayoutManager.waitForLayout(1);
2770        checkForMainThreadException();
2771    }
2772
2773    public void addItemDecoration(final RecyclerView recyclerView, final
2774    RecyclerView.ItemDecoration itemDecoration) throws Throwable {
2775        runTestOnUiThread(new Runnable() {
2776            @Override
2777            public void run() {
2778                recyclerView.addItemDecoration(itemDecoration);
2779            }
2780        });
2781    }
2782
2783    public void removeItemDecoration(final RecyclerView recyclerView, final
2784    RecyclerView.ItemDecoration itemDecoration) throws Throwable {
2785        runTestOnUiThread(new Runnable() {
2786            @Override
2787            public void run() {
2788                recyclerView.removeItemDecoration(itemDecoration);
2789            }
2790        });
2791    }
2792
2793    public void invalidateDecorOffsets(final RecyclerView recyclerView) throws Throwable {
2794        runTestOnUiThread(new Runnable() {
2795            @Override
2796            public void run() {
2797                recyclerView.invalidateItemDecorations();
2798            }
2799        });
2800    }
2801
2802    @Test
2803    public void invalidateDecorOffsets() throws Throwable {
2804        final TestAdapter adapter = new TestAdapter(10);
2805        adapter.setHasStableIds(true);
2806        final RecyclerView recyclerView = new RecyclerView(getActivity());
2807        recyclerView.setAdapter(adapter);
2808
2809        final Map<Long, Boolean> changes = new HashMap<>();
2810
2811        TestLayoutManager testLayoutManager = new TestLayoutManager() {
2812            @Override
2813            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2814                try {
2815                    if (changes.size() > 0) {
2816                        // test
2817                        for (int i = 0; i < getChildCount(); i++) {
2818                            View child = getChildAt(i);
2819                            RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
2820                                    child.getLayoutParams();
2821                            RecyclerView.ViewHolder vh = lp.mViewHolder;
2822                            if (!changes.containsKey(vh.getItemId())) {
2823                                continue; //nothing to test
2824                            }
2825                            assertEquals(
2826                                    "Decor insets validation for VH should have expected value.",
2827                                    changes.get(vh.getItemId()), lp.mInsetsDirty);
2828                        }
2829                    }
2830                    detachAndScrapAttachedViews(recycler);
2831                    layoutRange(recycler, 0, state.getItemCount());
2832                } catch (Throwable t) {
2833                    postExceptionToInstrumentation(t);
2834                } finally {
2835                    layoutLatch.countDown();
2836                }
2837            }
2838
2839            @Override
2840            public boolean supportsPredictiveItemAnimations() {
2841                return false;
2842            }
2843        };
2844        recyclerView.setLayoutManager(testLayoutManager);
2845        testLayoutManager.expectLayouts(1);
2846        setRecyclerView(recyclerView);
2847        testLayoutManager.waitForLayout(2);
2848        int itemAddedTo = 5;
2849        for (int i = 0; i < itemAddedTo; i++) {
2850            changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false);
2851        }
2852        for (int i = itemAddedTo; i < mRecyclerView.getChildCount(); i++) {
2853            changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true);
2854        }
2855        testLayoutManager.expectLayouts(1);
2856        adapter.addAndNotify(5, 1);
2857        testLayoutManager.waitForLayout(2);
2858        checkForMainThreadException();
2859
2860        changes.clear();
2861        int[] changedItems = new int[]{3, 5, 6};
2862        for (int i = 0; i < adapter.getItemCount(); i++) {
2863            changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false);
2864        }
2865        for (int changedItem : changedItems) {
2866            changes.put(mRecyclerView.findViewHolderForLayoutPosition(changedItem).getItemId(),
2867                    true);
2868        }
2869        testLayoutManager.expectLayouts(1);
2870        adapter.changePositionsAndNotify(changedItems);
2871        testLayoutManager.waitForLayout(2);
2872        checkForMainThreadException();
2873
2874        for (int i = 0; i < adapter.getItemCount(); i++) {
2875            changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true);
2876        }
2877        testLayoutManager.expectLayouts(1);
2878        adapter.dispatchDataSetChanged();
2879        testLayoutManager.waitForLayout(2);
2880        checkForMainThreadException();
2881    }
2882
2883    @Test
2884    public void movingViaStableIds() throws Throwable {
2885        stableIdsMoveTest(true);
2886        removeRecyclerView();
2887        stableIdsMoveTest(false);
2888        removeRecyclerView();
2889    }
2890
2891    public void stableIdsMoveTest(final boolean supportsPredictive) throws Throwable {
2892        final TestAdapter testAdapter = new TestAdapter(10);
2893        testAdapter.setHasStableIds(true);
2894        final AtomicBoolean test = new AtomicBoolean(false);
2895        final int movedViewFromIndex = 3;
2896        final int movedViewToIndex = 6;
2897        final View[] movedView = new View[1];
2898        TestLayoutManager lm = new TestLayoutManager() {
2899            @Override
2900            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2901                detachAndScrapAttachedViews(recycler);
2902                try {
2903                    if (test.get()) {
2904                        if (state.isPreLayout()) {
2905                            View view = recycler.getViewForPosition(movedViewFromIndex, true);
2906                            assertSame("In pre layout, should be able to get moved view w/ old "
2907                                    + "position", movedView[0], view);
2908                            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
2909                            assertTrue("it should come from scrap", holder.wasReturnedFromScrap());
2910                            // clear scrap flag
2911                            holder.clearReturnedFromScrapFlag();
2912                        } else {
2913                            View view = recycler.getViewForPosition(movedViewToIndex, true);
2914                            assertSame("In post layout, should be able to get moved view w/ new "
2915                                    + "position", movedView[0], view);
2916                            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
2917                            assertTrue("it should come from scrap", holder.wasReturnedFromScrap());
2918                            // clear scrap flag
2919                            holder.clearReturnedFromScrapFlag();
2920                        }
2921                    }
2922                    layoutRange(recycler, 0, state.getItemCount());
2923                } catch (Throwable t) {
2924                    postExceptionToInstrumentation(t);
2925                } finally {
2926                    layoutLatch.countDown();
2927                }
2928
2929
2930            }
2931
2932            @Override
2933            public boolean supportsPredictiveItemAnimations() {
2934                return supportsPredictive;
2935            }
2936        };
2937        RecyclerView recyclerView = new RecyclerView(this.getActivity());
2938        recyclerView.setAdapter(testAdapter);
2939        recyclerView.setLayoutManager(lm);
2940        lm.expectLayouts(1);
2941        setRecyclerView(recyclerView);
2942        lm.waitForLayout(1);
2943
2944        movedView[0] = recyclerView.getChildAt(movedViewFromIndex);
2945        test.set(true);
2946        lm.expectLayouts(supportsPredictive ? 2 : 1);
2947        runTestOnUiThread(new Runnable() {
2948            @Override
2949            public void run() {
2950                Item item = testAdapter.mItems.remove(movedViewFromIndex);
2951                testAdapter.mItems.add(movedViewToIndex, item);
2952                testAdapter.notifyItemRemoved(movedViewFromIndex);
2953                testAdapter.notifyItemInserted(movedViewToIndex);
2954            }
2955        });
2956        lm.waitForLayout(2);
2957        checkForMainThreadException();
2958    }
2959
2960    @Test
2961    public void adapterChangeDuringLayout() throws Throwable {
2962        adapterChangeInMainThreadTest("notifyDataSetChanged", new Runnable() {
2963            @Override
2964            public void run() {
2965                mRecyclerView.getAdapter().notifyDataSetChanged();
2966            }
2967        });
2968
2969        adapterChangeInMainThreadTest("notifyItemChanged", new Runnable() {
2970            @Override
2971            public void run() {
2972                mRecyclerView.getAdapter().notifyItemChanged(2);
2973            }
2974        });
2975
2976        adapterChangeInMainThreadTest("notifyItemInserted", new Runnable() {
2977            @Override
2978            public void run() {
2979                mRecyclerView.getAdapter().notifyItemInserted(2);
2980            }
2981        });
2982        adapterChangeInMainThreadTest("notifyItemRemoved", new Runnable() {
2983            @Override
2984            public void run() {
2985                mRecyclerView.getAdapter().notifyItemRemoved(2);
2986            }
2987        });
2988    }
2989
2990    public void adapterChangeInMainThreadTest(String msg,
2991            final Runnable onLayoutRunnable) throws Throwable {
2992        setIgnoreMainThreadException(true);
2993        final AtomicBoolean doneFirstLayout = new AtomicBoolean(false);
2994        TestAdapter testAdapter = new TestAdapter(10);
2995        TestLayoutManager lm = new TestLayoutManager() {
2996            @Override
2997            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2998                super.onLayoutChildren(recycler, state);
2999                try {
3000                    layoutRange(recycler, 0, state.getItemCount());
3001                    if (doneFirstLayout.get()) {
3002                        onLayoutRunnable.run();
3003                    }
3004                } catch (Throwable t) {
3005                    postExceptionToInstrumentation(t);
3006                } finally {
3007                    layoutLatch.countDown();
3008                }
3009
3010            }
3011        };
3012        RecyclerView recyclerView = new RecyclerView(getActivity());
3013        recyclerView.setLayoutManager(lm);
3014        recyclerView.setAdapter(testAdapter);
3015        lm.expectLayouts(1);
3016        setRecyclerView(recyclerView);
3017        lm.waitForLayout(2);
3018        doneFirstLayout.set(true);
3019        lm.expectLayouts(1);
3020        requestLayoutOnUIThread(recyclerView);
3021        lm.waitForLayout(2);
3022        removeRecyclerView();
3023        assertTrue("Invalid data updates should be caught:" + msg,
3024                getMainThreadException() instanceof IllegalStateException);
3025    }
3026
3027    @Test
3028    public void adapterChangeDuringScroll() throws Throwable {
3029        for (int orientation : new int[]{OrientationHelper.HORIZONTAL,
3030                OrientationHelper.VERTICAL}) {
3031            adapterChangeDuringScrollTest("notifyDataSetChanged", orientation,
3032                    new Runnable() {
3033                        @Override
3034                        public void run() {
3035                            mRecyclerView.getAdapter().notifyDataSetChanged();
3036                        }
3037                    });
3038            adapterChangeDuringScrollTest("notifyItemChanged", orientation, new Runnable() {
3039                @Override
3040                public void run() {
3041                    mRecyclerView.getAdapter().notifyItemChanged(2);
3042                }
3043            });
3044
3045            adapterChangeDuringScrollTest("notifyItemInserted", orientation, new Runnable() {
3046                @Override
3047                public void run() {
3048                    mRecyclerView.getAdapter().notifyItemInserted(2);
3049                }
3050            });
3051            adapterChangeDuringScrollTest("notifyItemRemoved", orientation, new Runnable() {
3052                @Override
3053                public void run() {
3054                    mRecyclerView.getAdapter().notifyItemRemoved(2);
3055                }
3056            });
3057        }
3058    }
3059
3060    public void adapterChangeDuringScrollTest(String msg, final int orientation,
3061            final Runnable onScrollRunnable) throws Throwable {
3062        setIgnoreMainThreadException(true);
3063        TestAdapter testAdapter = new TestAdapter(100);
3064        TestLayoutManager lm = new TestLayoutManager() {
3065            @Override
3066            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3067                super.onLayoutChildren(recycler, state);
3068                try {
3069                    layoutRange(recycler, 0, 10);
3070                } catch (Throwable t) {
3071                    postExceptionToInstrumentation(t);
3072                } finally {
3073                    layoutLatch.countDown();
3074                }
3075            }
3076
3077            @Override
3078            public boolean canScrollVertically() {
3079                return orientation == OrientationHelper.VERTICAL;
3080            }
3081
3082            @Override
3083            public boolean canScrollHorizontally() {
3084                return orientation == OrientationHelper.HORIZONTAL;
3085            }
3086
3087            public int mockScroll() {
3088                try {
3089                    onScrollRunnable.run();
3090                } catch (Throwable t) {
3091                    postExceptionToInstrumentation(t);
3092                } finally {
3093                    layoutLatch.countDown();
3094                }
3095                return 0;
3096            }
3097
3098            @Override
3099            public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
3100                    RecyclerView.State state) {
3101                return mockScroll();
3102            }
3103
3104            @Override
3105            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
3106                    RecyclerView.State state) {
3107                return mockScroll();
3108            }
3109        };
3110        RecyclerView recyclerView = new RecyclerView(getActivity());
3111        recyclerView.setLayoutManager(lm);
3112        recyclerView.setAdapter(testAdapter);
3113        lm.expectLayouts(1);
3114        setRecyclerView(recyclerView);
3115        lm.waitForLayout(2);
3116        lm.expectLayouts(1);
3117        scrollBy(200);
3118        lm.waitForLayout(2);
3119        removeRecyclerView();
3120        assertTrue("Invalid data updates should be caught:" + msg,
3121                getMainThreadException() instanceof IllegalStateException);
3122    }
3123
3124    @Test
3125    public void recycleOnDetach() throws Throwable {
3126        final RecyclerView recyclerView = new RecyclerView(getActivity());
3127        final TestAdapter testAdapter = new TestAdapter(10);
3128        final AtomicBoolean didRunOnDetach = new AtomicBoolean(false);
3129        final TestLayoutManager lm = new TestLayoutManager() {
3130            @Override
3131            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3132                super.onLayoutChildren(recycler, state);
3133                layoutRange(recycler, 0, state.getItemCount() - 1);
3134                layoutLatch.countDown();
3135            }
3136
3137            @Override
3138            public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
3139                super.onDetachedFromWindow(view, recycler);
3140                didRunOnDetach.set(true);
3141                removeAndRecycleAllViews(recycler);
3142            }
3143        };
3144        recyclerView.setAdapter(testAdapter);
3145        recyclerView.setLayoutManager(lm);
3146        lm.expectLayouts(1);
3147        setRecyclerView(recyclerView);
3148        lm.waitForLayout(2);
3149        removeRecyclerView();
3150        assertTrue("When recycler view is removed, detach should run", didRunOnDetach.get());
3151        assertEquals("All children should be recycled", recyclerView.getChildCount(), 0);
3152    }
3153
3154    @Test
3155    public void updatesWhileDetached() throws Throwable {
3156        final RecyclerView recyclerView = new RecyclerView(getActivity());
3157        final int initialAdapterSize = 20;
3158        final TestAdapter adapter = new TestAdapter(initialAdapterSize);
3159        final AtomicInteger layoutCount = new AtomicInteger(0);
3160        TestLayoutManager lm = new TestLayoutManager() {
3161            @Override
3162            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3163                super.onLayoutChildren(recycler, state);
3164                layoutRange(recycler, 0, 5);
3165                layoutCount.incrementAndGet();
3166                layoutLatch.countDown();
3167            }
3168        };
3169        recyclerView.setAdapter(adapter);
3170        recyclerView.setLayoutManager(lm);
3171        recyclerView.setHasFixedSize(true);
3172        lm.expectLayouts(1);
3173        adapter.addAndNotify(4, 5);
3174        lm.assertNoLayout("When RV is not attached, layout should not happen", 1);
3175    }
3176
3177    @Test
3178    public void updatesAfterDetach() throws Throwable {
3179        final RecyclerView recyclerView = new RecyclerView(getActivity());
3180        final int initialAdapterSize = 20;
3181        final TestAdapter adapter = new TestAdapter(initialAdapterSize);
3182        final AtomicInteger layoutCount = new AtomicInteger(0);
3183        TestLayoutManager lm = new TestLayoutManager() {
3184            @Override
3185            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3186                super.onLayoutChildren(recycler, state);
3187                layoutRange(recycler, 0, 5);
3188                layoutCount.incrementAndGet();
3189                layoutLatch.countDown();
3190            }
3191        };
3192        recyclerView.setAdapter(adapter);
3193        recyclerView.setLayoutManager(lm);
3194        lm.expectLayouts(1);
3195        recyclerView.setHasFixedSize(true);
3196        setRecyclerView(recyclerView);
3197        lm.waitForLayout(2);
3198        lm.expectLayouts(1);
3199        final int prevLayoutCount = layoutCount.get();
3200        runTestOnUiThread(new Runnable() {
3201            @Override
3202            public void run() {
3203                try {
3204                    adapter.addAndNotify(4, 5);
3205                    removeRecyclerView();
3206                } catch (Throwable throwable) {
3207                    postExceptionToInstrumentation(throwable);
3208                }
3209            }
3210        });
3211        checkForMainThreadException();
3212
3213        lm.assertNoLayout("When RV is not attached, layout should not happen", 1);
3214        assertEquals("No extra layout should happen when detached", prevLayoutCount,
3215                layoutCount.get());
3216    }
3217
3218    @Test
3219    public void notifyDataSetChangedWithStableIds() throws Throwable {
3220        final Map<Integer, Integer> oldPositionToNewPositionMapping = new HashMap<>();
3221        final TestAdapter adapter = new TestAdapter(100) {
3222            @Override
3223            public long getItemId(int position) {
3224                return mItems.get(position).mId;
3225            }
3226        };
3227        adapter.setHasStableIds(true);
3228        final ArrayList<Item> previousItems = new ArrayList<>();
3229        previousItems.addAll(adapter.mItems);
3230
3231        final AtomicInteger layoutStart = new AtomicInteger(50);
3232        final AtomicBoolean validate = new AtomicBoolean(false);
3233        final int childCount = 10;
3234        final TestLayoutManager lm = new TestLayoutManager() {
3235            @Override
3236            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3237                try {
3238                    super.onLayoutChildren(recycler, state);
3239                    if (validate.get()) {
3240                        assertEquals("Cached views should be kept", 5, recycler
3241                                .mCachedViews.size());
3242                        for (RecyclerView.ViewHolder vh : recycler.mCachedViews) {
3243                            TestViewHolder tvh = (TestViewHolder) vh;
3244                            assertTrue("view holder should be marked for update",
3245                                    tvh.needsUpdate());
3246                            assertTrue("view holder should be marked as invalid", tvh.isInvalid());
3247                        }
3248                    }
3249                    detachAndScrapAttachedViews(recycler);
3250                    if (validate.get()) {
3251                        assertEquals("cache size should stay the same", 5,
3252                                recycler.mCachedViews.size());
3253                        assertEquals("all views should be scrapped", childCount,
3254                                recycler.getScrapList().size());
3255                        for (RecyclerView.ViewHolder vh : recycler.getScrapList()) {
3256                            // TODO create test case for type change
3257                            TestViewHolder tvh = (TestViewHolder) vh;
3258                            assertTrue("view holder should be marked for update",
3259                                    tvh.needsUpdate());
3260                            assertTrue("view holder should be marked as invalid", tvh.isInvalid());
3261                        }
3262                    }
3263                    layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount);
3264                    if (validate.get()) {
3265                        for (int i = 0; i < getChildCount(); i++) {
3266                            View view = getChildAt(i);
3267                            TestViewHolder tvh = (TestViewHolder) mRecyclerView
3268                                    .getChildViewHolder(view);
3269                            final int oldPos = previousItems.indexOf(tvh.mBoundItem);
3270                            assertEquals("view holder's position should be correct",
3271                                    oldPositionToNewPositionMapping.get(oldPos).intValue(),
3272                                    tvh.getLayoutPosition());
3273                        }
3274                    }
3275                } catch (Throwable t) {
3276                    postExceptionToInstrumentation(t);
3277                } finally {
3278                    layoutLatch.countDown();
3279                }
3280            }
3281        };
3282        final RecyclerView recyclerView = new RecyclerView(getActivity());
3283        recyclerView.setItemAnimator(null);
3284        recyclerView.setAdapter(adapter);
3285        recyclerView.setLayoutManager(lm);
3286        recyclerView.setItemViewCacheSize(10);
3287        lm.expectLayouts(1);
3288        setRecyclerView(recyclerView);
3289        lm.waitForLayout(2);
3290        checkForMainThreadException();
3291        getInstrumentation().waitForIdleSync();
3292        layoutStart.set(layoutStart.get() + 5);//55
3293        lm.expectLayouts(1);
3294        requestLayoutOnUIThread(recyclerView);
3295        lm.waitForLayout(2);
3296        validate.set(true);
3297        lm.expectLayouts(1);
3298        runTestOnUiThread(new Runnable() {
3299            @Override
3300            public void run() {
3301                try {
3302                    adapter.moveItems(false,
3303                            new int[]{50, 56}, new int[]{51, 1}, new int[]{52, 2},
3304                            new int[]{53, 54}, new int[]{60, 61}, new int[]{62, 64},
3305                            new int[]{75, 58});
3306                    for (int i = 0; i < previousItems.size(); i++) {
3307                        Item item = previousItems.get(i);
3308                        oldPositionToNewPositionMapping.put(i, adapter.mItems.indexOf(item));
3309                    }
3310                    adapter.dispatchDataSetChanged();
3311                } catch (Throwable throwable) {
3312                    postExceptionToInstrumentation(throwable);
3313                }
3314            }
3315        });
3316        lm.waitForLayout(2);
3317        checkForMainThreadException();
3318    }
3319
3320    @Test
3321    public void callbacksDuringAdapterSwap() throws Throwable {
3322        callbacksDuringAdapterChange(true);
3323    }
3324
3325    @Test
3326    public void callbacksDuringAdapterSet() throws Throwable {
3327        callbacksDuringAdapterChange(false);
3328    }
3329
3330    public void callbacksDuringAdapterChange(boolean swap) throws Throwable {
3331        final TestAdapter2 adapter1 = swap ? createBinderCheckingAdapter()
3332                : createOwnerCheckingAdapter();
3333        final TestAdapter2 adapter2 = swap ? createBinderCheckingAdapter()
3334                : createOwnerCheckingAdapter();
3335
3336        TestLayoutManager tlm = new TestLayoutManager() {
3337            @Override
3338            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3339                try {
3340                    layoutRange(recycler, 0, state.getItemCount());
3341                } catch (Throwable t) {
3342                    postExceptionToInstrumentation(t);
3343                }
3344                layoutLatch.countDown();
3345            }
3346        };
3347        RecyclerView rv = new RecyclerView(getActivity());
3348        rv.setAdapter(adapter1);
3349        rv.setLayoutManager(tlm);
3350        tlm.expectLayouts(1);
3351        setRecyclerView(rv);
3352        tlm.waitForLayout(1);
3353        checkForMainThreadException();
3354        tlm.expectLayouts(1);
3355        if (swap) {
3356            swapAdapter(adapter2, true);
3357        } else {
3358            setAdapter(adapter2);
3359        }
3360        checkForMainThreadException();
3361        tlm.waitForLayout(1);
3362        checkForMainThreadException();
3363    }
3364
3365    private TestAdapter2 createOwnerCheckingAdapter() {
3366        return new TestAdapter2(10) {
3367            @Override
3368            public void onViewRecycled(TestViewHolder2 holder) {
3369                assertSame("on recycled should be called w/ the creator adapter", this,
3370                        holder.mData);
3371                super.onViewRecycled(holder);
3372            }
3373
3374            @Override
3375            public void onBindViewHolder(TestViewHolder2 holder, int position) {
3376                super.onBindViewHolder(holder, position);
3377                assertSame("on bind should be called w/ the creator adapter", this, holder.mData);
3378            }
3379
3380            @Override
3381            public TestViewHolder2 onCreateViewHolder(ViewGroup parent,
3382                    int viewType) {
3383                final TestViewHolder2 vh = super.onCreateViewHolder(parent, viewType);
3384                vh.mData = this;
3385                return vh;
3386            }
3387        };
3388    }
3389
3390    private TestAdapter2 createBinderCheckingAdapter() {
3391        return new TestAdapter2(10) {
3392            @Override
3393            public void onViewRecycled(TestViewHolder2 holder) {
3394                assertSame("on recycled should be called w/ the creator adapter", this,
3395                        holder.mData);
3396                holder.mData = null;
3397                super.onViewRecycled(holder);
3398            }
3399
3400            @Override
3401            public void onBindViewHolder(TestViewHolder2 holder, int position) {
3402                super.onBindViewHolder(holder, position);
3403                holder.mData = this;
3404            }
3405        };
3406    }
3407
3408    @Test
3409    public void findViewById() throws Throwable {
3410        findViewByIdTest(false);
3411        removeRecyclerView();
3412        findViewByIdTest(true);
3413    }
3414
3415    public void findViewByIdTest(final boolean supportPredictive) throws Throwable {
3416        final RecyclerView recyclerView = new RecyclerView(getActivity());
3417        final int initialAdapterSize = 20;
3418        final TestAdapter adapter = new TestAdapter(initialAdapterSize);
3419        final int deleteStart = 6;
3420        final int deleteCount = 5;
3421        recyclerView.setAdapter(adapter);
3422        final AtomicBoolean assertPositions = new AtomicBoolean(false);
3423        TestLayoutManager lm = new TestLayoutManager() {
3424            @Override
3425            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3426                super.onLayoutChildren(recycler, state);
3427                if (assertPositions.get()) {
3428                    if (state.isPreLayout()) {
3429                        for (int i = 0; i < deleteStart; i++) {
3430                            View view = findViewByPosition(i);
3431                            assertNotNull("find view by position for existing items should work "
3432                                    + "fine", view);
3433                            assertFalse("view should not be marked as removed",
3434                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
3435                                            .isItemRemoved());
3436                        }
3437                        for (int i = 0; i < deleteCount; i++) {
3438                            View view = findViewByPosition(i + deleteStart);
3439                            assertNotNull("find view by position should work fine for removed "
3440                                    + "views in pre-layout", view);
3441                            assertTrue("view should be marked as removed",
3442                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
3443                                            .isItemRemoved());
3444                        }
3445                        for (int i = deleteStart + deleteCount; i < 20; i++) {
3446                            View view = findViewByPosition(i);
3447                            assertNotNull(view);
3448                            assertFalse("view should not be marked as removed",
3449                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
3450                                            .isItemRemoved());
3451                        }
3452                    } else {
3453                        for (int i = 0; i < initialAdapterSize - deleteCount; i++) {
3454                            View view = findViewByPosition(i);
3455                            assertNotNull("find view by position for existing item " + i +
3456                                    " should work fine. child count:" + getChildCount(), view);
3457                            TestViewHolder viewHolder =
3458                                    (TestViewHolder) mRecyclerView.getChildViewHolder(view);
3459                            assertSame("should be the correct item " + viewHolder
3460                                    , viewHolder.mBoundItem,
3461                                    adapter.mItems.get(viewHolder.mPosition));
3462                            assertFalse("view should not be marked as removed",
3463                                    ((RecyclerView.LayoutParams) view.getLayoutParams())
3464                                            .isItemRemoved());
3465                        }
3466                    }
3467                }
3468                detachAndScrapAttachedViews(recycler);
3469                layoutRange(recycler, state.getItemCount() - 1, -1);
3470                layoutLatch.countDown();
3471            }
3472
3473            @Override
3474            public boolean supportsPredictiveItemAnimations() {
3475                return supportPredictive;
3476            }
3477        };
3478        recyclerView.setLayoutManager(lm);
3479        lm.expectLayouts(1);
3480        setRecyclerView(recyclerView);
3481        lm.waitForLayout(2);
3482        getInstrumentation().waitForIdleSync();
3483
3484        assertPositions.set(true);
3485        lm.expectLayouts(supportPredictive ? 2 : 1);
3486        adapter.deleteAndNotify(new int[]{deleteStart, deleteCount - 1}, new int[]{deleteStart, 1});
3487        lm.waitForLayout(2);
3488    }
3489
3490    @Test
3491    public void typeForCache() throws Throwable {
3492        final AtomicInteger viewType = new AtomicInteger(1);
3493        final TestAdapter adapter = new TestAdapter(100) {
3494            @Override
3495            public int getItemViewType(int position) {
3496                return viewType.get();
3497            }
3498
3499            @Override
3500            public long getItemId(int position) {
3501                return mItems.get(position).mId;
3502            }
3503        };
3504        adapter.setHasStableIds(true);
3505        final AtomicInteger layoutStart = new AtomicInteger(2);
3506        final int childCount = 10;
3507        final TestLayoutManager lm = new TestLayoutManager() {
3508            @Override
3509            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3510                super.onLayoutChildren(recycler, state);
3511                detachAndScrapAttachedViews(recycler);
3512                layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount);
3513                layoutLatch.countDown();
3514            }
3515        };
3516        final RecyclerView recyclerView = new RecyclerView(getActivity());
3517        recyclerView.setItemAnimator(null);
3518        recyclerView.setAdapter(adapter);
3519        recyclerView.setLayoutManager(lm);
3520        recyclerView.setItemViewCacheSize(10);
3521        lm.expectLayouts(1);
3522        setRecyclerView(recyclerView);
3523        lm.waitForLayout(2);
3524        getInstrumentation().waitForIdleSync();
3525        layoutStart.set(4); // trigger a cache for 3,4
3526        lm.expectLayouts(1);
3527        requestLayoutOnUIThread(recyclerView);
3528        lm.waitForLayout(2);
3529        //
3530        viewType.incrementAndGet();
3531        layoutStart.set(2); // go back to bring views from cache
3532        lm.expectLayouts(1);
3533        adapter.mItems.remove(1);
3534        adapter.dispatchDataSetChanged();
3535        lm.waitForLayout(2);
3536        runTestOnUiThread(new Runnable() {
3537            @Override
3538            public void run() {
3539                for (int i = 2; i < 4; i++) {
3540                    RecyclerView.ViewHolder vh = recyclerView.findViewHolderForLayoutPosition(i);
3541                    assertEquals("View holder's type should match latest type", viewType.get(),
3542                            vh.getItemViewType());
3543                }
3544            }
3545        });
3546    }
3547
3548    @Test
3549    public void typeForExistingViews() throws Throwable {
3550        final AtomicInteger viewType = new AtomicInteger(1);
3551        final int invalidatedCount = 2;
3552        final int layoutStart = 2;
3553        final TestAdapter adapter = new TestAdapter(100) {
3554            @Override
3555            public int getItemViewType(int position) {
3556                return viewType.get();
3557            }
3558
3559            @Override
3560            public void onBindViewHolder(TestViewHolder holder,
3561                    int position) {
3562                super.onBindViewHolder(holder, position);
3563                if (position >= layoutStart && position < invalidatedCount + layoutStart) {
3564                    try {
3565                        assertEquals("holder type should match current view type at position " +
3566                                position, viewType.get(), holder.getItemViewType());
3567                    } catch (Throwable t) {
3568                        postExceptionToInstrumentation(t);
3569                    }
3570                }
3571            }
3572
3573            @Override
3574            public long getItemId(int position) {
3575                return mItems.get(position).mId;
3576            }
3577        };
3578        adapter.setHasStableIds(true);
3579
3580        final int childCount = 10;
3581        final TestLayoutManager lm = new TestLayoutManager() {
3582            @Override
3583            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3584                super.onLayoutChildren(recycler, state);
3585                detachAndScrapAttachedViews(recycler);
3586                layoutRange(recycler, layoutStart, layoutStart + childCount);
3587                layoutLatch.countDown();
3588            }
3589        };
3590        final RecyclerView recyclerView = new RecyclerView(getActivity());
3591        recyclerView.setAdapter(adapter);
3592        recyclerView.setLayoutManager(lm);
3593        lm.expectLayouts(1);
3594        setRecyclerView(recyclerView);
3595        lm.waitForLayout(2);
3596        getInstrumentation().waitForIdleSync();
3597        viewType.incrementAndGet();
3598        lm.expectLayouts(1);
3599        adapter.changeAndNotify(layoutStart, invalidatedCount);
3600        lm.waitForLayout(2);
3601        checkForMainThreadException();
3602    }
3603
3604
3605    @Test
3606    public void state() throws Throwable {
3607        final TestAdapter adapter = new TestAdapter(10);
3608        final RecyclerView recyclerView = new RecyclerView(getActivity());
3609        recyclerView.setAdapter(adapter);
3610        recyclerView.setItemAnimator(null);
3611        final AtomicInteger itemCount = new AtomicInteger();
3612        final AtomicBoolean structureChanged = new AtomicBoolean();
3613        TestLayoutManager testLayoutManager = new TestLayoutManager() {
3614            @Override
3615            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3616                detachAndScrapAttachedViews(recycler);
3617                layoutRange(recycler, 0, state.getItemCount());
3618                itemCount.set(state.getItemCount());
3619                structureChanged.set(state.didStructureChange());
3620                layoutLatch.countDown();
3621            }
3622        };
3623        recyclerView.setLayoutManager(testLayoutManager);
3624        testLayoutManager.expectLayouts(1);
3625        runTestOnUiThread(new Runnable() {
3626            @Override
3627            public void run() {
3628                getActivity().getContainer().addView(recyclerView);
3629            }
3630        });
3631        testLayoutManager.waitForLayout(2);
3632
3633        assertEquals("item count in state should be correct", adapter.getItemCount()
3634                , itemCount.get());
3635        assertEquals("structure changed should be true for first layout", true,
3636                structureChanged.get());
3637        Thread.sleep(1000); //wait for other layouts.
3638        testLayoutManager.expectLayouts(1);
3639        runTestOnUiThread(new Runnable() {
3640            @Override
3641            public void run() {
3642                recyclerView.requestLayout();
3643            }
3644        });
3645        testLayoutManager.waitForLayout(2);
3646        assertEquals("in second layout,structure changed should be false", false,
3647                structureChanged.get());
3648        testLayoutManager.expectLayouts(1); //
3649        adapter.deleteAndNotify(3, 2);
3650        testLayoutManager.waitForLayout(2);
3651        assertEquals("when items are removed, item count in state should be updated",
3652                adapter.getItemCount(),
3653                itemCount.get());
3654        assertEquals("structure changed should be true when items are removed", true,
3655                structureChanged.get());
3656        testLayoutManager.expectLayouts(1);
3657        adapter.addAndNotify(2, 5);
3658        testLayoutManager.waitForLayout(2);
3659
3660        assertEquals("when items are added, item count in state should be updated",
3661                adapter.getItemCount(),
3662                itemCount.get());
3663        assertEquals("structure changed should be true when items are removed", true,
3664                structureChanged.get());
3665    }
3666
3667    @Test
3668    public void detachWithoutLayoutManager() throws Throwable {
3669        final RecyclerView recyclerView = new RecyclerView(getActivity());
3670        runTestOnUiThread(new Runnable() {
3671            @Override
3672            public void run() {
3673                try {
3674                    setRecyclerView(recyclerView);
3675                    removeRecyclerView();
3676                } catch (Throwable t) {
3677                    postExceptionToInstrumentation(t);
3678                }
3679            }
3680        });
3681        checkForMainThreadException();
3682    }
3683
3684    @Test
3685    public void updateHiddenView() throws Throwable {
3686        final RecyclerView recyclerView = new RecyclerView(getActivity());
3687        final int[] preLayoutRange = new int[]{0, 10};
3688        final int[] postLayoutRange = new int[]{0, 10};
3689        final AtomicBoolean enableGetViewTest = new AtomicBoolean(false);
3690        final List<Integer> disappearingPositions = new ArrayList<>();
3691        final TestLayoutManager tlm = new TestLayoutManager() {
3692            @Override
3693            public boolean supportsPredictiveItemAnimations() {
3694                return true;
3695            }
3696
3697            @Override
3698            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3699                try {
3700                    final int[] layoutRange = state.isPreLayout() ? preLayoutRange
3701                            : postLayoutRange;
3702                    detachAndScrapAttachedViews(recycler);
3703                    layoutRange(recycler, layoutRange[0], layoutRange[1]);
3704                    if (!state.isPreLayout()) {
3705                        for (Integer position : disappearingPositions) {
3706                            // test sanity.
3707                            assertNull(findViewByPosition(position));
3708                            final View view = recycler.getViewForPosition(position);
3709                            assertNotNull(view);
3710                            addDisappearingView(view);
3711                            measureChildWithMargins(view, 0, 0);
3712                            // position item out of bounds.
3713                            view.layout(0, -500, view.getMeasuredWidth(),
3714                                    -500 + view.getMeasuredHeight());
3715                        }
3716                    }
3717                } catch (Throwable t) {
3718                    postExceptionToInstrumentation(t);
3719                }
3720                layoutLatch.countDown();
3721            }
3722        };
3723        recyclerView.getItemAnimator().setMoveDuration(4000);
3724        recyclerView.getItemAnimator().setRemoveDuration(4000);
3725        final TestAdapter adapter = new TestAdapter(100);
3726        recyclerView.setAdapter(adapter);
3727        recyclerView.setLayoutManager(tlm);
3728        tlm.expectLayouts(1);
3729        setRecyclerView(recyclerView);
3730        tlm.waitForLayout(1);
3731        checkForMainThreadException();
3732        // now, a child disappears
3733        disappearingPositions.add(0);
3734        // layout one shifted
3735        postLayoutRange[0] = 1;
3736        postLayoutRange[1] = 11;
3737        tlm.expectLayouts(2);
3738        adapter.addAndNotify(8, 1);
3739        tlm.waitForLayout(2);
3740        checkForMainThreadException();
3741
3742        tlm.expectLayouts(2);
3743        disappearingPositions.clear();
3744        // now that item should be moving, invalidate it and delete it.
3745        enableGetViewTest.set(true);
3746        runTestOnUiThread(new Runnable() {
3747            @Override
3748            public void run() {
3749                try {
3750                    assertThat("test sanity, should still be animating",
3751                            mRecyclerView.isAnimating(), CoreMatchers.is(true));
3752                    adapter.changeAndNotify(0, 1);
3753                    adapter.deleteAndNotify(0, 1);
3754                } catch (Throwable throwable) {
3755                    fail(throwable.getMessage());
3756                }
3757            }
3758        });
3759        tlm.waitForLayout(2);
3760        checkForMainThreadException();
3761    }
3762
3763    @Test
3764    public void focusBigViewOnTop() throws Throwable {
3765        focusTooBigViewTest(Gravity.TOP);
3766    }
3767
3768    @Test
3769    public void focusBigViewOnLeft() throws Throwable {
3770        focusTooBigViewTest(Gravity.LEFT);
3771    }
3772
3773    @Test
3774    public void focusBigViewOnRight() throws Throwable {
3775        focusTooBigViewTest(Gravity.RIGHT);
3776    }
3777
3778    @Test
3779    public void focusBigViewOnBottom() throws Throwable {
3780        focusTooBigViewTest(Gravity.BOTTOM);
3781    }
3782
3783    @Test
3784    public void focusBigViewOnLeftRTL() throws Throwable {
3785        focusTooBigViewTest(Gravity.LEFT, true);
3786        assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL,
3787                mRecyclerView.getLayoutManager().getLayoutDirection());
3788    }
3789
3790    @Test
3791    public void focusBigViewOnRightRTL() throws Throwable {
3792        focusTooBigViewTest(Gravity.RIGHT, true);
3793        assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL,
3794                mRecyclerView.getLayoutManager().getLayoutDirection());
3795    }
3796
3797    public void focusTooBigViewTest(final int gravity) throws Throwable {
3798        focusTooBigViewTest(gravity, false);
3799    }
3800
3801    public void focusTooBigViewTest(final int gravity, final boolean rtl) throws Throwable {
3802        RecyclerView rv = new RecyclerView(getActivity());
3803        if (rtl) {
3804            ViewCompat.setLayoutDirection(rv, ViewCompat.LAYOUT_DIRECTION_RTL);
3805        }
3806        final AtomicInteger vScrollDist = new AtomicInteger(0);
3807        final AtomicInteger hScrollDist = new AtomicInteger(0);
3808        final AtomicInteger vDesiredDist = new AtomicInteger(0);
3809        final AtomicInteger hDesiredDist = new AtomicInteger(0);
3810        TestLayoutManager tlm = new TestLayoutManager() {
3811
3812            @Override
3813            public int getLayoutDirection() {
3814                return rtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR;
3815            }
3816
3817            @Override
3818            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3819                detachAndScrapAttachedViews(recycler);
3820                final View view = recycler.getViewForPosition(0);
3821                addView(view);
3822                int left = 0, top = 0;
3823                view.setBackgroundColor(Color.rgb(0, 0, 255));
3824                switch (gravity) {
3825                    case Gravity.LEFT:
3826                    case Gravity.RIGHT:
3827                        view.measure(
3828                                View.MeasureSpec.makeMeasureSpec((int) (getWidth() * 1.5),
3829                                        View.MeasureSpec.EXACTLY),
3830                                View.MeasureSpec.makeMeasureSpec((int) (getHeight() * .9),
3831                                        View.MeasureSpec.AT_MOST));
3832                        left = gravity == Gravity.LEFT ? getWidth() - view.getMeasuredWidth() - 80
3833                                : 90;
3834                        top = 0;
3835                        if (ViewCompat.LAYOUT_DIRECTION_RTL == getLayoutDirection()) {
3836                            hDesiredDist.set((left + view.getMeasuredWidth()) - getWidth());
3837                        } else {
3838                            hDesiredDist.set(left);
3839                        }
3840                        break;
3841                    case Gravity.TOP:
3842                    case Gravity.BOTTOM:
3843                        view.measure(
3844                                View.MeasureSpec.makeMeasureSpec((int) (getWidth() * .9),
3845                                        View.MeasureSpec.AT_MOST),
3846                                View.MeasureSpec.makeMeasureSpec((int) (getHeight() * 1.5),
3847                                        View.MeasureSpec.EXACTLY));
3848                        top = gravity == Gravity.TOP ? getHeight() - view.getMeasuredHeight() -
3849                                80 : 90;
3850                        left = 0;
3851                        vDesiredDist.set(top);
3852                        break;
3853                }
3854
3855                view.layout(left, top, left + view.getMeasuredWidth(),
3856                        top + view.getMeasuredHeight());
3857                layoutLatch.countDown();
3858            }
3859
3860            @Override
3861            public boolean canScrollVertically() {
3862                return true;
3863            }
3864
3865            @Override
3866            public boolean canScrollHorizontally() {
3867                return super.canScrollHorizontally();
3868            }
3869
3870            @Override
3871            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
3872                    RecyclerView.State state) {
3873                vScrollDist.addAndGet(dy);
3874                getChildAt(0).offsetTopAndBottom(-dy);
3875                return dy;
3876            }
3877
3878            @Override
3879            public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
3880                    RecyclerView.State state) {
3881                hScrollDist.addAndGet(dx);
3882                getChildAt(0).offsetLeftAndRight(-dx);
3883                return dx;
3884            }
3885        };
3886        TestAdapter adapter = new TestAdapter(10);
3887        rv.setAdapter(adapter);
3888        rv.setLayoutManager(tlm);
3889        tlm.expectLayouts(1);
3890        setRecyclerView(rv);
3891        tlm.waitForLayout(2);
3892        View view = rv.getChildAt(0);
3893        assertTrue("test sanity", requestFocus(view, true));
3894        assertTrue("test sanity", view.hasFocus());
3895        assertEquals(vDesiredDist.get(), vScrollDist.get());
3896        assertEquals(hDesiredDist.get(), hScrollDist.get());
3897        assertEquals(mRecyclerView.getPaddingTop(), view.getTop());
3898        if (rtl) {
3899            assertEquals(mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(),
3900                    view.getRight());
3901        } else {
3902            assertEquals(mRecyclerView.getPaddingLeft(), view.getLeft());
3903        }
3904    }
3905
3906    @Test
3907    public void firstLayoutWithAdapterChanges() throws Throwable {
3908        final TestAdapter adapter = new TestAdapter(0);
3909        final RecyclerView rv = new RecyclerView(getActivity());
3910        setVisibility(rv, View.GONE);
3911        TestLayoutManager tlm = new TestLayoutManager() {
3912            @Override
3913            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3914                try {
3915                    super.onLayoutChildren(recycler, state);
3916                    layoutRange(recycler, 0, state.getItemCount());
3917                } catch (Throwable t) {
3918                    postExceptionToInstrumentation(t);
3919                } finally {
3920                    layoutLatch.countDown();
3921                }
3922            }
3923
3924            @Override
3925            public boolean supportsPredictiveItemAnimations() {
3926                return true;
3927            }
3928        };
3929        rv.setLayoutManager(tlm);
3930        rv.setAdapter(adapter);
3931        rv.setHasFixedSize(true);
3932        setRecyclerView(rv);
3933        tlm.expectLayouts(1);
3934        tlm.assertNoLayout("test sanity, layout should not run", 1);
3935        getInstrumentation().waitForIdleSync();
3936        runTestOnUiThread(new Runnable() {
3937            @Override
3938            public void run() {
3939                try {
3940                    adapter.addAndNotify(2);
3941                } catch (Throwable throwable) {
3942                    throwable.printStackTrace();
3943                }
3944                rv.setVisibility(View.VISIBLE);
3945            }
3946        });
3947        checkForMainThreadException();
3948        tlm.waitForLayout(2);
3949        assertEquals(2, rv.getChildCount());
3950        checkForMainThreadException();
3951    }
3952
3953    @Test
3954    public void computeScrollOffsetWithoutLayoutManager() throws Throwable {
3955        RecyclerView rv = new RecyclerView(getActivity());
3956        rv.setAdapter(new TestAdapter(10));
3957        setRecyclerView(rv);
3958        assertEquals(0, rv.computeHorizontalScrollExtent());
3959        assertEquals(0, rv.computeHorizontalScrollOffset());
3960        assertEquals(0, rv.computeHorizontalScrollRange());
3961
3962        assertEquals(0, rv.computeVerticalScrollExtent());
3963        assertEquals(0, rv.computeVerticalScrollOffset());
3964        assertEquals(0, rv.computeVerticalScrollRange());
3965    }
3966
3967    @Test
3968    public void computeScrollOffsetWithoutAdapter() throws Throwable {
3969        RecyclerView rv = new RecyclerView(getActivity());
3970        rv.setLayoutManager(new TestLayoutManager());
3971        setRecyclerView(rv);
3972        assertEquals(0, rv.computeHorizontalScrollExtent());
3973        assertEquals(0, rv.computeHorizontalScrollOffset());
3974        assertEquals(0, rv.computeHorizontalScrollRange());
3975
3976        assertEquals(0, rv.computeVerticalScrollExtent());
3977        assertEquals(0, rv.computeVerticalScrollOffset());
3978        assertEquals(0, rv.computeVerticalScrollRange());
3979    }
3980
3981    @Test
3982    public void focusRectOnScreenWithDecorOffsets() throws Throwable {
3983        focusRectOnScreenTest(true);
3984    }
3985
3986    @Test
3987    public void focusRectOnScreenWithout() throws Throwable {
3988        focusRectOnScreenTest(false);
3989    }
3990
3991    public void focusRectOnScreenTest(boolean addItemDecors) throws Throwable {
3992        RecyclerView rv = new RecyclerView(getActivity());
3993        final AtomicInteger scrollDist = new AtomicInteger(0);
3994        TestLayoutManager tlm = new TestLayoutManager() {
3995            @Override
3996            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3997                detachAndScrapAttachedViews(recycler);
3998                final View view = recycler.getViewForPosition(0);
3999                addView(view);
4000                measureChildWithMargins(view, 0, 0);
4001                view.layout(0, -20, view.getWidth(),
4002                        -20 + view.getHeight());// ignore decors on purpose
4003                layoutLatch.countDown();
4004            }
4005
4006            @Override
4007            public boolean canScrollVertically() {
4008                return true;
4009            }
4010
4011            @Override
4012            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
4013                    RecyclerView.State state) {
4014                scrollDist.addAndGet(dy);
4015                return dy;
4016            }
4017        };
4018        TestAdapter adapter = new TestAdapter(10);
4019        if (addItemDecors) {
4020            rv.addItemDecoration(new RecyclerView.ItemDecoration() {
4021                @Override
4022                public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
4023                        RecyclerView.State state) {
4024                    outRect.set(0, 10, 0, 10);
4025                }
4026            });
4027        }
4028        rv.setAdapter(adapter);
4029        rv.setLayoutManager(tlm);
4030        tlm.expectLayouts(1);
4031        setRecyclerView(rv);
4032        tlm.waitForLayout(2);
4033
4034        View view = rv.getChildAt(0);
4035        requestFocus(view, true);
4036        assertEquals(addItemDecors ? -30 : -20, scrollDist.get());
4037    }
4038
4039    @Test
4040    public void unimplementedSmoothScroll() throws Throwable {
4041        final AtomicInteger receivedScrollToPosition = new AtomicInteger(-1);
4042        final AtomicInteger receivedSmoothScrollToPosition = new AtomicInteger(-1);
4043        final CountDownLatch cbLatch = new CountDownLatch(2);
4044        TestLayoutManager tlm = new TestLayoutManager() {
4045            @Override
4046            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
4047                detachAndScrapAttachedViews(recycler);
4048                layoutRange(recycler, 0, 10);
4049                layoutLatch.countDown();
4050            }
4051
4052            @Override
4053            public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
4054                    int position) {
4055                assertEquals(-1, receivedSmoothScrollToPosition.get());
4056                receivedSmoothScrollToPosition.set(position);
4057                RecyclerView.SmoothScroller ss =
4058                        new LinearSmoothScroller(recyclerView.getContext()) {
4059                            @Override
4060                            public PointF computeScrollVectorForPosition(int targetPosition) {
4061                                return null;
4062                            }
4063                        };
4064                ss.setTargetPosition(position);
4065                startSmoothScroll(ss);
4066                cbLatch.countDown();
4067            }
4068
4069            @Override
4070            public void scrollToPosition(int position) {
4071                assertEquals(-1, receivedScrollToPosition.get());
4072                receivedScrollToPosition.set(position);
4073                cbLatch.countDown();
4074            }
4075        };
4076        RecyclerView rv = new RecyclerView(getActivity());
4077        rv.setAdapter(new TestAdapter(100));
4078        rv.setLayoutManager(tlm);
4079        tlm.expectLayouts(1);
4080        setRecyclerView(rv);
4081        tlm.waitForLayout(2);
4082        freezeLayout(true);
4083        smoothScrollToPosition(35, false);
4084        assertEquals("smoothScrollToPosition should be ignored when frozen",
4085                -1, receivedSmoothScrollToPosition.get());
4086        freezeLayout(false);
4087        smoothScrollToPosition(35, false);
4088        assertTrue("both scrolls should be called", cbLatch.await(3, TimeUnit.SECONDS));
4089        checkForMainThreadException();
4090        assertEquals(35, receivedSmoothScrollToPosition.get());
4091        assertEquals(35, receivedScrollToPosition.get());
4092    }
4093
4094    @Test
4095    public void jumpingJackSmoothScroller() throws Throwable {
4096        jumpingJackSmoothScrollerTest(true);
4097    }
4098
4099    @Test
4100    public void jumpingJackSmoothScrollerGoesIdle() throws Throwable {
4101        jumpingJackSmoothScrollerTest(false);
4102    }
4103
4104    @Test
4105    public void testScrollByBeforeFirstLayout() throws Throwable {
4106        final RecyclerView recyclerView = new RecyclerView(getActivity());
4107        TestAdapter adapter = new TestAdapter(10);
4108        recyclerView.setLayoutManager(new TestLayoutManager() {
4109            AtomicBoolean didLayout = new AtomicBoolean(false);
4110            @Override
4111            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
4112                super.onLayoutChildren(recycler, state);
4113                didLayout.set(true);
4114            }
4115
4116            @Override
4117            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
4118                    RecyclerView.State state) {
4119                assertThat("should run layout before scroll",
4120                        didLayout.get(), CoreMatchers.is(true));
4121                return super.scrollVerticallyBy(dy, recycler, state);
4122            }
4123
4124            @Override
4125            public boolean canScrollVertically() {
4126                return true;
4127            }
4128        });
4129        recyclerView.setAdapter(adapter);
4130
4131        runTestOnUiThread(new Runnable() {
4132            @Override
4133            public void run() {
4134                try {
4135                    setRecyclerView(recyclerView);
4136                    recyclerView.scrollBy(10, 19);
4137                } catch (Throwable throwable) {
4138                    postExceptionToInstrumentation(throwable);
4139                }
4140            }
4141        });
4142
4143        checkForMainThreadException();
4144    }
4145
4146    private void jumpingJackSmoothScrollerTest(final boolean succeed) throws Throwable {
4147        final List<Integer> receivedScrollToPositions = new ArrayList<>();
4148        final TestAdapter testAdapter = new TestAdapter(200);
4149        final AtomicBoolean mTargetFound = new AtomicBoolean(false);
4150        TestLayoutManager tlm = new TestLayoutManager() {
4151            int pendingScrollPosition = -1;
4152            @Override
4153            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
4154                detachAndScrapAttachedViews(recycler);
4155                final int pos = pendingScrollPosition < 0 ? 0: pendingScrollPosition;
4156                layoutRange(recycler, pos, pos + 10);
4157                if (layoutLatch != null) {
4158                    layoutLatch.countDown();
4159                }
4160            }
4161
4162            @Override
4163            public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
4164                    final int position) {
4165                RecyclerView.SmoothScroller ss =
4166                        new LinearSmoothScroller(recyclerView.getContext()) {
4167                            @Override
4168                            public PointF computeScrollVectorForPosition(int targetPosition) {
4169                                return new PointF(0, 1);
4170                            }
4171
4172                            @Override
4173                            protected void onTargetFound(View targetView, RecyclerView.State state,
4174                                                         Action action) {
4175                                super.onTargetFound(targetView, state, action);
4176                                mTargetFound.set(true);
4177                            }
4178
4179                            @Override
4180                            protected void updateActionForInterimTarget(Action action) {
4181                                int limit = succeed ? getTargetPosition() : 100;
4182                                if (pendingScrollPosition + 2 < limit) {
4183                                    if (pendingScrollPosition != NO_POSITION) {
4184                                        assertEquals(pendingScrollPosition,
4185                                                getChildViewHolderInt(getChildAt(0))
4186                                                        .getAdapterPosition());
4187                                    }
4188                                    action.jumpTo(pendingScrollPosition + 2);
4189                                }
4190                            }
4191                        };
4192                ss.setTargetPosition(position);
4193                startSmoothScroll(ss);
4194            }
4195
4196            @Override
4197            public void scrollToPosition(int position) {
4198                receivedScrollToPositions.add(position);
4199                pendingScrollPosition = position;
4200                requestLayout();
4201            }
4202        };
4203        final RecyclerView rv = new RecyclerView(getActivity());
4204        rv.setAdapter(testAdapter);
4205        rv.setLayoutManager(tlm);
4206
4207        tlm.expectLayouts(1);
4208        setRecyclerView(rv);
4209        tlm.waitForLayout(2);
4210
4211        runTestOnUiThread(new Runnable() {
4212            @Override
4213            public void run() {
4214                rv.smoothScrollToPosition(150);
4215            }
4216        });
4217        int limit = 100;
4218        while (rv.getLayoutManager().isSmoothScrolling() && --limit > 0) {
4219            Thread.sleep(200);
4220            checkForMainThreadException();
4221        }
4222        checkForMainThreadException();
4223        assertTrue(limit > 0);
4224        for (int i = 1; i < 100; i+=2) {
4225            assertTrue("scroll positions must include " + i, receivedScrollToPositions.contains(i));
4226        }
4227
4228        assertEquals(succeed, mTargetFound.get());
4229
4230    }
4231
4232    private static class TestViewHolder2 extends RecyclerView.ViewHolder {
4233
4234        Object mData;
4235
4236        public TestViewHolder2(View itemView) {
4237            super(itemView);
4238        }
4239    }
4240
4241    private static class TestAdapter2 extends RecyclerView.Adapter<TestViewHolder2> {
4242
4243        List<Item> mItems;
4244
4245        private TestAdapter2(int count) {
4246            mItems = new ArrayList<>(count);
4247            for (int i = 0; i < count; i++) {
4248                mItems.add(new Item(i, "Item " + i));
4249            }
4250        }
4251
4252        @Override
4253        public TestViewHolder2 onCreateViewHolder(ViewGroup parent,
4254                int viewType) {
4255            return new TestViewHolder2(new TextView(parent.getContext()));
4256        }
4257
4258        @Override
4259        public void onBindViewHolder(TestViewHolder2 holder, int position) {
4260            final Item item = mItems.get(position);
4261            ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")");
4262        }
4263
4264        @Override
4265        public int getItemCount() {
4266            return mItems.size();
4267        }
4268    }
4269
4270    public interface AdapterRunnable {
4271
4272        void run(TestAdapter adapter) throws Throwable;
4273    }
4274
4275    public class LayoutAllLayoutManager extends TestLayoutManager {
4276        private final boolean mAllowNullLayoutLatch;
4277
4278        public LayoutAllLayoutManager() {
4279            // by default, we don't allow unexpected layouts.
4280            this(false);
4281        }
4282        public LayoutAllLayoutManager(boolean allowNullLayoutLatch) {
4283            mAllowNullLayoutLatch = allowNullLayoutLatch;
4284        }
4285
4286
4287        @Override
4288        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
4289            detachAndScrapAttachedViews(recycler);
4290            layoutRange(recycler, 0, state.getItemCount());
4291            if (!mAllowNullLayoutLatch || layoutLatch != null) {
4292                layoutLatch.countDown();
4293            }
4294        }
4295    }
4296
4297    /**
4298     * Proxy class to make protected methods public
4299     */
4300    public static class TestRecyclerView extends RecyclerView {
4301
4302        public TestRecyclerView(Context context) {
4303            super(context);
4304        }
4305
4306        public TestRecyclerView(Context context, @Nullable AttributeSet attrs) {
4307            super(context, attrs);
4308        }
4309
4310        public TestRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
4311            super(context, attrs, defStyle);
4312        }
4313
4314        @Override
4315        public void detachViewFromParent(int index) {
4316            super.detachViewFromParent(index);
4317        }
4318
4319        @Override
4320        public void attachViewToParent(View child, int index, ViewGroup.LayoutParams params) {
4321            super.attachViewToParent(child, index, params);
4322        }
4323    }
4324
4325    private interface ViewRunnable {
4326        void run(View view) throws RuntimeException;
4327    }
4328}
4329