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