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 
18 package android.support.v7.widget;
19 
20 import org.junit.After;
21 import org.junit.Before;
22 import org.junit.Test;
23 import org.junit.runner.RunWith;
24 
25 import android.support.test.InstrumentationRegistry;
26 import android.graphics.Color;
27 import android.graphics.PointF;
28 import android.graphics.Rect;
29 import android.os.SystemClock;
30 import android.support.v4.view.ViewCompat;
31 import android.support.v7.widget.RecyclerView;
32 import android.test.TouchUtils;
33 import android.util.Log;
34 import android.view.Gravity;
35 import android.view.KeyEvent;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.view.ViewConfiguration;
39 import android.view.ViewGroup;
40 import android.view.ViewTreeObserver;
41 import android.widget.TextView;
42 
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.concurrent.CountDownLatch;
48 import java.util.concurrent.TimeUnit;
49 import java.util.concurrent.atomic.AtomicBoolean;
50 import java.util.concurrent.atomic.AtomicInteger;
51 
52 import static android.support.v7.widget.RecyclerView.NO_POSITION;
53 import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
54 import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING;
55 import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING;
56 import static android.support.v7.widget.RecyclerView.getChildViewHolderInt;
57 import android.support.test.runner.AndroidJUnit4;
58 
59 @RunWith(AndroidJUnit4.class)
60 public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest {
61     private static final int FLAG_HORIZONTAL = 1;
62     private static final int FLAG_VERTICAL = 1 << 1;
63     private static final int FLAG_FLING = 1 << 2;
64 
65     private static final boolean DEBUG = true;
66 
67     private static final String TAG = "RecyclerViewLayoutTest";
68 
RecyclerViewLayoutTest()69     public RecyclerViewLayoutTest() {
70         super(DEBUG);
71     }
72 
73     @Before
74     @Override
setUp()75     public void setUp() throws Exception {
76         super.setUp();
77         injectInstrumentation(InstrumentationRegistry.getInstrumentation());
78     }
79 
80     @After
81     @Override
tearDown()82     public void tearDown() throws Exception {
83         super.tearDown();
84     }
85 
86     @Test
testFlingFrozen()87     public void testFlingFrozen() throws Throwable {
88         testScrollFrozen(true);
89     }
90 
91     @Test
testDragFrozen()92     public void testDragFrozen() throws Throwable {
93         testScrollFrozen(false);
94     }
95 
testScrollFrozen(boolean fling)96     private void testScrollFrozen(boolean fling) throws Throwable {
97         RecyclerView recyclerView = new RecyclerView(getActivity());
98 
99         final int horizontalScrollCount = 3;
100         final int verticalScrollCount = 3;
101         final int horizontalVelocity = 1000;
102         final int verticalVelocity = 1000;
103         final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount);
104         final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount);
105         TestLayoutManager tlm = new TestLayoutManager() {
106             @Override
107             public boolean canScrollHorizontally() {
108                 return true;
109             }
110 
111             @Override
112             public boolean canScrollVertically() {
113                 return true;
114             }
115 
116             @Override
117             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
118                 layoutRange(recycler, 0, 10);
119                 layoutLatch.countDown();
120             }
121 
122             @Override
123             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
124                     RecyclerView.State state) {
125                 if (verticalCounter.get() > 0) {
126                     verticalCounter.decrementAndGet();
127                     return dy;
128                 }
129                 return 0;
130             }
131 
132             @Override
133             public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
134                     RecyclerView.State state) {
135                 if (horizontalCounter.get() > 0) {
136                     horizontalCounter.decrementAndGet();
137                     return dx;
138                 }
139                 return 0;
140             }
141         };
142         TestAdapter adapter = new TestAdapter(100);
143         recyclerView.setAdapter(adapter);
144         recyclerView.setLayoutManager(tlm);
145         tlm.expectLayouts(1);
146         setRecyclerView(recyclerView);
147         tlm.waitForLayout(2);
148 
149         freezeLayout(true);
150 
151         if (fling) {
152             assertFalse("fling should be blocked", fling(horizontalVelocity, verticalVelocity));
153         } else { // drag
154             TouchUtils.dragViewTo(this, recyclerView,
155                     Gravity.LEFT | Gravity.TOP,
156                     mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2);
157         }
158         assertEquals("rv's horizontal scroll cb must not run", horizontalScrollCount,
159                 horizontalCounter.get());
160         assertEquals("rv's vertical scroll cb must not run", verticalScrollCount,
161                 verticalCounter.get());
162 
163         freezeLayout(false);
164 
165         if (fling) {
166             assertTrue("fling should be started", fling(horizontalVelocity, verticalVelocity));
167         } else { // drag
168             TouchUtils.dragViewTo(this, recyclerView,
169                     Gravity.LEFT | Gravity.TOP,
170                     mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2);
171         }
172         assertEquals("rv's horizontal scroll cb must finishes", 0, horizontalCounter.get());
173         assertEquals("rv's vertical scroll cb must finishes", 0, verticalCounter.get());
174     }
175 
176     @Test
testFocusSearchFailFrozen()177     public void testFocusSearchFailFrozen() throws Throwable {
178         RecyclerView recyclerView = new RecyclerView(getActivity());
179 
180         final AtomicInteger focusSearchCalled = new AtomicInteger(0);
181         TestLayoutManager tlm = new TestLayoutManager() {
182             @Override
183             public boolean canScrollHorizontally() {
184                 return true;
185             }
186 
187             @Override
188             public boolean canScrollVertically() {
189                 return true;
190             }
191 
192             @Override
193             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
194                 layoutRange(recycler, 0, 10);
195                 layoutLatch.countDown();
196             }
197 
198             @Override
199             public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
200                     RecyclerView.State state) {
201                 focusSearchCalled.addAndGet(1);
202                 return null;
203             }
204         };
205         TestAdapter adapter = new TestAdapter(100);
206         recyclerView.setAdapter(adapter);
207         recyclerView.setLayoutManager(tlm);
208         tlm.expectLayouts(1);
209         setRecyclerView(recyclerView);
210         tlm.waitForLayout(2);
211 
212         final View c = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
213         runTestOnUiThread(new Runnable() {
214             @Override
215             public void run() {
216                 c.requestFocus();
217             }
218         });
219         assertTrue(c.hasFocus());
220 
221         freezeLayout(true);
222         sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
223         assertEquals("onFocusSearchFailed should not be called when layout is frozen",
224                 0, focusSearchCalled.get());
225 
226         freezeLayout(false);
227         sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
228         assertEquals(1, focusSearchCalled.get());
229     }
230 
231     @Test
testFrozenAndChangeAdapter()232     public void testFrozenAndChangeAdapter() throws Throwable {
233         RecyclerView recyclerView = new RecyclerView(getActivity());
234 
235         final AtomicInteger focusSearchCalled = new AtomicInteger(0);
236         TestLayoutManager tlm = new TestLayoutManager() {
237             @Override
238             public boolean canScrollHorizontally() {
239                 return true;
240             }
241 
242             @Override
243             public boolean canScrollVertically() {
244                 return true;
245             }
246 
247             @Override
248             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
249                 layoutRange(recycler, 0, 10);
250                 layoutLatch.countDown();
251             }
252 
253             @Override
254             public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
255                     RecyclerView.State state) {
256                 focusSearchCalled.addAndGet(1);
257                 return null;
258             }
259         };
260         TestAdapter adapter = new TestAdapter(100);
261         recyclerView.setAdapter(adapter);
262         recyclerView.setLayoutManager(tlm);
263         tlm.expectLayouts(1);
264         setRecyclerView(recyclerView);
265         tlm.waitForLayout(2);
266 
267         freezeLayout(true);
268         TestAdapter adapter2 = new TestAdapter(1000);
269         setAdapter(adapter2);
270         assertFalse(recyclerView.isLayoutFrozen());
271         assertSame(adapter2, recyclerView.getAdapter());
272 
273         freezeLayout(true);
274         TestAdapter adapter3 = new TestAdapter(1000);
275         swapAdapter(adapter3, true);
276         assertFalse(recyclerView.isLayoutFrozen());
277         assertSame(adapter3, recyclerView.getAdapter());
278     }
279 
280     @Test
testScrollToPositionCallback()281     public void testScrollToPositionCallback() throws Throwable {
282         RecyclerView recyclerView = new RecyclerView(getActivity());
283         TestLayoutManager tlm = new TestLayoutManager() {
284             int scrollPos = RecyclerView.NO_POSITION;
285 
286             @Override
287             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
288                 layoutLatch.countDown();
289                 if (scrollPos == RecyclerView.NO_POSITION) {
290                     layoutRange(recycler, 0, 10);
291                 } else {
292                     layoutRange(recycler, scrollPos, scrollPos + 10);
293                 }
294             }
295 
296             @Override
297             public void scrollToPosition(int position) {
298                 scrollPos = position;
299                 requestLayout();
300             }
301         };
302         recyclerView.setLayoutManager(tlm);
303         TestAdapter adapter = new TestAdapter(100);
304         recyclerView.setAdapter(adapter);
305         final AtomicInteger rvCounter = new AtomicInteger(0);
306         final AtomicInteger viewGroupCounter = new AtomicInteger(0);
307         recyclerView.getViewTreeObserver().addOnScrollChangedListener(
308                 new ViewTreeObserver.OnScrollChangedListener() {
309                     @Override
310                     public void onScrollChanged() {
311                         viewGroupCounter.incrementAndGet();
312                     }
313                 });
314         recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
315             @Override
316             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
317                 rvCounter.incrementAndGet();
318                 super.onScrolled(recyclerView, dx, dy);
319             }
320         });
321         tlm.expectLayouts(1);
322 
323         setRecyclerView(recyclerView);
324         tlm.waitForLayout(2);
325         // wait for draw :/
326         Thread.sleep(1000);
327 
328         assertEquals("RV on scroll should be called for initialization", 1, rvCounter.get());
329         assertEquals("VTO on scroll should be called for initialization", 1,
330                 viewGroupCounter.get());
331         tlm.expectLayouts(1);
332         freezeLayout(true);
333         scrollToPosition(3);
334         tlm.assertNoLayout("scrollToPosition should be ignored", 2);
335         freezeLayout(false);
336         scrollToPosition(3);
337         tlm.waitForLayout(2);
338         assertEquals("RV on scroll should be called", 2, rvCounter.get());
339         assertEquals("VTO on scroll should be called", 2, viewGroupCounter.get());
340         tlm.expectLayouts(1);
341         requestLayoutOnUIThread(recyclerView);
342         tlm.waitForLayout(2);
343         // wait for draw :/
344         Thread.sleep(1000);
345         assertEquals("on scroll should NOT be called", 2, rvCounter.get());
346         assertEquals("on scroll should NOT be called", 2, viewGroupCounter.get());
347 
348     }
349 
350     @Test
testScrollInBothDirectionEqual()351     public void testScrollInBothDirectionEqual() throws Throwable {
352         scrollInBothDirection(3, 3, 1000, 1000);
353     }
354 
355     @Test
testScrollInBothDirectionMoreVertical()356     public void testScrollInBothDirectionMoreVertical() throws Throwable {
357         scrollInBothDirection(2, 3, 1000, 1000);
358     }
359 
360     @Test
testScrollInBothDirectionMoreHorizontal()361     public void testScrollInBothDirectionMoreHorizontal() throws Throwable {
362         scrollInBothDirection(3, 2, 1000, 1000);
363     }
364 
365     @Test
testScrollHorizontalOnly()366     public void testScrollHorizontalOnly() throws Throwable {
367         scrollInBothDirection(3, 0, 1000, 0);
368     }
369 
370     @Test
testScrollVerticalOnly()371     public void testScrollVerticalOnly() throws Throwable {
372         scrollInBothDirection(0, 3, 0, 1000);
373     }
374 
375     @Test
testScrollInBothDirectionEqualReverse()376     public void testScrollInBothDirectionEqualReverse() throws Throwable {
377         scrollInBothDirection(3, 3, -1000, -1000);
378     }
379 
380     @Test
testScrollInBothDirectionMoreVerticalReverse()381     public void testScrollInBothDirectionMoreVerticalReverse() throws Throwable {
382         scrollInBothDirection(2, 3, -1000, -1000);
383     }
384 
385     @Test
testScrollInBothDirectionMoreHorizontalReverse()386     public void testScrollInBothDirectionMoreHorizontalReverse() throws Throwable {
387         scrollInBothDirection(3, 2, -1000, -1000);
388     }
389 
390     @Test
testScrollHorizontalOnlyReverse()391     public void testScrollHorizontalOnlyReverse() throws Throwable {
392         scrollInBothDirection(3, 0, -1000, 0);
393     }
394 
395     @Test
testScrollVerticalOnlyReverse()396     public void testScrollVerticalOnlyReverse() throws Throwable {
397         scrollInBothDirection(0, 3, 0, -1000);
398     }
399 
scrollInBothDirection(int horizontalScrollCount, int verticalScrollCount, int horizontalVelocity, int verticalVelocity)400     public void scrollInBothDirection(int horizontalScrollCount, int verticalScrollCount,
401             int horizontalVelocity, int verticalVelocity)
402             throws Throwable {
403         RecyclerView recyclerView = new RecyclerView(getActivity());
404         final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount);
405         final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount);
406         TestLayoutManager tlm = new TestLayoutManager() {
407             @Override
408             public boolean canScrollHorizontally() {
409                 return true;
410             }
411 
412             @Override
413             public boolean canScrollVertically() {
414                 return true;
415             }
416 
417             @Override
418             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
419                 layoutRange(recycler, 0, 10);
420                 layoutLatch.countDown();
421             }
422 
423             @Override
424             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
425                     RecyclerView.State state) {
426                 if (verticalCounter.get() > 0) {
427                     verticalCounter.decrementAndGet();
428                     return dy;
429                 }
430                 return 0;
431             }
432 
433             @Override
434             public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
435                     RecyclerView.State state) {
436                 if (horizontalCounter.get() > 0) {
437                     horizontalCounter.decrementAndGet();
438                     return dx;
439                 }
440                 return 0;
441             }
442         };
443         TestAdapter adapter = new TestAdapter(100);
444         recyclerView.setAdapter(adapter);
445         recyclerView.setLayoutManager(tlm);
446         tlm.expectLayouts(1);
447         setRecyclerView(recyclerView);
448         tlm.waitForLayout(2);
449         assertTrue("test sanity, fling must run", fling(horizontalVelocity, verticalVelocity));
450         assertEquals("rv's horizontal scroll cb must run " + horizontalScrollCount + " times'", 0,
451                 horizontalCounter.get());
452         assertEquals("rv's vertical scroll cb must run " + verticalScrollCount + " times'", 0,
453                 verticalCounter.get());
454     }
455 
456     @Test
testDragHorizontal()457     public void testDragHorizontal() throws Throwable {
458         scrollInOtherOrientationTest(FLAG_HORIZONTAL);
459     }
460 
461     @Test
testDragVertical()462     public void testDragVertical() throws Throwable {
463         scrollInOtherOrientationTest(FLAG_VERTICAL);
464     }
465 
466     @Test
testFlingHorizontal()467     public void testFlingHorizontal() throws Throwable {
468         scrollInOtherOrientationTest(FLAG_HORIZONTAL | FLAG_FLING);
469     }
470 
471     @Test
testFlingVertical()472     public void testFlingVertical() throws Throwable {
473         scrollInOtherOrientationTest(FLAG_VERTICAL | FLAG_FLING);
474     }
475 
476     @Test
testNestedDragVertical()477     public void testNestedDragVertical() throws Throwable {
478         TestedFrameLayout tfl = getActivity().mContainer;
479         tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
480         tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
481         scrollInOtherOrientationTest(FLAG_VERTICAL, 0);
482     }
483 
484     @Test
testNestedDragHorizontal()485     public void testNestedDragHorizontal() throws Throwable {
486         TestedFrameLayout tfl = getActivity().mContainer;
487         tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
488         tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
489         scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0);
490     }
491 
492     @Test
testNestedDragHorizontalCallsStopNestedScroll()493     public void testNestedDragHorizontalCallsStopNestedScroll() throws Throwable {
494         TestedFrameLayout tfl = getActivity().mContainer;
495         tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
496         tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
497         scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0);
498         assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled());
499     }
500 
501     @Test
testNestedDragVerticalCallsStopNestedScroll()502     public void testNestedDragVerticalCallsStopNestedScroll() throws Throwable {
503         TestedFrameLayout tfl = getActivity().mContainer;
504         tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
505         tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME);
506         scrollInOtherOrientationTest(FLAG_VERTICAL, 0);
507         assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled());
508     }
509 
scrollInOtherOrientationTest(int flags)510     private void scrollInOtherOrientationTest(int flags)
511             throws Throwable {
512         scrollInOtherOrientationTest(flags, flags);
513     }
514 
scrollInOtherOrientationTest(final int flags, int expectedFlags)515     private void scrollInOtherOrientationTest(final int flags, int expectedFlags) throws Throwable {
516         RecyclerView recyclerView = new RecyclerView(getActivity());
517         final AtomicBoolean scrolledHorizontal = new AtomicBoolean(false);
518         final AtomicBoolean scrolledVertical = new AtomicBoolean(false);
519 
520         final TestLayoutManager tlm = new TestLayoutManager() {
521             @Override
522             public boolean canScrollHorizontally() {
523                 return (flags & FLAG_HORIZONTAL) != 0;
524             }
525 
526             @Override
527             public boolean canScrollVertically() {
528                 return (flags & FLAG_VERTICAL) != 0;
529             }
530 
531             @Override
532             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
533                 layoutRange(recycler, 0, 10);
534                 layoutLatch.countDown();
535             }
536 
537             @Override
538             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
539                     RecyclerView.State state) {
540                 scrolledVertical.set(true);
541                 return super.scrollVerticallyBy(dy, recycler, state);
542             }
543 
544             @Override
545             public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
546                     RecyclerView.State state) {
547                 scrolledHorizontal.set(true);
548                 return super.scrollHorizontallyBy(dx, recycler, state);
549             }
550         };
551         TestAdapter adapter = new TestAdapter(100);
552         recyclerView.setAdapter(adapter);
553         recyclerView.setLayoutManager(tlm);
554         tlm.expectLayouts(1);
555         setRecyclerView(recyclerView);
556         tlm.waitForLayout(2);
557         if ( (flags & FLAG_FLING) != 0 ) {
558             int flingVelocity = (mRecyclerView.getMaxFlingVelocity() +
559                     mRecyclerView.getMinFlingVelocity()) / 2;
560             assertEquals("fling started", (expectedFlags & FLAG_FLING) != 0,
561                     fling(flingVelocity, flingVelocity));
562         } else { // drag
563             TouchUtils.dragViewTo(this, recyclerView, Gravity.LEFT | Gravity.TOP,
564                     mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2);
565         }
566         assertEquals("horizontally scrolled: " + tlm.mScrollHorizontallyAmount,
567                 (expectedFlags & FLAG_HORIZONTAL) != 0, scrolledHorizontal.get());
568         assertEquals("vertically scrolled: " + tlm.mScrollVerticallyAmount,
569                 (expectedFlags & FLAG_VERTICAL) != 0, scrolledVertical.get());
570     }
571 
fling(final int velocityX, final int velocityY)572     private boolean fling(final int velocityX, final int velocityY) throws Throwable {
573         final AtomicBoolean didStart = new AtomicBoolean(false);
574         runTestOnUiThread(new Runnable() {
575             @Override
576             public void run() {
577                 boolean result = mRecyclerView.fling(velocityX, velocityY);
578                 didStart.set(result);
579             }
580         });
581         if (!didStart.get()) {
582             return false;
583         }
584         // cannot set scroll listener in case it is subject to some test so instead doing a busy
585         // loop until state goes idle
586         while (mRecyclerView.getScrollState() != SCROLL_STATE_IDLE) {
587             getInstrumentation().waitForIdleSync();
588         }
589         return true;
590     }
591 
assertPendingUpdatesAndLayout(TestLayoutManager testLayoutManager, final Runnable runnable)592     private void assertPendingUpdatesAndLayout(TestLayoutManager testLayoutManager,
593             final Runnable runnable) throws Throwable {
594         testLayoutManager.expectLayouts(1);
595         runTestOnUiThread(new Runnable() {
596             @Override
597             public void run() {
598                 runnable.run();
599                 assertTrue(mRecyclerView.hasPendingAdapterUpdates());
600             }
601         });
602         testLayoutManager.waitForLayout(1);
603         assertFalse(mRecyclerView.hasPendingAdapterUpdates());
604     }
605 
setupBasic(RecyclerView recyclerView, TestLayoutManager tlm, TestAdapter adapter, boolean waitForFirstLayout)606     private void setupBasic(RecyclerView recyclerView, TestLayoutManager tlm,
607             TestAdapter adapter, boolean waitForFirstLayout) throws Throwable {
608         recyclerView.setLayoutManager(tlm);
609         recyclerView.setAdapter(adapter);
610         if (waitForFirstLayout) {
611             tlm.expectLayouts(1);
612             setRecyclerView(recyclerView);
613             tlm.waitForLayout(1);
614         } else {
615             setRecyclerView(recyclerView);
616         }
617     }
618 
619     @Test
testHasPendingUpdatesBeforeFirstLayout()620     public void testHasPendingUpdatesBeforeFirstLayout() throws Throwable {
621         RecyclerView recyclerView = new RecyclerView(getActivity());
622         TestLayoutManager layoutManager = new DumbLayoutManager();
623         TestAdapter testAdapter = new TestAdapter(10);
624         setupBasic(recyclerView, layoutManager, testAdapter, false);
625         assertTrue(mRecyclerView.hasPendingAdapterUpdates());
626     }
627 
628     @Test
testNoPendingUpdatesAfterLayout()629     public void testNoPendingUpdatesAfterLayout() throws Throwable {
630         RecyclerView recyclerView = new RecyclerView(getActivity());
631         TestLayoutManager layoutManager = new DumbLayoutManager();
632         TestAdapter testAdapter = new TestAdapter(10);
633         setupBasic(recyclerView, layoutManager, testAdapter, true);
634         assertFalse(mRecyclerView.hasPendingAdapterUpdates());
635     }
636 
637     @Test
testHasPendingUpdatesWhenAdapterIsChanged()638     public void testHasPendingUpdatesWhenAdapterIsChanged() throws Throwable {
639         RecyclerView recyclerView = new RecyclerView(getActivity());
640         TestLayoutManager layoutManager = new DumbLayoutManager();
641         final TestAdapter testAdapter = new TestAdapter(10);
642         setupBasic(recyclerView, layoutManager, testAdapter, false);
643         assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
644             @Override
645             public void run() {
646                 testAdapter.notifyItemRemoved(1);
647             }
648         });
649         assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
650             @Override
651             public void run() {
652                 testAdapter.notifyItemInserted(2);
653             }
654         });
655 
656         assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
657             @Override
658             public void run() {
659                 testAdapter.notifyItemMoved(2, 3);
660             }
661         });
662 
663         assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
664             @Override
665             public void run() {
666                 testAdapter.notifyItemChanged(2);
667             }
668         });
669 
670         assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
671             @Override
672             public void run() {
673                 testAdapter.notifyDataSetChanged();
674             }
675         });
676     }
677 
678     @Test
testTransientStateRecycleViaAdapter()679     public void testTransientStateRecycleViaAdapter() throws Throwable {
680         transientStateRecycleTest(true, false);
681     }
682 
683     @Test
testTransientStateRecycleViaTransientStateCleanup()684     public void testTransientStateRecycleViaTransientStateCleanup() throws Throwable {
685         transientStateRecycleTest(false, true);
686     }
687 
688     @Test
testTransientStateDontRecycle()689     public void testTransientStateDontRecycle() throws Throwable {
690         transientStateRecycleTest(false, false);
691     }
692 
transientStateRecycleTest(final boolean succeed, final boolean unsetTransientState)693     public void transientStateRecycleTest(final boolean succeed, final boolean unsetTransientState)
694             throws Throwable {
695         final List<View> failedToRecycle = new ArrayList<View>();
696         final List<View> recycled = new ArrayList<View>();
697         TestAdapter testAdapter = new TestAdapter(10) {
698             @Override
699             public boolean onFailedToRecycleView(
700                     TestViewHolder holder) {
701                 failedToRecycle.add(holder.itemView);
702                 if (unsetTransientState) {
703                     setHasTransientState(holder.itemView, false);
704                 }
705                 return succeed;
706             }
707 
708             @Override
709             public void onViewRecycled(TestViewHolder holder) {
710                 recycled.add(holder.itemView);
711                 super.onViewRecycled(holder);
712             }
713         };
714         TestLayoutManager tlm = new TestLayoutManager() {
715             @Override
716             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
717                 if (getChildCount() == 0) {
718                     detachAndScrapAttachedViews(recycler);
719                     layoutRange(recycler, 0, 5);
720                 } else {
721                     removeAndRecycleAllViews(recycler);
722                 }
723                 if (layoutLatch != null) {
724                     layoutLatch.countDown();
725                 }
726             }
727         };
728         RecyclerView recyclerView = new RecyclerView(getActivity());
729         recyclerView.setAdapter(testAdapter);
730         recyclerView.setLayoutManager(tlm);
731         recyclerView.setItemAnimator(null);
732         setRecyclerView(recyclerView);
733         getInstrumentation().waitForIdleSync();
734         // make sure we have enough views after this position so that we'll receive the on recycled
735         // callback
736         View view = recyclerView.getChildAt(3);//this has to be greater than def cache size.
737         setHasTransientState(view, true);
738         tlm.expectLayouts(1);
739         requestLayoutOnUIThread(recyclerView);
740         tlm.waitForLayout(2);
741 
742         assertTrue(failedToRecycle.contains(view));
743         assertEquals(succeed || unsetTransientState, recycled.contains(view));
744     }
745 
746     @Test
testAdapterPositionInvalidation()747     public void testAdapterPositionInvalidation() throws Throwable {
748         final RecyclerView recyclerView = new RecyclerView(getActivity());
749         final TestAdapter adapter = new TestAdapter(10);
750         final TestLayoutManager tlm = new TestLayoutManager() {
751             @Override
752             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
753                 layoutRange(recycler, 0, state.getItemCount());
754                 layoutLatch.countDown();
755             }
756         };
757         recyclerView.setAdapter(adapter);
758         recyclerView.setLayoutManager(tlm);
759         tlm.expectLayouts(1);
760         setRecyclerView(recyclerView);
761         tlm.waitForLayout(1);
762         runTestOnUiThread(new Runnable() {
763             @Override
764             public void run() {
765                 for (int i = 0; i < tlm.getChildCount(); i++) {
766                     assertNotSame("adapter positions should not be undefined",
767                             recyclerView.getChildAdapterPosition(tlm.getChildAt(i)),
768                             RecyclerView.NO_POSITION);
769                 }
770                 adapter.notifyDataSetChanged();
771                 for (int i = 0; i < tlm.getChildCount(); i++) {
772                     assertSame("adapter positions should be undefined",
773                             recyclerView.getChildAdapterPosition(tlm.getChildAt(i)),
774                             RecyclerView.NO_POSITION);
775                 }
776             }
777         });
778     }
779 
780     @Test
testAdapterPositionsBasic()781     public void testAdapterPositionsBasic() throws Throwable {
782         adapterPositionsTest(null);
783     }
784 
785     @Test
testAdapterPositionsRemoveItems()786     public void testAdapterPositionsRemoveItems() throws Throwable {
787         adapterPositionsTest(new AdapterRunnable() {
788             @Override
789             public void run(TestAdapter adapter) throws Throwable {
790                 adapter.deleteAndNotify(3, 4);
791             }
792         });
793     }
794 
795     @Test
testAdapterPositionsRemoveItemsBefore()796     public void testAdapterPositionsRemoveItemsBefore() throws Throwable {
797         adapterPositionsTest(new AdapterRunnable() {
798             @Override
799             public void run(TestAdapter adapter) throws Throwable {
800                 adapter.deleteAndNotify(0, 1);
801             }
802         });
803     }
804 
805     @Test
testAdapterPositionsAddItemsBefore()806     public void testAdapterPositionsAddItemsBefore() throws Throwable {
807         adapterPositionsTest(new AdapterRunnable() {
808             @Override
809             public void run(TestAdapter adapter) throws Throwable {
810                 adapter.addAndNotify(0, 5);
811             }
812         });
813     }
814 
815     @Test
testAdapterPositionsAddItemsInside()816     public void testAdapterPositionsAddItemsInside() throws Throwable {
817         adapterPositionsTest(new AdapterRunnable() {
818             @Override
819             public void run(TestAdapter adapter) throws Throwable {
820                 adapter.addAndNotify(3, 2);
821             }
822         });
823     }
824 
825     @Test
testAdapterPositionsMoveItems()826     public void testAdapterPositionsMoveItems() throws Throwable {
827         adapterPositionsTest(new AdapterRunnable() {
828             @Override
829             public void run(TestAdapter adapter) throws Throwable {
830                 adapter.moveAndNotify(3, 5);
831             }
832         });
833     }
834 
835     @Test
testAdapterPositionsNotifyDataSetChanged()836     public void testAdapterPositionsNotifyDataSetChanged() throws Throwable {
837         adapterPositionsTest(new AdapterRunnable() {
838             @Override
839             public void run(TestAdapter adapter) throws Throwable {
840                 adapter.mItems.clear();
841                 for (int i = 0; i < 20; i++) {
842                     adapter.mItems.add(new Item(i, "added item"));
843                 }
844                 adapter.notifyDataSetChanged();
845             }
846         });
847     }
848 
849     @Test
testAvoidLeakingRecyclerViewIfViewIsNotRecycled()850     public void testAvoidLeakingRecyclerViewIfViewIsNotRecycled() throws Throwable {
851         final AtomicBoolean failedToRecycle = new AtomicBoolean(false);
852         RecyclerView rv = new RecyclerView(getActivity());
853         TestLayoutManager tlm = new TestLayoutManager() {
854             @Override
855             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
856                 detachAndScrapAttachedViews(recycler);
857                 layoutRange(recycler, 0, state.getItemCount());
858                 layoutLatch.countDown();
859             }
860         };
861         TestAdapter adapter = new TestAdapter(10) {
862             @Override
863             public boolean onFailedToRecycleView(
864                     TestViewHolder holder) {
865                 failedToRecycle.set(true);
866                 return false;
867             }
868         };
869         rv.setAdapter(adapter);
870         rv.setLayoutManager(tlm);
871         tlm.expectLayouts(1);
872         setRecyclerView(rv);
873         tlm.waitForLayout(1);
874         final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0));
875         runTestOnUiThread(new Runnable() {
876             @Override
877             public void run() {
878                 ViewCompat.setHasTransientState(vh.itemView, true);
879             }
880         });
881         tlm.expectLayouts(1);
882         adapter.deleteAndNotify(0, 10);
883         tlm.waitForLayout(2);
884         final CountDownLatch animationsLatch = new CountDownLatch(1);
885         rv.getItemAnimator().isRunning(
886                 new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
887                     @Override
888                     public void onAnimationsFinished() {
889                         animationsLatch.countDown();
890                     }
891                 });
892         assertTrue(animationsLatch.await(2, TimeUnit.SECONDS));
893         assertTrue(failedToRecycle.get());
894         assertNull(vh.mOwnerRecyclerView);
895         checkForMainThreadException();
896     }
897 
898     @Test
testAvoidLeakingRecyclerViewViaViewHolder()899     public void testAvoidLeakingRecyclerViewViaViewHolder() throws Throwable {
900         RecyclerView rv = new RecyclerView(getActivity());
901         TestLayoutManager tlm = new TestLayoutManager() {
902             @Override
903             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
904                 detachAndScrapAttachedViews(recycler);
905                 layoutRange(recycler, 0, state.getItemCount());
906                 layoutLatch.countDown();
907             }
908         };
909         TestAdapter adapter = new TestAdapter(10);
910         rv.setAdapter(adapter);
911         rv.setLayoutManager(tlm);
912         tlm.expectLayouts(1);
913         setRecyclerView(rv);
914         tlm.waitForLayout(1);
915         final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0));
916         tlm.expectLayouts(1);
917         adapter.deleteAndNotify(0, 10);
918         tlm.waitForLayout(2);
919         final CountDownLatch animationsLatch = new CountDownLatch(1);
920         rv.getItemAnimator().isRunning(
921                 new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
922                     @Override
923                     public void onAnimationsFinished() {
924                         animationsLatch.countDown();
925                     }
926                 });
927         assertTrue(animationsLatch.await(2, TimeUnit.SECONDS));
928         assertNull(vh.mOwnerRecyclerView);
929         checkForMainThreadException();
930     }
931 
adapterPositionsTest(final AdapterRunnable adapterChanges)932     public void adapterPositionsTest(final AdapterRunnable adapterChanges) throws Throwable {
933         final TestAdapter testAdapter = new TestAdapter(10);
934         TestLayoutManager tlm = new TestLayoutManager() {
935             @Override
936             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
937                 try {
938                     layoutRange(recycler, Math.min(state.getItemCount(), 2)
939                             , Math.min(state.getItemCount(), 7));
940                     layoutLatch.countDown();
941                 } catch (Throwable t) {
942                     postExceptionToInstrumentation(t);
943                 }
944             }
945         };
946         final RecyclerView recyclerView = new RecyclerView(getActivity());
947         recyclerView.setLayoutManager(tlm);
948         recyclerView.setAdapter(testAdapter);
949         tlm.expectLayouts(1);
950         setRecyclerView(recyclerView);
951         tlm.waitForLayout(1);
952         runTestOnUiThread(new Runnable() {
953             @Override
954             public void run() {
955                 try {
956                     final int count = recyclerView.getChildCount();
957                     Map<View, Integer> layoutPositions = new HashMap<View, Integer>();
958                     assertTrue("test sanity", count > 0);
959                     for (int i = 0; i < count; i++) {
960                         View view = recyclerView.getChildAt(i);
961                         TestViewHolder vh = (TestViewHolder) recyclerView.getChildViewHolder(view);
962                         int index = testAdapter.mItems.indexOf(vh.mBoundItem);
963                         assertEquals("should be able to find VH with adapter position " + index, vh,
964                                 recyclerView.findViewHolderForAdapterPosition(index));
965                         assertEquals("get adapter position should return correct index", index,
966                                 vh.getAdapterPosition());
967                         layoutPositions.put(view, vh.mPosition);
968                     }
969                     if (adapterChanges != null) {
970                         adapterChanges.run(testAdapter);
971                         for (int i = 0; i < count; i++) {
972                             View view = recyclerView.getChildAt(i);
973                             TestViewHolder vh = (TestViewHolder) recyclerView
974                                     .getChildViewHolder(view);
975                             int index = testAdapter.mItems.indexOf(vh.mBoundItem);
976                             if (index >= 0) {
977                                 assertEquals("should be able to find VH with adapter position "
978                                                 + index, vh,
979                                         recyclerView.findViewHolderForAdapterPosition(index));
980                             }
981                             assertSame("get adapter position should return correct index", index,
982                                     vh.getAdapterPosition());
983                             assertSame("should be able to find view with layout position",
984                                     vh, mRecyclerView.findViewHolderForLayoutPosition(
985                                             layoutPositions.get(view)));
986                         }
987 
988                     }
989 
990                 } catch (Throwable t) {
991                     postExceptionToInstrumentation(t);
992                 }
993             }
994         });
995         checkForMainThreadException();
996     }
997 
998     @Test
testScrollStateForSmoothScroll()999     public void testScrollStateForSmoothScroll() throws Throwable {
1000         TestAdapter testAdapter = new TestAdapter(10);
1001         TestLayoutManager tlm = new TestLayoutManager();
1002         RecyclerView recyclerView = new RecyclerView(getActivity());
1003         recyclerView.setAdapter(testAdapter);
1004         recyclerView.setLayoutManager(tlm);
1005         setRecyclerView(recyclerView);
1006         getInstrumentation().waitForIdleSync();
1007         assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1008         final int[] stateCnts = new int[10];
1009         final CountDownLatch latch = new CountDownLatch(2);
1010         recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
1011             @Override
1012             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
1013                 stateCnts[newState] = stateCnts[newState] + 1;
1014                 latch.countDown();
1015             }
1016         });
1017         runTestOnUiThread(new Runnable() {
1018             @Override
1019             public void run() {
1020                 mRecyclerView.smoothScrollBy(0, 500);
1021             }
1022         });
1023         latch.await(5, TimeUnit.SECONDS);
1024         assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]);
1025         assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
1026         assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]);
1027     }
1028 
1029     @Test
testScrollStateForSmoothScrollWithStop()1030     public void testScrollStateForSmoothScrollWithStop() throws Throwable {
1031         TestAdapter testAdapter = new TestAdapter(10);
1032         TestLayoutManager tlm = new TestLayoutManager();
1033         RecyclerView recyclerView = new RecyclerView(getActivity());
1034         recyclerView.setAdapter(testAdapter);
1035         recyclerView.setLayoutManager(tlm);
1036         setRecyclerView(recyclerView);
1037         getInstrumentation().waitForIdleSync();
1038         assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1039         final int[] stateCnts = new int[10];
1040         final CountDownLatch latch = new CountDownLatch(1);
1041         recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
1042             @Override
1043             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
1044                 stateCnts[newState] = stateCnts[newState] + 1;
1045                 latch.countDown();
1046             }
1047         });
1048         runTestOnUiThread(new Runnable() {
1049             @Override
1050             public void run() {
1051                 mRecyclerView.smoothScrollBy(0, 500);
1052             }
1053         });
1054         latch.await(5, TimeUnit.SECONDS);
1055         runTestOnUiThread(new Runnable() {
1056             @Override
1057             public void run() {
1058                 mRecyclerView.stopScroll();
1059             }
1060         });
1061         assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1062         assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]);
1063         assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
1064         assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]);
1065     }
1066 
1067     @Test
testScrollStateForFling()1068     public void testScrollStateForFling() throws Throwable {
1069         TestAdapter testAdapter = new TestAdapter(10);
1070         TestLayoutManager tlm = new TestLayoutManager();
1071         RecyclerView recyclerView = new RecyclerView(getActivity());
1072         recyclerView.setAdapter(testAdapter);
1073         recyclerView.setLayoutManager(tlm);
1074         setRecyclerView(recyclerView);
1075         getInstrumentation().waitForIdleSync();
1076         assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1077         final int[] stateCnts = new int[10];
1078         final CountDownLatch latch = new CountDownLatch(2);
1079         recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
1080             @Override
1081             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
1082                 stateCnts[newState] = stateCnts[newState] + 1;
1083                 latch.countDown();
1084             }
1085         });
1086         final ViewConfiguration vc = ViewConfiguration.get(getActivity());
1087         final float fling = vc.getScaledMinimumFlingVelocity()
1088                 + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .1f;
1089         runTestOnUiThread(new Runnable() {
1090             @Override
1091             public void run() {
1092                 mRecyclerView.fling(0, Math.round(fling));
1093             }
1094         });
1095         latch.await(5, TimeUnit.SECONDS);
1096         assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]);
1097         assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
1098         assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]);
1099     }
1100 
1101     @Test
testScrollStateForFlingWithStop()1102     public void testScrollStateForFlingWithStop() throws Throwable {
1103         TestAdapter testAdapter = new TestAdapter(10);
1104         TestLayoutManager tlm = new TestLayoutManager();
1105         RecyclerView recyclerView = new RecyclerView(getActivity());
1106         recyclerView.setAdapter(testAdapter);
1107         recyclerView.setLayoutManager(tlm);
1108         setRecyclerView(recyclerView);
1109         getInstrumentation().waitForIdleSync();
1110         assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1111         final int[] stateCnts = new int[10];
1112         final CountDownLatch latch = new CountDownLatch(1);
1113         recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
1114             @Override
1115             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
1116                 stateCnts[newState] = stateCnts[newState] + 1;
1117                 latch.countDown();
1118             }
1119         });
1120         final ViewConfiguration vc = ViewConfiguration.get(getActivity());
1121         final float fling = vc.getScaledMinimumFlingVelocity()
1122                 + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .8f;
1123         runTestOnUiThread(new Runnable() {
1124             @Override
1125             public void run() {
1126                 mRecyclerView.fling(0, Math.round(fling));
1127             }
1128         });
1129         latch.await(5, TimeUnit.SECONDS);
1130         runTestOnUiThread(new Runnable() {
1131             @Override
1132             public void run() {
1133                 mRecyclerView.stopScroll();
1134             }
1135         });
1136         assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1137         assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]);
1138         assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
1139         assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]);
1140     }
1141 
1142     @Test
testScrollStateDrag()1143     public void testScrollStateDrag() throws Throwable {
1144         TestAdapter testAdapter = new TestAdapter(10);
1145         TestLayoutManager tlm = new TestLayoutManager();
1146         RecyclerView recyclerView = new RecyclerView(getActivity());
1147         recyclerView.setAdapter(testAdapter);
1148         recyclerView.setLayoutManager(tlm);
1149         setRecyclerView(recyclerView);
1150         getInstrumentation().waitForIdleSync();
1151         assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState());
1152         final int[] stateCnts = new int[10];
1153         recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
1154             @Override
1155             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
1156                 stateCnts[newState] = stateCnts[newState] + 1;
1157             }
1158         });
1159         drag(mRecyclerView, 0, 0, 0, 500, 5);
1160         assertEquals(0, stateCnts[SCROLL_STATE_SETTLING]);
1161         assertEquals(1, stateCnts[SCROLL_STATE_IDLE]);
1162         assertEquals(1, stateCnts[SCROLL_STATE_DRAGGING]);
1163     }
1164 
drag(ViewGroup view, float fromX, float toX, float fromY, float toY, int stepCount)1165     public void drag(ViewGroup view, float fromX, float toX, float fromY, float toY,
1166             int stepCount) throws Throwable {
1167         long downTime = SystemClock.uptimeMillis();
1168         long eventTime = SystemClock.uptimeMillis();
1169 
1170         float y = fromY;
1171         float x = fromX;
1172 
1173         float yStep = (toY - fromY) / stepCount;
1174         float xStep = (toX - fromX) / stepCount;
1175 
1176         MotionEvent event = MotionEvent.obtain(downTime, eventTime,
1177                 MotionEvent.ACTION_DOWN, x, y, 0);
1178         sendTouch(view, event);
1179         for (int i = 0; i < stepCount; ++i) {
1180             y += yStep;
1181             x += xStep;
1182             eventTime = SystemClock.uptimeMillis();
1183             event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0);
1184             sendTouch(view, event);
1185         }
1186 
1187         eventTime = SystemClock.uptimeMillis();
1188         event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0);
1189         sendTouch(view, event);
1190         getInstrumentation().waitForIdleSync();
1191     }
1192 
sendTouch(final ViewGroup view, final MotionEvent event)1193     private void sendTouch(final ViewGroup view, final MotionEvent event) throws Throwable {
1194         runTestOnUiThread(new Runnable() {
1195             @Override
1196             public void run() {
1197                 if (view.onInterceptTouchEvent(event)) {
1198                     view.onTouchEvent(event);
1199                 }
1200             }
1201         });
1202     }
1203 
1204     @Test
testRecycleScrap()1205     public void testRecycleScrap() throws Throwable {
1206         recycleScrapTest(false);
1207         removeRecyclerView();
1208         recycleScrapTest(true);
1209     }
1210 
recycleScrapTest(final boolean useRecycler)1211     public void recycleScrapTest(final boolean useRecycler) throws Throwable {
1212         TestAdapter testAdapter = new TestAdapter(10);
1213         final AtomicBoolean test = new AtomicBoolean(false);
1214         TestLayoutManager lm = new TestLayoutManager() {
1215             @Override
1216             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1217                 if (test.get()) {
1218                     try {
1219                         detachAndScrapAttachedViews(recycler);
1220                         for (int i = recycler.getScrapList().size() - 1; i >= 0; i--) {
1221                             if (useRecycler) {
1222                                 recycler.recycleView(recycler.getScrapList().get(i).itemView);
1223                             } else {
1224                                 removeAndRecycleView(recycler.getScrapList().get(i).itemView,
1225                                         recycler);
1226                             }
1227                         }
1228                         if (state.mOldChangedHolders != null) {
1229                             for (int i = state.mOldChangedHolders.size() - 1; i >= 0; i--) {
1230                                 if (useRecycler) {
1231                                     recycler.recycleView(
1232                                             state.mOldChangedHolders.valueAt(i).itemView);
1233                                 } else {
1234                                     removeAndRecycleView(
1235                                             state.mOldChangedHolders.valueAt(i).itemView, recycler);
1236                                 }
1237                             }
1238                         }
1239                         assertEquals("no scrap should be left over", 0, recycler.getScrapCount());
1240                         assertEquals("pre layout map should be empty", 0,
1241                                 state.mPreLayoutHolderMap.size());
1242                         assertEquals("post layout map should be empty", 0,
1243                                 state.mPostLayoutHolderMap.size());
1244                         if (state.mOldChangedHolders != null) {
1245                             assertEquals("post old change map should be empty", 0,
1246                                     state.mOldChangedHolders.size());
1247                         }
1248                     } catch (Throwable t) {
1249                         postExceptionToInstrumentation(t);
1250                     }
1251 
1252                 }
1253                 layoutRange(recycler, 0, 5);
1254                 layoutLatch.countDown();
1255                 super.onLayoutChildren(recycler, state);
1256             }
1257         };
1258         RecyclerView recyclerView = new RecyclerView(getActivity());
1259         recyclerView.setAdapter(testAdapter);
1260         recyclerView.setLayoutManager(lm);
1261         recyclerView.getItemAnimator().setSupportsChangeAnimations(true);
1262         lm.expectLayouts(1);
1263         setRecyclerView(recyclerView);
1264         lm.waitForLayout(2);
1265         test.set(true);
1266         lm.expectLayouts(1);
1267         testAdapter.changeAndNotify(3, 1);
1268         lm.waitForLayout(2);
1269         checkForMainThreadException();
1270     }
1271 
1272     @Test
testAccessRecyclerOnOnMeasure()1273     public void testAccessRecyclerOnOnMeasure() throws Throwable {
1274         accessRecyclerOnOnMeasureTest(false);
1275         removeRecyclerView();
1276         accessRecyclerOnOnMeasureTest(true);
1277     }
1278 
1279     @Test
testSmoothScrollWithRemovedItemsAndRemoveItem()1280     public void testSmoothScrollWithRemovedItemsAndRemoveItem() throws Throwable {
1281         smoothScrollTest(true);
1282     }
1283 
1284     @Test
testSmoothScrollWithRemovedItems()1285     public void testSmoothScrollWithRemovedItems() throws Throwable {
1286         smoothScrollTest(false);
1287     }
1288 
smoothScrollTest(final boolean removeItem)1289     public void smoothScrollTest(final boolean removeItem) throws Throwable {
1290         final LinearSmoothScroller[] lss = new LinearSmoothScroller[1];
1291         final CountDownLatch calledOnStart = new CountDownLatch(1);
1292         final CountDownLatch calledOnStop = new CountDownLatch(1);
1293         final int visibleChildCount = 10;
1294         TestLayoutManager lm = new TestLayoutManager() {
1295             int start = 0;
1296 
1297             @Override
1298             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1299                 super.onLayoutChildren(recycler, state);
1300                 layoutRange(recycler, start, visibleChildCount);
1301                 layoutLatch.countDown();
1302             }
1303 
1304             @Override
1305             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
1306                     RecyclerView.State state) {
1307                 start++;
1308                 if (DEBUG) {
1309                     Log.d(TAG, "on scroll, remove and recycling. start:" + start + ", cnt:"
1310                             + visibleChildCount);
1311                 }
1312                 removeAndRecycleAllViews(recycler);
1313                 layoutRange(recycler, start,
1314                         Math.max(state.getItemCount(), start + visibleChildCount));
1315                 return dy;
1316             }
1317 
1318             @Override
1319             public boolean canScrollVertically() {
1320                 return true;
1321             }
1322 
1323             @Override
1324             public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
1325                     int position) {
1326                 LinearSmoothScroller linearSmoothScroller =
1327                         new LinearSmoothScroller(recyclerView.getContext()) {
1328                             @Override
1329                             public PointF computeScrollVectorForPosition(int targetPosition) {
1330                                 return new PointF(0, 1);
1331                             }
1332 
1333                             @Override
1334                             protected void onStart() {
1335                                 super.onStart();
1336                                 calledOnStart.countDown();
1337                             }
1338 
1339                             @Override
1340                             protected void onStop() {
1341                                 super.onStop();
1342                                 calledOnStop.countDown();
1343                             }
1344                         };
1345                 linearSmoothScroller.setTargetPosition(position);
1346                 lss[0] = linearSmoothScroller;
1347                 startSmoothScroll(linearSmoothScroller);
1348             }
1349         };
1350         final RecyclerView rv = new RecyclerView(getActivity());
1351         TestAdapter testAdapter = new TestAdapter(500);
1352         rv.setLayoutManager(lm);
1353         rv.setAdapter(testAdapter);
1354         lm.expectLayouts(1);
1355         setRecyclerView(rv);
1356         lm.waitForLayout(1);
1357         // regular scroll
1358         final int targetPosition = visibleChildCount * (removeItem ? 30 : 4);
1359         runTestOnUiThread(new Runnable() {
1360             @Override
1361             public void run() {
1362                 rv.smoothScrollToPosition(targetPosition);
1363             }
1364         });
1365         if (DEBUG) {
1366             Log.d(TAG, "scrolling to target position " + targetPosition);
1367         }
1368         assertTrue("on start should be called very soon", calledOnStart.await(2, TimeUnit.SECONDS));
1369         if (removeItem) {
1370             final int newTarget = targetPosition - 10;
1371             testAdapter.deleteAndNotify(newTarget + 1, testAdapter.getItemCount() - newTarget - 1);
1372             final CountDownLatch targetCheck = new CountDownLatch(1);
1373             runTestOnUiThread(new Runnable() {
1374                 @Override
1375                 public void run() {
1376                     ViewCompat.postOnAnimationDelayed(rv, new Runnable() {
1377                         @Override
1378                         public void run() {
1379                             try {
1380                                 assertEquals("scroll position should be updated to next available",
1381                                         newTarget, lss[0].getTargetPosition());
1382                             } catch (Throwable t) {
1383                                 postExceptionToInstrumentation(t);
1384                             }
1385                             targetCheck.countDown();
1386                         }
1387                     }, 50);
1388                 }
1389             });
1390             assertTrue("target position should be checked on time ",
1391                     targetCheck.await(10, TimeUnit.SECONDS));
1392             checkForMainThreadException();
1393             assertTrue("on stop should be called", calledOnStop.await(30, TimeUnit.SECONDS));
1394             checkForMainThreadException();
1395             assertNotNull("should scroll to new target " + newTarget
1396                     , rv.findViewHolderForLayoutPosition(newTarget));
1397             if (DEBUG) {
1398                 Log.d(TAG, "on stop has been called on time");
1399             }
1400         } else {
1401             assertTrue("on stop should be called eventually",
1402                     calledOnStop.await(30, TimeUnit.SECONDS));
1403             assertNotNull("scroll to position should succeed",
1404                     rv.findViewHolderForLayoutPosition(targetPosition));
1405         }
1406         checkForMainThreadException();
1407     }
1408 
1409     @Test
testConsecutiveSmoothScroll()1410     public void testConsecutiveSmoothScroll() throws Throwable {
1411         final AtomicInteger visibleChildCount = new AtomicInteger(10);
1412         final AtomicInteger totalScrolled = new AtomicInteger(0);
1413         final TestLayoutManager lm = new TestLayoutManager() {
1414             int start = 0;
1415 
1416             @Override
1417             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1418                 super.onLayoutChildren(recycler, state);
1419                 layoutRange(recycler, start, visibleChildCount.get());
1420                 layoutLatch.countDown();
1421             }
1422 
1423             @Override
1424             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
1425                     RecyclerView.State state) {
1426                 totalScrolled.set(totalScrolled.get() + dy);
1427                 return dy;
1428             }
1429 
1430             @Override
1431             public boolean canScrollVertically() {
1432                 return true;
1433             }
1434         };
1435         final RecyclerView rv = new RecyclerView(getActivity());
1436         TestAdapter testAdapter = new TestAdapter(500);
1437         rv.setLayoutManager(lm);
1438         rv.setAdapter(testAdapter);
1439         lm.expectLayouts(1);
1440         setRecyclerView(rv);
1441         lm.waitForLayout(1);
1442         runTestOnUiThread(new Runnable() {
1443             @Override
1444             public void run() {
1445                 rv.smoothScrollBy(0, 2000);
1446             }
1447         });
1448         Thread.sleep(250);
1449         final AtomicInteger scrollAmt = new AtomicInteger();
1450         runTestOnUiThread(new Runnable() {
1451             @Override
1452             public void run() {
1453                 final int soFar = totalScrolled.get();
1454                 scrollAmt.set(soFar);
1455                 rv.smoothScrollBy(0, 5000 - soFar);
1456             }
1457         });
1458         while (rv.getScrollState() != SCROLL_STATE_IDLE) {
1459             Thread.sleep(100);
1460         }
1461         final int soFar = totalScrolled.get();
1462         assertEquals("second scroll should be competed properly", 5000, soFar);
1463     }
1464 
accessRecyclerOnOnMeasureTest(final boolean enablePredictiveAnimations)1465     public void accessRecyclerOnOnMeasureTest(final boolean enablePredictiveAnimations)
1466             throws Throwable {
1467         TestAdapter testAdapter = new TestAdapter(10);
1468         final AtomicInteger expectedOnMeasureStateCount = new AtomicInteger(10);
1469         TestLayoutManager lm = new TestLayoutManager() {
1470             @Override
1471             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1472                 super.onLayoutChildren(recycler, state);
1473                 try {
1474                     layoutRange(recycler, 0, state.getItemCount());
1475                     layoutLatch.countDown();
1476                 } catch (Throwable t) {
1477                     postExceptionToInstrumentation(t);
1478                 } finally {
1479                     layoutLatch.countDown();
1480                 }
1481             }
1482 
1483             @Override
1484             public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
1485                     int widthSpec, int heightSpec) {
1486                 try {
1487                     // make sure we access all views
1488                     for (int i = 0; i < state.getItemCount(); i++) {
1489                         View view = recycler.getViewForPosition(i);
1490                         assertNotNull(view);
1491                         assertEquals(i, getPosition(view));
1492                     }
1493                     assertEquals(state.toString(),
1494                             expectedOnMeasureStateCount.get(), state.getItemCount());
1495                 } catch (Throwable t) {
1496                     postExceptionToInstrumentation(t);
1497                 }
1498                 super.onMeasure(recycler, state, widthSpec, heightSpec);
1499             }
1500 
1501             @Override
1502             public boolean supportsPredictiveItemAnimations() {
1503                 return enablePredictiveAnimations;
1504             }
1505         };
1506         RecyclerView recyclerView = new RecyclerView(getActivity());
1507         recyclerView.setLayoutManager(lm);
1508         recyclerView.setAdapter(testAdapter);
1509         recyclerView.setLayoutManager(lm);
1510         lm.expectLayouts(1);
1511         setRecyclerView(recyclerView);
1512         lm.waitForLayout(2);
1513         checkForMainThreadException();
1514         lm.expectLayouts(1);
1515         if (!enablePredictiveAnimations) {
1516             expectedOnMeasureStateCount.set(15);
1517         }
1518         testAdapter.addAndNotify(4, 5);
1519         lm.waitForLayout(2);
1520         checkForMainThreadException();
1521     }
1522 
1523     @Test
testSetCompatibleAdapter()1524     public void testSetCompatibleAdapter() throws Throwable {
1525         compatibleAdapterTest(true, true);
1526         removeRecyclerView();
1527         compatibleAdapterTest(false, true);
1528         removeRecyclerView();
1529         compatibleAdapterTest(true, false);
1530         removeRecyclerView();
1531         compatibleAdapterTest(false, false);
1532         removeRecyclerView();
1533     }
1534 
compatibleAdapterTest(boolean useCustomPool, boolean removeAndRecycleExistingViews)1535     private void compatibleAdapterTest(boolean useCustomPool, boolean removeAndRecycleExistingViews)
1536             throws Throwable {
1537         TestAdapter testAdapter = new TestAdapter(10);
1538         final AtomicInteger recycledViewCount = new AtomicInteger();
1539         TestLayoutManager lm = new TestLayoutManager() {
1540             @Override
1541             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1542                 try {
1543                     layoutRange(recycler, 0, state.getItemCount());
1544                     layoutLatch.countDown();
1545                 } catch (Throwable t) {
1546                     postExceptionToInstrumentation(t);
1547                 } finally {
1548                     layoutLatch.countDown();
1549                 }
1550             }
1551         };
1552         RecyclerView recyclerView = new RecyclerView(getActivity());
1553         recyclerView.setLayoutManager(lm);
1554         recyclerView.setAdapter(testAdapter);
1555         recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() {
1556             @Override
1557             public void onViewRecycled(RecyclerView.ViewHolder holder) {
1558                 recycledViewCount.incrementAndGet();
1559             }
1560         });
1561         lm.expectLayouts(1);
1562         setRecyclerView(recyclerView, !useCustomPool);
1563         lm.waitForLayout(2);
1564         checkForMainThreadException();
1565         lm.expectLayouts(1);
1566         swapAdapter(new TestAdapter(10), removeAndRecycleExistingViews);
1567         lm.waitForLayout(2);
1568         checkForMainThreadException();
1569         if (removeAndRecycleExistingViews) {
1570             assertTrue("Previous views should be recycled", recycledViewCount.get() > 0);
1571         } else {
1572             assertEquals("No views should be recycled if adapters are compatible and developer "
1573                     + "did not request a recycle", 0, recycledViewCount.get());
1574         }
1575     }
1576 
1577     @Test
testSetIncompatibleAdapter()1578     public void testSetIncompatibleAdapter() throws Throwable {
1579         incompatibleAdapterTest(true);
1580         incompatibleAdapterTest(false);
1581     }
1582 
incompatibleAdapterTest(boolean useCustomPool)1583     public void incompatibleAdapterTest(boolean useCustomPool) throws Throwable {
1584         TestAdapter testAdapter = new TestAdapter(10);
1585         TestLayoutManager lm = new TestLayoutManager() {
1586             @Override
1587             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1588                 super.onLayoutChildren(recycler, state);
1589                 try {
1590                     layoutRange(recycler, 0, state.getItemCount());
1591                     layoutLatch.countDown();
1592                 } catch (Throwable t) {
1593                     postExceptionToInstrumentation(t);
1594                 } finally {
1595                     layoutLatch.countDown();
1596                 }
1597             }
1598         };
1599         RecyclerView recyclerView = new RecyclerView(getActivity());
1600         recyclerView.setLayoutManager(lm);
1601         recyclerView.setAdapter(testAdapter);
1602         recyclerView.setLayoutManager(lm);
1603         lm.expectLayouts(1);
1604         setRecyclerView(recyclerView, !useCustomPool);
1605         lm.waitForLayout(2);
1606         checkForMainThreadException();
1607         lm.expectLayouts(1);
1608         setAdapter(new TestAdapter2(10));
1609         lm.waitForLayout(2);
1610         checkForMainThreadException();
1611     }
1612 
1613     @Test
testRecycleIgnored()1614     public void testRecycleIgnored() throws Throwable {
1615         final TestAdapter adapter = new TestAdapter(10);
1616         final TestLayoutManager lm = new TestLayoutManager() {
1617             @Override
1618             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1619                 layoutRange(recycler, 0, 5);
1620                 layoutLatch.countDown();
1621             }
1622         };
1623         final RecyclerView recyclerView = new RecyclerView(getActivity());
1624         recyclerView.setAdapter(adapter);
1625         recyclerView.setLayoutManager(lm);
1626         lm.expectLayouts(1);
1627         setRecyclerView(recyclerView);
1628         lm.waitForLayout(2);
1629         runTestOnUiThread(new Runnable() {
1630             @Override
1631             public void run() {
1632                 View child1 = lm.findViewByPosition(0);
1633                 View child2 = lm.findViewByPosition(1);
1634                 lm.ignoreView(child1);
1635                 lm.ignoreView(child2);
1636 
1637                 lm.removeAndRecycleAllViews(recyclerView.mRecycler);
1638                 assertEquals("ignored child should not be recycled or removed", 2,
1639                         lm.getChildCount());
1640 
1641                 Throwable[] throwables = new Throwable[1];
1642                 try {
1643                     lm.removeAndRecycleView(child1, mRecyclerView.mRecycler);
1644                 } catch (Throwable t) {
1645                     throwables[0] = t;
1646                 }
1647                 assertTrue("Trying to recycle an ignored view should throw IllegalArgException "
1648                         , throwables[0] instanceof IllegalArgumentException);
1649                 lm.removeAllViews();
1650                 assertEquals("ignored child should be removed as well ", 0, lm.getChildCount());
1651             }
1652         });
1653     }
1654 
1655     @Test
testFindIgnoredByPosition()1656     public void testFindIgnoredByPosition() throws Throwable {
1657         final TestAdapter adapter = new TestAdapter(10);
1658         final TestLayoutManager lm = new TestLayoutManager() {
1659             @Override
1660             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1661                 detachAndScrapAttachedViews(recycler);
1662                 layoutRange(recycler, 0, 5);
1663                 layoutLatch.countDown();
1664             }
1665         };
1666         final RecyclerView recyclerView = new RecyclerView(getActivity());
1667         recyclerView.setAdapter(adapter);
1668         recyclerView.setLayoutManager(lm);
1669         lm.expectLayouts(1);
1670         setRecyclerView(recyclerView);
1671         lm.waitForLayout(2);
1672         Thread.sleep(5000);
1673         final int pos = 1;
1674         final View[] ignored = new View[1];
1675         runTestOnUiThread(new Runnable() {
1676             @Override
1677             public void run() {
1678                 View child = lm.findViewByPosition(pos);
1679                 lm.ignoreView(child);
1680                 ignored[0] = child;
1681             }
1682         });
1683         assertNotNull("ignored child should not be null", ignored[0]);
1684         assertNull("find view by position should not return ignored child",
1685                 lm.findViewByPosition(pos));
1686         lm.expectLayouts(1);
1687         requestLayoutOnUIThread(mRecyclerView);
1688         lm.waitForLayout(1);
1689         assertEquals("child count should be ", 6, lm.getChildCount());
1690         View replacement = lm.findViewByPosition(pos);
1691         assertNotNull("re-layout should replace ignored child w/ another one", replacement);
1692         assertNotSame("replacement should be a different view", replacement, ignored[0]);
1693     }
1694 
1695     @Test
testInvalidateAllDecorOffsets()1696     public void testInvalidateAllDecorOffsets() throws Throwable {
1697         final TestAdapter adapter = new TestAdapter(10);
1698         final RecyclerView recyclerView = new RecyclerView(getActivity());
1699         final AtomicBoolean invalidatedOffsets = new AtomicBoolean(true);
1700         recyclerView.setAdapter(adapter);
1701         final AtomicInteger layoutCount = new AtomicInteger(4);
1702         final RecyclerView.ItemDecoration dummyItemDecoration = new RecyclerView.ItemDecoration() {
1703         };
1704         TestLayoutManager testLayoutManager = new TestLayoutManager() {
1705             @Override
1706             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1707                 try {
1708                     // test
1709                     for (int i = 0; i < getChildCount(); i++) {
1710                         View child = getChildAt(i);
1711                         RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
1712                                 child.getLayoutParams();
1713                         assertEquals(
1714                                 "Decor insets validation for VH should have expected value.",
1715                                 invalidatedOffsets.get(), lp.mInsetsDirty);
1716                     }
1717                     for (RecyclerView.ViewHolder vh : mRecyclerView.mRecycler.mCachedViews) {
1718                         RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
1719                                 vh.itemView.getLayoutParams();
1720                         assertEquals(
1721                                 "Decor insets invalidation in cache for VH should have expected "
1722                                         + "value.",
1723                                 invalidatedOffsets.get(), lp.mInsetsDirty);
1724                     }
1725                     detachAndScrapAttachedViews(recycler);
1726                     layoutRange(recycler, 0, layoutCount.get());
1727                 } catch (Throwable t) {
1728                     postExceptionToInstrumentation(t);
1729                 } finally {
1730                     layoutLatch.countDown();
1731                 }
1732             }
1733 
1734             @Override
1735             public boolean supportsPredictiveItemAnimations() {
1736                 return false;
1737             }
1738         };
1739         // first layout
1740         recyclerView.setItemViewCacheSize(5);
1741         recyclerView.setLayoutManager(testLayoutManager);
1742         testLayoutManager.expectLayouts(1);
1743         setRecyclerView(recyclerView, true, false);
1744         testLayoutManager.waitForLayout(2);
1745         checkForMainThreadException();
1746 
1747         // re-layout w/o any change
1748         invalidatedOffsets.set(false);
1749         testLayoutManager.expectLayouts(1);
1750         requestLayoutOnUIThread(recyclerView);
1751         testLayoutManager.waitForLayout(1);
1752         checkForMainThreadException();
1753 
1754         // invalidate w/o an item decorator
1755 
1756         invalidateDecorOffsets(recyclerView);
1757         testLayoutManager.expectLayouts(1);
1758         invalidateDecorOffsets(recyclerView);
1759         testLayoutManager.assertNoLayout("layout should not happen", 2);
1760         checkForMainThreadException();
1761 
1762         // set item decorator, should invalidate
1763         invalidatedOffsets.set(true);
1764         testLayoutManager.expectLayouts(1);
1765         addItemDecoration(mRecyclerView, dummyItemDecoration);
1766         testLayoutManager.waitForLayout(1);
1767         checkForMainThreadException();
1768 
1769         // re-layout w/o any change
1770         invalidatedOffsets.set(false);
1771         testLayoutManager.expectLayouts(1);
1772         requestLayoutOnUIThread(recyclerView);
1773         testLayoutManager.waitForLayout(1);
1774         checkForMainThreadException();
1775 
1776         // invalidate w/ item decorator
1777         invalidatedOffsets.set(true);
1778         invalidateDecorOffsets(recyclerView);
1779         testLayoutManager.expectLayouts(1);
1780         invalidateDecorOffsets(recyclerView);
1781         testLayoutManager.waitForLayout(2);
1782         checkForMainThreadException();
1783 
1784         // trigger cache.
1785         layoutCount.set(3);
1786         invalidatedOffsets.set(false);
1787         testLayoutManager.expectLayouts(1);
1788         requestLayoutOnUIThread(mRecyclerView);
1789         testLayoutManager.waitForLayout(1);
1790         checkForMainThreadException();
1791         assertEquals("a view should be cached", 1, mRecyclerView.mRecycler.mCachedViews.size());
1792 
1793         layoutCount.set(5);
1794         invalidatedOffsets.set(true);
1795         testLayoutManager.expectLayouts(1);
1796         invalidateDecorOffsets(recyclerView);
1797         testLayoutManager.waitForLayout(1);
1798         checkForMainThreadException();
1799 
1800         // remove item decorator
1801         invalidatedOffsets.set(true);
1802         testLayoutManager.expectLayouts(1);
1803         removeItemDecoration(mRecyclerView, dummyItemDecoration);
1804         testLayoutManager.waitForLayout(1);
1805         checkForMainThreadException();
1806     }
1807 
addItemDecoration(final RecyclerView recyclerView, final RecyclerView.ItemDecoration itemDecoration)1808     public void addItemDecoration(final RecyclerView recyclerView, final
1809     RecyclerView.ItemDecoration itemDecoration) throws Throwable {
1810         runTestOnUiThread(new Runnable() {
1811             @Override
1812             public void run() {
1813                 recyclerView.addItemDecoration(itemDecoration);
1814             }
1815         });
1816     }
1817 
removeItemDecoration(final RecyclerView recyclerView, final RecyclerView.ItemDecoration itemDecoration)1818     public void removeItemDecoration(final RecyclerView recyclerView, final
1819     RecyclerView.ItemDecoration itemDecoration) throws Throwable {
1820         runTestOnUiThread(new Runnable() {
1821             @Override
1822             public void run() {
1823                 recyclerView.removeItemDecoration(itemDecoration);
1824             }
1825         });
1826     }
1827 
invalidateDecorOffsets(final RecyclerView recyclerView)1828     public void invalidateDecorOffsets(final RecyclerView recyclerView) throws Throwable {
1829         runTestOnUiThread(new Runnable() {
1830             @Override
1831             public void run() {
1832                 recyclerView.invalidateItemDecorations();
1833             }
1834         });
1835     }
1836 
1837     @Test
testInvalidateDecorOffsets()1838     public void testInvalidateDecorOffsets() throws Throwable {
1839         final TestAdapter adapter = new TestAdapter(10);
1840         adapter.setHasStableIds(true);
1841         final RecyclerView recyclerView = new RecyclerView(getActivity());
1842         recyclerView.setAdapter(adapter);
1843 
1844         final Map<Long, Boolean> changes = new HashMap<Long, Boolean>();
1845 
1846         TestLayoutManager testLayoutManager = new TestLayoutManager() {
1847             @Override
1848             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1849                 try {
1850                     if (changes.size() > 0) {
1851                         // test
1852                         for (int i = 0; i < getChildCount(); i++) {
1853                             View child = getChildAt(i);
1854                             RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
1855                                     child.getLayoutParams();
1856                             RecyclerView.ViewHolder vh = lp.mViewHolder;
1857                             if (!changes.containsKey(vh.getItemId())) {
1858                                 continue; //nothing to test
1859                             }
1860                             assertEquals(
1861                                     "Decord insets validation for VH should have expected value.",
1862                                     changes.get(vh.getItemId()).booleanValue(),
1863                                     lp.mInsetsDirty);
1864                         }
1865                     }
1866                     detachAndScrapAttachedViews(recycler);
1867                     layoutRange(recycler, 0, state.getItemCount());
1868                 } catch (Throwable t) {
1869                     postExceptionToInstrumentation(t);
1870                 } finally {
1871                     layoutLatch.countDown();
1872                 }
1873             }
1874 
1875             @Override
1876             public boolean supportsPredictiveItemAnimations() {
1877                 return false;
1878             }
1879         };
1880         recyclerView.setLayoutManager(testLayoutManager);
1881         testLayoutManager.expectLayouts(1);
1882         setRecyclerView(recyclerView);
1883         testLayoutManager.waitForLayout(2);
1884         int itemAddedTo = 5;
1885         for (int i = 0; i < itemAddedTo; i++) {
1886             changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false);
1887         }
1888         for (int i = itemAddedTo; i < mRecyclerView.getChildCount(); i++) {
1889             changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true);
1890         }
1891         testLayoutManager.expectLayouts(1);
1892         adapter.addAndNotify(5, 1);
1893         testLayoutManager.waitForLayout(2);
1894         checkForMainThreadException();
1895 
1896         changes.clear();
1897         int[] changedItems = new int[]{3, 5, 6};
1898         for (int i = 0; i < adapter.getItemCount(); i++) {
1899             changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false);
1900         }
1901         for (int i = 0; i < changedItems.length; i++) {
1902             changes.put(mRecyclerView.findViewHolderForLayoutPosition(changedItems[i]).getItemId(),
1903                     true);
1904         }
1905         testLayoutManager.expectLayouts(1);
1906         adapter.changePositionsAndNotify(changedItems);
1907         testLayoutManager.waitForLayout(2);
1908         checkForMainThreadException();
1909 
1910         for (int i = 0; i < adapter.getItemCount(); i++) {
1911             changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true);
1912         }
1913         testLayoutManager.expectLayouts(1);
1914         adapter.dispatchDataSetChanged();
1915         testLayoutManager.waitForLayout(2);
1916         checkForMainThreadException();
1917     }
1918 
1919     @Test
testMovingViaStableIds()1920     public void testMovingViaStableIds() throws Throwable {
1921         stableIdsMoveTest(true);
1922         removeRecyclerView();
1923         stableIdsMoveTest(false);
1924         removeRecyclerView();
1925     }
1926 
stableIdsMoveTest(final boolean supportsPredictive)1927     public void stableIdsMoveTest(final boolean supportsPredictive) throws Throwable {
1928         final TestAdapter testAdapter = new TestAdapter(10);
1929         testAdapter.setHasStableIds(true);
1930         final AtomicBoolean test = new AtomicBoolean(false);
1931         final int movedViewFromIndex = 3;
1932         final int movedViewToIndex = 6;
1933         final View[] movedView = new View[1];
1934         TestLayoutManager lm = new TestLayoutManager() {
1935             @Override
1936             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1937                 detachAndScrapAttachedViews(recycler);
1938                 try {
1939                     if (test.get()) {
1940                         if (state.isPreLayout()) {
1941                             View view = recycler.getViewForPosition(movedViewFromIndex, true);
1942                             assertSame("In pre layout, should be able to get moved view w/ old "
1943                                     + "position", movedView[0], view);
1944                             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
1945                             assertTrue("it should come from scrap", holder.wasReturnedFromScrap());
1946                             // clear scrap flag
1947                             holder.clearReturnedFromScrapFlag();
1948                         } else {
1949                             View view = recycler.getViewForPosition(movedViewToIndex, true);
1950                             assertSame("In post layout, should be able to get moved view w/ new "
1951                                     + "position", movedView[0], view);
1952                             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
1953                             assertTrue("it should come from scrap", holder.wasReturnedFromScrap());
1954                             // clear scrap flag
1955                             holder.clearReturnedFromScrapFlag();
1956                         }
1957                     }
1958                     layoutRange(recycler, 0, state.getItemCount());
1959                 } catch (Throwable t) {
1960                     postExceptionToInstrumentation(t);
1961                 } finally {
1962                     layoutLatch.countDown();
1963                 }
1964 
1965 
1966             }
1967 
1968             @Override
1969             public boolean supportsPredictiveItemAnimations() {
1970                 return supportsPredictive;
1971             }
1972         };
1973         RecyclerView recyclerView = new RecyclerView(this.getActivity());
1974         recyclerView.setAdapter(testAdapter);
1975         recyclerView.setLayoutManager(lm);
1976         lm.expectLayouts(1);
1977         setRecyclerView(recyclerView);
1978         lm.waitForLayout(1);
1979 
1980         movedView[0] = recyclerView.getChildAt(movedViewFromIndex);
1981         test.set(true);
1982         lm.expectLayouts(supportsPredictive ? 2 : 1);
1983         runTestOnUiThread(new Runnable() {
1984             @Override
1985             public void run() {
1986                 Item item = testAdapter.mItems.remove(movedViewFromIndex);
1987                 testAdapter.mItems.add(movedViewToIndex, item);
1988                 testAdapter.notifyItemRemoved(movedViewFromIndex);
1989                 testAdapter.notifyItemInserted(movedViewToIndex);
1990             }
1991         });
1992         lm.waitForLayout(2);
1993         checkForMainThreadException();
1994     }
1995 
1996     @Test
testAdapterChangeDuringLayout()1997     public void testAdapterChangeDuringLayout() throws Throwable {
1998         adapterChangeInMainThreadTest("notifyDataSetChanged", new Runnable() {
1999             @Override
2000             public void run() {
2001                 mRecyclerView.getAdapter().notifyDataSetChanged();
2002             }
2003         });
2004 
2005         adapterChangeInMainThreadTest("notifyItemChanged", new Runnable() {
2006             @Override
2007             public void run() {
2008                 mRecyclerView.getAdapter().notifyItemChanged(2);
2009             }
2010         });
2011 
2012         adapterChangeInMainThreadTest("notifyItemInserted", new Runnable() {
2013             @Override
2014             public void run() {
2015                 mRecyclerView.getAdapter().notifyItemInserted(2);
2016             }
2017         });
2018         adapterChangeInMainThreadTest("notifyItemRemoved", new Runnable() {
2019             @Override
2020             public void run() {
2021                 mRecyclerView.getAdapter().notifyItemRemoved(2);
2022             }
2023         });
2024     }
2025 
adapterChangeInMainThreadTest(String msg, final Runnable onLayoutRunnable)2026     public void adapterChangeInMainThreadTest(String msg,
2027             final Runnable onLayoutRunnable) throws Throwable {
2028         final AtomicBoolean doneFirstLayout = new AtomicBoolean(false);
2029         TestAdapter testAdapter = new TestAdapter(10);
2030         TestLayoutManager lm = new TestLayoutManager() {
2031             @Override
2032             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2033                 super.onLayoutChildren(recycler, state);
2034                 try {
2035                     layoutRange(recycler, 0, state.getItemCount());
2036                     if (doneFirstLayout.get()) {
2037                         onLayoutRunnable.run();
2038                     }
2039                 } catch (Throwable t) {
2040                     postExceptionToInstrumentation(t);
2041                 } finally {
2042                     layoutLatch.countDown();
2043                 }
2044 
2045             }
2046         };
2047         RecyclerView recyclerView = new RecyclerView(getActivity());
2048         recyclerView.setLayoutManager(lm);
2049         recyclerView.setAdapter(testAdapter);
2050         lm.expectLayouts(1);
2051         setRecyclerView(recyclerView);
2052         lm.waitForLayout(2);
2053         doneFirstLayout.set(true);
2054         lm.expectLayouts(1);
2055         requestLayoutOnUIThread(recyclerView);
2056         lm.waitForLayout(2);
2057         removeRecyclerView();
2058         assertTrue("Invalid data updates should be caught:" + msg,
2059                 mainThreadException instanceof IllegalStateException);
2060         mainThreadException = null;
2061     }
2062 
2063     @Test
testAdapterChangeDuringScroll()2064     public void testAdapterChangeDuringScroll() throws Throwable {
2065         for (int orientation : new int[]{OrientationHelper.HORIZONTAL,
2066                 OrientationHelper.VERTICAL}) {
2067             adapterChangeDuringScrollTest("notifyDataSetChanged", orientation,
2068                     new Runnable() {
2069                         @Override
2070                         public void run() {
2071                             mRecyclerView.getAdapter().notifyDataSetChanged();
2072                         }
2073                     });
2074             adapterChangeDuringScrollTest("notifyItemChanged", orientation, new Runnable() {
2075                 @Override
2076                 public void run() {
2077                     mRecyclerView.getAdapter().notifyItemChanged(2);
2078                 }
2079             });
2080 
2081             adapterChangeDuringScrollTest("notifyItemInserted", orientation, new Runnable() {
2082                 @Override
2083                 public void run() {
2084                     mRecyclerView.getAdapter().notifyItemInserted(2);
2085                 }
2086             });
2087             adapterChangeDuringScrollTest("notifyItemRemoved", orientation, new Runnable() {
2088                 @Override
2089                 public void run() {
2090                     mRecyclerView.getAdapter().notifyItemRemoved(2);
2091                 }
2092             });
2093         }
2094     }
2095 
adapterChangeDuringScrollTest(String msg, final int orientation, final Runnable onScrollRunnable)2096     public void adapterChangeDuringScrollTest(String msg, final int orientation,
2097             final Runnable onScrollRunnable) throws Throwable {
2098         TestAdapter testAdapter = new TestAdapter(100);
2099         TestLayoutManager lm = new TestLayoutManager() {
2100             @Override
2101             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2102                 super.onLayoutChildren(recycler, state);
2103                 try {
2104                     layoutRange(recycler, 0, 10);
2105                 } catch (Throwable t) {
2106                     postExceptionToInstrumentation(t);
2107                 } finally {
2108                     layoutLatch.countDown();
2109                 }
2110             }
2111 
2112             @Override
2113             public boolean canScrollVertically() {
2114                 return orientation == OrientationHelper.VERTICAL;
2115             }
2116 
2117             @Override
2118             public boolean canScrollHorizontally() {
2119                 return orientation == OrientationHelper.HORIZONTAL;
2120             }
2121 
2122             public int mockScroll() {
2123                 try {
2124                     onScrollRunnable.run();
2125                 } catch (Throwable t) {
2126                     postExceptionToInstrumentation(t);
2127                 } finally {
2128                     layoutLatch.countDown();
2129                 }
2130                 return 0;
2131             }
2132 
2133             @Override
2134             public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
2135                     RecyclerView.State state) {
2136                 return mockScroll();
2137             }
2138 
2139             @Override
2140             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
2141                     RecyclerView.State state) {
2142                 return mockScroll();
2143             }
2144         };
2145         RecyclerView recyclerView = new RecyclerView(getActivity());
2146         recyclerView.setLayoutManager(lm);
2147         recyclerView.setAdapter(testAdapter);
2148         lm.expectLayouts(1);
2149         setRecyclerView(recyclerView);
2150         lm.waitForLayout(2);
2151         lm.expectLayouts(1);
2152         scrollBy(200);
2153         lm.waitForLayout(2);
2154         removeRecyclerView();
2155         assertTrue("Invalid data updates should be caught:" + msg,
2156                 mainThreadException instanceof IllegalStateException);
2157         mainThreadException = null;
2158     }
2159 
2160     @Test
testRecycleOnDetach()2161     public void testRecycleOnDetach() throws Throwable {
2162         final RecyclerView recyclerView = new RecyclerView(getActivity());
2163         final TestAdapter testAdapter = new TestAdapter(10);
2164         final AtomicBoolean didRunOnDetach = new AtomicBoolean(false);
2165         final TestLayoutManager lm = new TestLayoutManager() {
2166             @Override
2167             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2168                 super.onLayoutChildren(recycler, state);
2169                 layoutRange(recycler, 0, state.getItemCount() - 1);
2170                 layoutLatch.countDown();
2171             }
2172 
2173             @Override
2174             public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
2175                 super.onDetachedFromWindow(view, recycler);
2176                 didRunOnDetach.set(true);
2177                 removeAndRecycleAllViews(recycler);
2178             }
2179         };
2180         recyclerView.setAdapter(testAdapter);
2181         recyclerView.setLayoutManager(lm);
2182         lm.expectLayouts(1);
2183         setRecyclerView(recyclerView);
2184         lm.waitForLayout(2);
2185         removeRecyclerView();
2186         assertTrue("When recycler view is removed, detach should run", didRunOnDetach.get());
2187         assertEquals("All children should be recycled", recyclerView.getChildCount(), 0);
2188     }
2189 
2190     @Test
testUpdatesWhileDetached()2191     public void testUpdatesWhileDetached() throws Throwable {
2192         final RecyclerView recyclerView = new RecyclerView(getActivity());
2193         final int initialAdapterSize = 20;
2194         final TestAdapter adapter = new TestAdapter(initialAdapterSize);
2195         final AtomicInteger layoutCount = new AtomicInteger(0);
2196         TestLayoutManager lm = new TestLayoutManager() {
2197             @Override
2198             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2199                 super.onLayoutChildren(recycler, state);
2200                 layoutRange(recycler, 0, 5);
2201                 layoutCount.incrementAndGet();
2202                 layoutLatch.countDown();
2203             }
2204         };
2205         recyclerView.setAdapter(adapter);
2206         recyclerView.setLayoutManager(lm);
2207         recyclerView.setHasFixedSize(true);
2208         lm.expectLayouts(1);
2209         adapter.addAndNotify(4, 5);
2210         lm.assertNoLayout("When RV is not attached, layout should not happen", 1);
2211     }
2212 
2213     @Test
testUpdatesAfterDetach()2214     public void testUpdatesAfterDetach() throws Throwable {
2215         final RecyclerView recyclerView = new RecyclerView(getActivity());
2216         final int initialAdapterSize = 20;
2217         final TestAdapter adapter = new TestAdapter(initialAdapterSize);
2218         final AtomicInteger layoutCount = new AtomicInteger(0);
2219         TestLayoutManager lm = new TestLayoutManager() {
2220             @Override
2221             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2222                 super.onLayoutChildren(recycler, state);
2223                 layoutRange(recycler, 0, 5);
2224                 layoutCount.incrementAndGet();
2225                 layoutLatch.countDown();
2226             }
2227         };
2228         recyclerView.setAdapter(adapter);
2229         recyclerView.setLayoutManager(lm);
2230         lm.expectLayouts(1);
2231         recyclerView.setHasFixedSize(true);
2232         setRecyclerView(recyclerView);
2233         lm.waitForLayout(2);
2234         lm.expectLayouts(1);
2235         final int prevLayoutCount = layoutCount.get();
2236         runTestOnUiThread(new Runnable() {
2237             @Override
2238             public void run() {
2239                 try {
2240                     adapter.addAndNotify(4, 5);
2241                     removeRecyclerView();
2242                 } catch (Throwable throwable) {
2243                     postExceptionToInstrumentation(throwable);
2244                 }
2245             }
2246         });
2247         checkForMainThreadException();
2248 
2249         lm.assertNoLayout("When RV is not attached, layout should not happen", 1);
2250         assertEquals("No extra layout should happen when detached", prevLayoutCount,
2251                 layoutCount.get());
2252     }
2253 
2254     @Test
testNotifyDataSetChangedWithStableIds()2255     public void testNotifyDataSetChangedWithStableIds() throws Throwable {
2256         final int defaultViewType = 1;
2257         final Map<Item, Integer> viewTypeMap = new HashMap<Item, Integer>();
2258         final Map<Integer, Integer> oldPositionToNewPositionMapping =
2259                 new HashMap<Integer, Integer>();
2260         final TestAdapter adapter = new TestAdapter(100) {
2261             @Override
2262             public int getItemViewType(int position) {
2263                 Integer type = viewTypeMap.get(mItems.get(position));
2264                 return type == null ? defaultViewType : type;
2265             }
2266 
2267             @Override
2268             public long getItemId(int position) {
2269                 return mItems.get(position).mId;
2270             }
2271         };
2272         adapter.setHasStableIds(true);
2273         final ArrayList<Item> previousItems = new ArrayList<Item>();
2274         previousItems.addAll(adapter.mItems);
2275 
2276         final AtomicInteger layoutStart = new AtomicInteger(50);
2277         final AtomicBoolean validate = new AtomicBoolean(false);
2278         final int childCount = 10;
2279         final TestLayoutManager lm = new TestLayoutManager() {
2280             @Override
2281             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2282                 try {
2283                     super.onLayoutChildren(recycler, state);
2284                     if (validate.get()) {
2285                         assertEquals("Cached views should be kept", 5, recycler
2286                                 .mCachedViews.size());
2287                         for (RecyclerView.ViewHolder vh : recycler.mCachedViews) {
2288                             TestViewHolder tvh = (TestViewHolder) vh;
2289                             assertTrue("view holder should be marked for update",
2290                                     tvh.needsUpdate());
2291                             assertTrue("view holder should be marked as invalid", tvh.isInvalid());
2292                         }
2293                     }
2294                     detachAndScrapAttachedViews(recycler);
2295                     if (validate.get()) {
2296                         assertEquals("cache size should stay the same", 5,
2297                                 recycler.mCachedViews.size());
2298                         assertEquals("all views should be scrapped", childCount,
2299                                 recycler.getScrapList().size());
2300                         for (RecyclerView.ViewHolder vh : recycler.getScrapList()) {
2301                             // TODO create test case for type change
2302                             TestViewHolder tvh = (TestViewHolder) vh;
2303                             assertTrue("view holder should be marked for update",
2304                                     tvh.needsUpdate());
2305                             assertTrue("view holder should be marked as invalid", tvh.isInvalid());
2306                         }
2307                     }
2308                     layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount);
2309                     if (validate.get()) {
2310                         for (int i = 0; i < getChildCount(); i++) {
2311                             View view = getChildAt(i);
2312                             TestViewHolder tvh = (TestViewHolder) mRecyclerView
2313                                     .getChildViewHolder(view);
2314                             final int oldPos = previousItems.indexOf(tvh.mBoundItem);
2315                             assertEquals("view holder's position should be correct",
2316                                     oldPositionToNewPositionMapping.get(oldPos).intValue(),
2317                                     tvh.getLayoutPosition());
2318                             ;
2319                         }
2320                     }
2321                 } catch (Throwable t) {
2322                     postExceptionToInstrumentation(t);
2323                 } finally {
2324                     layoutLatch.countDown();
2325                 }
2326             }
2327         };
2328         final RecyclerView recyclerView = new RecyclerView(getActivity());
2329         recyclerView.setItemAnimator(null);
2330         recyclerView.setAdapter(adapter);
2331         recyclerView.setLayoutManager(lm);
2332         recyclerView.setItemViewCacheSize(10);
2333         lm.expectLayouts(1);
2334         setRecyclerView(recyclerView);
2335         lm.waitForLayout(2);
2336         checkForMainThreadException();
2337         getInstrumentation().waitForIdleSync();
2338         layoutStart.set(layoutStart.get() + 5);//55
2339         lm.expectLayouts(1);
2340         requestLayoutOnUIThread(recyclerView);
2341         lm.waitForLayout(2);
2342         validate.set(true);
2343         lm.expectLayouts(1);
2344         runTestOnUiThread(new Runnable() {
2345             @Override
2346             public void run() {
2347                 try {
2348                     adapter.moveItems(false,
2349                             new int[]{50, 56}, new int[]{51, 1}, new int[]{52, 2},
2350                             new int[]{53, 54}, new int[]{60, 61}, new int[]{62, 64},
2351                             new int[]{75, 58});
2352                     for (int i = 0; i < previousItems.size(); i++) {
2353                         Item item = previousItems.get(i);
2354                         oldPositionToNewPositionMapping.put(i, adapter.mItems.indexOf(item));
2355                     }
2356                     adapter.dispatchDataSetChanged();
2357                 } catch (Throwable throwable) {
2358                     postExceptionToInstrumentation(throwable);
2359                 }
2360             }
2361         });
2362         lm.waitForLayout(2);
2363         checkForMainThreadException();
2364     }
2365 
2366     @Test
testCallbacksDuringAdapterSwap()2367     public void testCallbacksDuringAdapterSwap() throws Throwable {
2368         callbacksDuringAdapterChange(true);
2369     }
2370 
2371     @Test
testCallbacksDuringAdapterSet()2372     public void testCallbacksDuringAdapterSet() throws Throwable {
2373         callbacksDuringAdapterChange(false);
2374     }
2375 
callbacksDuringAdapterChange(boolean swap)2376     public void callbacksDuringAdapterChange(boolean swap) throws Throwable {
2377         final TestAdapter2 adapter1 = swap ? createBinderCheckingAdapter()
2378                 : createOwnerCheckingAdapter();
2379         final TestAdapter2 adapter2 = swap ? createBinderCheckingAdapter()
2380                 : createOwnerCheckingAdapter();
2381 
2382         TestLayoutManager tlm = new TestLayoutManager() {
2383             @Override
2384             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2385                 try {
2386                     layoutRange(recycler, 0, state.getItemCount());
2387                 } catch (Throwable t) {
2388                     postExceptionToInstrumentation(t);
2389                 }
2390                 layoutLatch.countDown();
2391             }
2392         };
2393         RecyclerView rv = new RecyclerView(getActivity());
2394         rv.setAdapter(adapter1);
2395         rv.setLayoutManager(tlm);
2396         tlm.expectLayouts(1);
2397         setRecyclerView(rv);
2398         tlm.waitForLayout(1);
2399         checkForMainThreadException();
2400         tlm.expectLayouts(1);
2401         if (swap) {
2402             swapAdapter(adapter2, true);
2403         } else {
2404             setAdapter(adapter2);
2405         }
2406         checkForMainThreadException();
2407         tlm.waitForLayout(1);
2408         checkForMainThreadException();
2409     }
2410 
createOwnerCheckingAdapter()2411     private TestAdapter2 createOwnerCheckingAdapter() {
2412         return new TestAdapter2(10) {
2413             @Override
2414             public void onViewRecycled(TestViewHolder2 holder) {
2415                 assertSame("on recycled should be called w/ the creator adapter", this,
2416                         holder.mData);
2417                 super.onViewRecycled(holder);
2418             }
2419 
2420             @Override
2421             public void onBindViewHolder(TestViewHolder2 holder, int position) {
2422                 super.onBindViewHolder(holder, position);
2423                 assertSame("on bind should be called w/ the creator adapter", this, holder.mData);
2424             }
2425 
2426             @Override
2427             public TestViewHolder2 onCreateViewHolder(ViewGroup parent,
2428                     int viewType) {
2429                 final TestViewHolder2 vh = super.onCreateViewHolder(parent, viewType);
2430                 vh.mData = this;
2431                 return vh;
2432             }
2433         };
2434     }
2435 
2436     private TestAdapter2 createBinderCheckingAdapter() {
2437         return new TestAdapter2(10) {
2438             @Override
2439             public void onViewRecycled(TestViewHolder2 holder) {
2440                 assertSame("on recycled should be called w/ the creator adapter", this,
2441                         holder.mData);
2442                 holder.mData = null;
2443                 super.onViewRecycled(holder);
2444             }
2445 
2446             @Override
2447             public void onBindViewHolder(TestViewHolder2 holder, int position) {
2448                 super.onBindViewHolder(holder, position);
2449                 holder.mData = this;
2450             }
2451         };
2452     }
2453 
2454     @Test
2455     public void testFindViewById() throws Throwable {
2456         findViewByIdTest(false);
2457         removeRecyclerView();
2458         findViewByIdTest(true);
2459     }
2460 
2461     public void findViewByIdTest(final boolean supportPredictive) throws Throwable {
2462         final RecyclerView recyclerView = new RecyclerView(getActivity());
2463         final int initialAdapterSize = 20;
2464         final TestAdapter adapter = new TestAdapter(initialAdapterSize);
2465         final int deleteStart = 6;
2466         final int deleteCount = 5;
2467         recyclerView.setAdapter(adapter);
2468         final AtomicBoolean assertPositions = new AtomicBoolean(false);
2469         TestLayoutManager lm = new TestLayoutManager() {
2470             @Override
2471             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2472                 super.onLayoutChildren(recycler, state);
2473                 if (assertPositions.get()) {
2474                     if (state.isPreLayout()) {
2475                         for (int i = 0; i < deleteStart; i++) {
2476                             View view = findViewByPosition(i);
2477                             assertNotNull("find view by position for existing items should work "
2478                                     + "fine", view);
2479                             assertFalse("view should not be marked as removed",
2480                                     ((RecyclerView.LayoutParams) view.getLayoutParams())
2481                                             .isItemRemoved());
2482                         }
2483                         for (int i = 0; i < deleteCount; i++) {
2484                             View view = findViewByPosition(i + deleteStart);
2485                             assertNotNull("find view by position should work fine for removed "
2486                                     + "views in pre-layout", view);
2487                             assertTrue("view should be marked as removed",
2488                                     ((RecyclerView.LayoutParams) view.getLayoutParams())
2489                                             .isItemRemoved());
2490                         }
2491                         for (int i = deleteStart + deleteCount; i < 20; i++) {
2492                             View view = findViewByPosition(i);
2493                             assertNotNull(view);
2494                             assertFalse("view should not be marked as removed",
2495                                     ((RecyclerView.LayoutParams) view.getLayoutParams())
2496                                             .isItemRemoved());
2497                         }
2498                     } else {
2499                         for (int i = 0; i < initialAdapterSize - deleteCount; i++) {
2500                             View view = findViewByPosition(i);
2501                             assertNotNull("find view by position for existing item " + i +
2502                                     " should work fine. child count:" + getChildCount(), view);
2503                             TestViewHolder viewHolder =
2504                                     (TestViewHolder) mRecyclerView.getChildViewHolder(view);
2505                             assertSame("should be the correct item " + viewHolder
2506                                     , viewHolder.mBoundItem,
2507                                     adapter.mItems.get(viewHolder.mPosition));
2508                             assertFalse("view should not be marked as removed",
2509                                     ((RecyclerView.LayoutParams) view.getLayoutParams())
2510                                             .isItemRemoved());
2511                         }
2512                     }
2513                 }
2514                 detachAndScrapAttachedViews(recycler);
2515                 layoutRange(recycler, state.getItemCount() - 1, -1);
2516                 layoutLatch.countDown();
2517             }
2518 
2519             @Override
2520             public boolean supportsPredictiveItemAnimations() {
2521                 return supportPredictive;
2522             }
2523         };
2524         recyclerView.setLayoutManager(lm);
2525         lm.expectLayouts(1);
2526         setRecyclerView(recyclerView);
2527         lm.waitForLayout(2);
2528         getInstrumentation().waitForIdleSync();
2529 
2530         assertPositions.set(true);
2531         lm.expectLayouts(supportPredictive ? 2 : 1);
2532         adapter.deleteAndNotify(new int[]{deleteStart, deleteCount - 1}, new int[]{deleteStart, 1});
2533         lm.waitForLayout(2);
2534     }
2535 
2536     @Test
2537     public void testTypeForCache() throws Throwable {
2538         final AtomicInteger viewType = new AtomicInteger(1);
2539         final TestAdapter adapter = new TestAdapter(100) {
2540             @Override
2541             public int getItemViewType(int position) {
2542                 return viewType.get();
2543             }
2544 
2545             @Override
2546             public long getItemId(int position) {
2547                 return mItems.get(position).mId;
2548             }
2549         };
2550         adapter.setHasStableIds(true);
2551         final AtomicInteger layoutStart = new AtomicInteger(2);
2552         final int childCount = 10;
2553         final TestLayoutManager lm = new TestLayoutManager() {
2554             @Override
2555             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2556                 super.onLayoutChildren(recycler, state);
2557                 detachAndScrapAttachedViews(recycler);
2558                 layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount);
2559                 layoutLatch.countDown();
2560             }
2561         };
2562         final RecyclerView recyclerView = new RecyclerView(getActivity());
2563         recyclerView.setItemAnimator(null);
2564         recyclerView.setAdapter(adapter);
2565         recyclerView.setLayoutManager(lm);
2566         recyclerView.setItemViewCacheSize(10);
2567         lm.expectLayouts(1);
2568         setRecyclerView(recyclerView);
2569         lm.waitForLayout(2);
2570         getInstrumentation().waitForIdleSync();
2571         layoutStart.set(4); // trigger a cache for 3,4
2572         lm.expectLayouts(1);
2573         requestLayoutOnUIThread(recyclerView);
2574         lm.waitForLayout(2);
2575         //
2576         viewType.incrementAndGet();
2577         layoutStart.set(2); // go back to bring views from cache
2578         lm.expectLayouts(1);
2579         adapter.mItems.remove(1);
2580         adapter.dispatchDataSetChanged();
2581         lm.waitForLayout(2);
2582         runTestOnUiThread(new Runnable() {
2583             @Override
2584             public void run() {
2585                 for (int i = 2; i < 4; i++) {
2586                     RecyclerView.ViewHolder vh = recyclerView.findViewHolderForLayoutPosition(i);
2587                     assertEquals("View holder's type should match latest type", viewType.get(),
2588                             vh.getItemViewType());
2589                 }
2590             }
2591         });
2592     }
2593 
2594     @Test
2595     public void testTypeForExistingViews() throws Throwable {
2596         final AtomicInteger viewType = new AtomicInteger(1);
2597         final int invalidatedCount = 2;
2598         final int layoutStart = 2;
2599         final TestAdapter adapter = new TestAdapter(100) {
2600             @Override
2601             public int getItemViewType(int position) {
2602                 return viewType.get();
2603             }
2604 
2605             @Override
2606             public void onBindViewHolder(TestViewHolder holder,
2607                     int position) {
2608                 super.onBindViewHolder(holder, position);
2609                 if (position >= layoutStart && position < invalidatedCount + layoutStart) {
2610                     try {
2611                         assertEquals("holder type should match current view type at position " +
2612                                 position, viewType.get(), holder.getItemViewType());
2613                     } catch (Throwable t) {
2614                         postExceptionToInstrumentation(t);
2615                     }
2616                 }
2617             }
2618 
2619             @Override
2620             public long getItemId(int position) {
2621                 return mItems.get(position).mId;
2622             }
2623         };
2624         adapter.setHasStableIds(true);
2625 
2626         final int childCount = 10;
2627         final TestLayoutManager lm = new TestLayoutManager() {
2628             @Override
2629             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2630                 super.onLayoutChildren(recycler, state);
2631                 detachAndScrapAttachedViews(recycler);
2632                 layoutRange(recycler, layoutStart, layoutStart + childCount);
2633                 layoutLatch.countDown();
2634             }
2635         };
2636         final RecyclerView recyclerView = new RecyclerView(getActivity());
2637         recyclerView.setAdapter(adapter);
2638         recyclerView.setLayoutManager(lm);
2639         lm.expectLayouts(1);
2640         setRecyclerView(recyclerView);
2641         lm.waitForLayout(2);
2642         getInstrumentation().waitForIdleSync();
2643         viewType.incrementAndGet();
2644         lm.expectLayouts(1);
2645         adapter.changeAndNotify(layoutStart, invalidatedCount);
2646         lm.waitForLayout(2);
2647         checkForMainThreadException();
2648     }
2649 
2650 
2651     @Test
2652     public void testState() throws Throwable {
2653         final TestAdapter adapter = new TestAdapter(10);
2654         final RecyclerView recyclerView = new RecyclerView(getActivity());
2655         recyclerView.setAdapter(adapter);
2656         recyclerView.setItemAnimator(null);
2657         final AtomicInteger itemCount = new AtomicInteger();
2658         final AtomicBoolean structureChanged = new AtomicBoolean();
2659         TestLayoutManager testLayoutManager = new TestLayoutManager() {
2660             @Override
2661             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2662                 detachAndScrapAttachedViews(recycler);
2663                 layoutRange(recycler, 0, state.getItemCount());
2664                 itemCount.set(state.getItemCount());
2665                 structureChanged.set(state.didStructureChange());
2666                 layoutLatch.countDown();
2667             }
2668         };
2669         recyclerView.setLayoutManager(testLayoutManager);
2670         testLayoutManager.expectLayouts(1);
2671         runTestOnUiThread(new Runnable() {
2672             @Override
2673             public void run() {
2674                 getActivity().mContainer.addView(recyclerView);
2675             }
2676         });
2677         testLayoutManager.waitForLayout(2, TimeUnit.SECONDS);
2678 
2679         assertEquals("item count in state should be correct", adapter.getItemCount()
2680                 , itemCount.get());
2681         assertEquals("structure changed should be true for first layout", true,
2682                 structureChanged.get());
2683         Thread.sleep(1000); //wait for other layouts.
2684         testLayoutManager.expectLayouts(1);
2685         runTestOnUiThread(new Runnable() {
2686             @Override
2687             public void run() {
2688                 recyclerView.requestLayout();
2689             }
2690         });
2691         testLayoutManager.waitForLayout(2);
2692         assertEquals("in second layout,structure changed should be false", false,
2693                 structureChanged.get());
2694         testLayoutManager.expectLayouts(1); //
2695         adapter.deleteAndNotify(3, 2);
2696         testLayoutManager.waitForLayout(2);
2697         assertEquals("when items are removed, item count in state should be updated",
2698                 adapter.getItemCount(),
2699                 itemCount.get());
2700         assertEquals("structure changed should be true when items are removed", true,
2701                 structureChanged.get());
2702         testLayoutManager.expectLayouts(1);
2703         adapter.addAndNotify(2, 5);
2704         testLayoutManager.waitForLayout(2);
2705 
2706         assertEquals("when items are added, item count in state should be updated",
2707                 adapter.getItemCount(),
2708                 itemCount.get());
2709         assertEquals("structure changed should be true when items are removed", true,
2710                 structureChanged.get());
2711     }
2712 
2713     @Test
2714     public void testDetachWithoutLayoutManager() throws Throwable {
2715         final RecyclerView recyclerView = new RecyclerView(getActivity());
2716         runTestOnUiThread(new Runnable() {
2717             @Override
2718             public void run() {
2719                 try {
2720                     setRecyclerView(recyclerView);
2721                     removeRecyclerView();
2722                 } catch (Throwable t) {
2723                     postExceptionToInstrumentation(t);
2724                 }
2725             }
2726         });
2727         checkForMainThreadException();
2728     }
2729 
2730     @Test
2731     public void testUpdateHiddenView() throws Throwable {
2732         final RecyclerView.ViewHolder[] mTargetVH = new RecyclerView.ViewHolder[1];
2733         final RecyclerView recyclerView = new RecyclerView(getActivity());
2734         final int[] preLayoutRange = new int[]{0, 10};
2735         final int[] postLayoutRange = new int[]{0, 10};
2736         final AtomicBoolean enableGetViewTest = new AtomicBoolean(false);
2737         final List<Integer> disappearingPositions = new ArrayList<Integer>();
2738         final TestLayoutManager tlm = new TestLayoutManager() {
2739             @Override
2740             public boolean supportsPredictiveItemAnimations() {
2741                 return true;
2742             }
2743 
2744             @Override
2745             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2746                 try {
2747                     final int[] layoutRange = state.isPreLayout() ? preLayoutRange
2748                             : postLayoutRange;
2749                     detachAndScrapAttachedViews(recycler);
2750                     layoutRange(recycler, layoutRange[0], layoutRange[1]);
2751                     if (!state.isPreLayout()) {
2752                         for (Integer position : disappearingPositions) {
2753                             // test sanity.
2754                             assertNull(findViewByPosition(position));
2755                             final View view = recycler.getViewForPosition(position);
2756                             addDisappearingView(view);
2757                             measureChildWithMargins(view, 0, 0);
2758                             // position item out of bounds.
2759                             view.layout(0, -500, view.getMeasuredWidth(),
2760                                     -500 + view.getMeasuredHeight());
2761                         }
2762                     }
2763                 } catch (Throwable t) {
2764                     postExceptionToInstrumentation(t);
2765                 }
2766                 layoutLatch.countDown();
2767             }
2768         };
2769 
2770         recyclerView.getItemAnimator().setMoveDuration(2000);
2771         recyclerView.getItemAnimator().setRemoveDuration(2000);
2772         final TestAdapter adapter = new TestAdapter(100);
2773         recyclerView.setAdapter(adapter);
2774         recyclerView.setLayoutManager(tlm);
2775         tlm.expectLayouts(1);
2776         setRecyclerView(recyclerView);
2777 
2778         tlm.waitForLayout(1);
2779         checkForMainThreadException();
2780         mTargetVH[0] = recyclerView.findViewHolderForAdapterPosition(0);
2781         // now, a child disappears
2782         disappearingPositions.add(0);
2783         // layout one shifted
2784         postLayoutRange[0] = 1;
2785         postLayoutRange[1] = 11;
2786         tlm.expectLayouts(2);
2787         adapter.addAndNotify(8, 1);
2788         tlm.waitForLayout(2);
2789         checkForMainThreadException();
2790 
2791         tlm.expectLayouts(2);
2792         disappearingPositions.clear();
2793         // now that item should be moving, invalidate it and delete it.
2794         enableGetViewTest.set(true);
2795         runTestOnUiThread(new Runnable() {
2796             @Override
2797             public void run() {
2798                 try {
2799                     adapter.changeAndNotify(0, 1);
2800                     adapter.deleteAndNotify(0, 1);
2801                 } catch (Throwable throwable) {
2802                     throwable.printStackTrace();
2803                 }
2804             }
2805         });
2806         tlm.waitForLayout(2);
2807         checkForMainThreadException();
2808     }
2809 
2810     @Test
2811     public void testFocusBigViewOnTop() throws Throwable {
2812         focusTooBigViewTest(Gravity.TOP);
2813     }
2814 
2815     @Test
2816     public void testFocusBigViewOnLeft() throws Throwable {
2817         focusTooBigViewTest(Gravity.LEFT);
2818     }
2819 
2820     @Test
2821     public void testFocusBigViewOnRight() throws Throwable {
2822         focusTooBigViewTest(Gravity.RIGHT);
2823     }
2824 
2825     @Test
2826     public void testFocusBigViewOnBottom() throws Throwable {
2827         focusTooBigViewTest(Gravity.BOTTOM);
2828     }
2829 
2830     @Test
2831     public void testFocusBigViewOnLeftRTL() throws Throwable {
2832         focusTooBigViewTest(Gravity.LEFT, true);
2833         assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL,
2834                 mRecyclerView.getLayoutManager().getLayoutDirection());
2835     }
2836 
2837     @Test
2838     public void testFocusBigViewOnRightRTL() throws Throwable {
2839         focusTooBigViewTest(Gravity.RIGHT, true);
2840         assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL,
2841                 mRecyclerView.getLayoutManager().getLayoutDirection());
2842     }
2843 
2844     public void focusTooBigViewTest(final int gravity) throws Throwable {
2845         focusTooBigViewTest(gravity, false);
2846     }
2847 
2848     public void focusTooBigViewTest(final int gravity, final boolean rtl) throws Throwable {
2849         RecyclerView rv = new RecyclerView(getActivity());
2850         if (rtl) {
2851             ViewCompat.setLayoutDirection(rv, ViewCompat.LAYOUT_DIRECTION_RTL);
2852         }
2853         final AtomicInteger vScrollDist = new AtomicInteger(0);
2854         final AtomicInteger hScrollDist = new AtomicInteger(0);
2855         final AtomicInteger vDesiredDist = new AtomicInteger(0);
2856         final AtomicInteger hDesiredDist = new AtomicInteger(0);
2857         TestLayoutManager tlm = new TestLayoutManager() {
2858 
2859             @Override
2860             public int getLayoutDirection() {
2861                 return rtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR;
2862             }
2863 
2864             @Override
2865             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2866                 detachAndScrapAttachedViews(recycler);
2867                 final View view = recycler.getViewForPosition(0);
2868                 addView(view);
2869                 int left = 0, top = 0;
2870                 view.setBackgroundColor(Color.rgb(0, 0, 255));
2871                 switch (gravity) {
2872                     case Gravity.LEFT:
2873                     case Gravity.RIGHT:
2874                         view.measure(
2875                                 View.MeasureSpec.makeMeasureSpec((int) (getWidth() * 1.5),
2876                                         View.MeasureSpec.EXACTLY),
2877                                 View.MeasureSpec.makeMeasureSpec((int) (getHeight() * .9),
2878                                         View.MeasureSpec.AT_MOST));
2879                         left = gravity == Gravity.LEFT ? getWidth() - view.getMeasuredWidth() - 80
2880                                 : 90;
2881                         top = 0;
2882                         if (ViewCompat.LAYOUT_DIRECTION_RTL == getLayoutDirection()) {
2883                             hDesiredDist.set((left + view.getMeasuredWidth()) - getWidth());
2884                         } else {
2885                             hDesiredDist.set(left);
2886                         }
2887                         break;
2888                     case Gravity.TOP:
2889                     case Gravity.BOTTOM:
2890                         view.measure(
2891                                 View.MeasureSpec.makeMeasureSpec((int) (getWidth() * .9),
2892                                         View.MeasureSpec.AT_MOST),
2893                                 View.MeasureSpec.makeMeasureSpec((int) (getHeight() * 1.5),
2894                                         View.MeasureSpec.EXACTLY));
2895                         top = gravity == Gravity.TOP ? getHeight() - view.getMeasuredHeight() -
2896                                 80 : 90;
2897                         left = 0;
2898                         vDesiredDist.set(top);
2899                         break;
2900                 }
2901 
2902                 view.layout(left, top, left + view.getMeasuredWidth(),
2903                         top + view.getMeasuredHeight());
2904                 layoutLatch.countDown();
2905             }
2906 
2907             @Override
2908             public boolean canScrollVertically() {
2909                 return true;
2910             }
2911 
2912             @Override
2913             public boolean canScrollHorizontally() {
2914                 return super.canScrollHorizontally();
2915             }
2916 
2917             @Override
2918             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
2919                     RecyclerView.State state) {
2920                 vScrollDist.addAndGet(dy);
2921                 getChildAt(0).offsetTopAndBottom(-dy);
2922                 return dy;
2923             }
2924 
2925             @Override
2926             public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
2927                     RecyclerView.State state) {
2928                 hScrollDist.addAndGet(dx);
2929                 getChildAt(0).offsetLeftAndRight(-dx);
2930                 return dx;
2931             }
2932         };
2933         TestAdapter adapter = new TestAdapter(10);
2934         rv.setAdapter(adapter);
2935         rv.setLayoutManager(tlm);
2936         tlm.expectLayouts(1);
2937         setRecyclerView(rv);
2938         tlm.waitForLayout(2);
2939         View view = rv.getChildAt(0);
2940         requestFocus(view);
2941         Thread.sleep(1000);
2942         assertEquals(vDesiredDist.get(), vScrollDist.get());
2943         assertEquals(hDesiredDist.get(), hScrollDist.get());
2944         assertEquals(mRecyclerView.getPaddingTop(), view.getTop());
2945         if (rtl) {
2946             assertEquals(mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(),
2947                     view.getRight());
2948         } else {
2949             assertEquals(mRecyclerView.getPaddingLeft(), view.getLeft());
2950         }
2951     }
2952 
2953     @Test
2954     public void testFocusRectOnScreenWithDecorOffsets() throws Throwable {
2955         focusRectOnScreenTest(true);
2956     }
2957 
2958     @Test
2959     public void testFocusRectOnScreenWithout() throws Throwable {
2960         focusRectOnScreenTest(false);
2961     }
2962 
2963 
2964     public void focusRectOnScreenTest(boolean addItemDecors) throws Throwable {
2965         RecyclerView rv = new RecyclerView(getActivity());
2966         final AtomicInteger scrollDist = new AtomicInteger(0);
2967         TestLayoutManager tlm = new TestLayoutManager() {
2968             @Override
2969             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
2970                 detachAndScrapAttachedViews(recycler);
2971                 final View view = recycler.getViewForPosition(0);
2972                 addView(view);
2973                 measureChildWithMargins(view, 0, 0);
2974                 view.layout(0, -20, view.getWidth(),
2975                         -20 + view.getHeight());// ignore decors on purpose
2976                 layoutLatch.countDown();
2977             }
2978 
2979             @Override
2980             public boolean canScrollVertically() {
2981                 return true;
2982             }
2983 
2984             @Override
2985             public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
2986                     RecyclerView.State state) {
2987                 scrollDist.addAndGet(dy);
2988                 return dy;
2989             }
2990         };
2991         TestAdapter adapter = new TestAdapter(10);
2992         if (addItemDecors) {
2993             rv.addItemDecoration(new RecyclerView.ItemDecoration() {
2994                 @Override
2995                 public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
2996                         RecyclerView.State state) {
2997                     outRect.set(0, 10, 0, 10);
2998                 }
2999             });
3000         }
3001         rv.setAdapter(adapter);
3002         rv.setLayoutManager(tlm);
3003         tlm.expectLayouts(1);
3004         setRecyclerView(rv);
3005         tlm.waitForLayout(2);
3006 
3007         View view = rv.getChildAt(0);
3008         requestFocus(view);
3009         Thread.sleep(1000);
3010         assertEquals(addItemDecors ? -30 : -20, scrollDist.get());
3011     }
3012 
3013     @Test
3014     public void testUnimplementedSmoothScroll() throws Throwable {
3015         final AtomicInteger receivedScrollToPosition = new AtomicInteger(-1);
3016         final AtomicInteger receivedSmoothScrollToPosition = new AtomicInteger(-1);
3017         final CountDownLatch cbLatch = new CountDownLatch(2);
3018         TestLayoutManager tlm = new TestLayoutManager() {
3019             @Override
3020             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3021                 detachAndScrapAttachedViews(recycler);
3022                 layoutRange(recycler, 0, 10);
3023                 layoutLatch.countDown();
3024             }
3025 
3026             @Override
3027             public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
3028                     int position) {
3029                 assertEquals(-1, receivedSmoothScrollToPosition.get());
3030                 receivedSmoothScrollToPosition.set(position);
3031                 RecyclerView.SmoothScroller ss =
3032                         new LinearSmoothScroller(recyclerView.getContext()) {
3033                             @Override
3034                             public PointF computeScrollVectorForPosition(int targetPosition) {
3035                                 return null;
3036                             }
3037                         };
3038                 ss.setTargetPosition(position);
3039                 startSmoothScroll(ss);
3040                 cbLatch.countDown();
3041             }
3042 
3043             @Override
3044             public void scrollToPosition(int position) {
3045                 assertEquals(-1, receivedScrollToPosition.get());
3046                 receivedScrollToPosition.set(position);
3047                 cbLatch.countDown();
3048             }
3049         };
3050         RecyclerView rv = new RecyclerView(getActivity());
3051         rv.setAdapter(new TestAdapter(100));
3052         rv.setLayoutManager(tlm);
3053         tlm.expectLayouts(1);
3054         setRecyclerView(rv);
3055         tlm.waitForLayout(2);
3056         freezeLayout(true);
3057         smoothScrollToPosition(35);
3058         assertEquals("smoothScrollToPosition should be ignored when frozen",
3059                 -1, receivedSmoothScrollToPosition.get());
3060         freezeLayout(false);
3061         smoothScrollToPosition(35);
3062         assertTrue("both scrolls should be called", cbLatch.await(3, TimeUnit.SECONDS));
3063         checkForMainThreadException();
3064         assertEquals(35, receivedSmoothScrollToPosition.get());
3065         assertEquals(35, receivedScrollToPosition.get());
3066     }
3067 
3068     @Test
3069     public void testJumpingJackSmoothScroller() throws Throwable {
3070         jumpingJackSmoothScrollerTest(true);
3071     }
3072 
3073     @Test
3074     public void testJumpingJackSmoothScrollerGoesIdle() throws Throwable {
3075         jumpingJackSmoothScrollerTest(false);
3076     }
3077 
3078     private void jumpingJackSmoothScrollerTest(final boolean succeed) throws Throwable {
3079         final List<Integer> receivedScrollToPositions = new ArrayList<>();
3080         final TestAdapter testAdapter = new TestAdapter(200);
3081         final AtomicBoolean mTargetFound = new AtomicBoolean(false);
3082         TestLayoutManager tlm = new TestLayoutManager() {
3083             int pendingScrollPosition = -1;
3084             @Override
3085             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
3086                 detachAndScrapAttachedViews(recycler);
3087                 final int pos = pendingScrollPosition < 0 ? 0: pendingScrollPosition;
3088                 layoutRange(recycler, pos, pos + 10);
3089                 if (layoutLatch != null) {
3090                     layoutLatch.countDown();
3091                 }
3092             }
3093 
3094             @Override
3095             public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
3096                     final int position) {
3097                 RecyclerView.SmoothScroller ss =
3098                         new LinearSmoothScroller(recyclerView.getContext()) {
3099                             @Override
3100                             public PointF computeScrollVectorForPosition(int targetPosition) {
3101                                 return new PointF(0, 1);
3102                             }
3103 
3104                             @Override
3105                             protected void onTargetFound(View targetView, RecyclerView.State state,
3106                                                          Action action) {
3107                                 super.onTargetFound(targetView, state, action);
3108                                 mTargetFound.set(true);
3109                             }
3110 
3111                             @Override
3112                             protected void updateActionForInterimTarget(Action action) {
3113                                 int limit = succeed ? getTargetPosition() : 100;
3114                                 if (pendingScrollPosition + 2 < limit) {
3115                                     if (pendingScrollPosition != NO_POSITION) {
3116                                         assertEquals(pendingScrollPosition,
3117                                                 getChildViewHolderInt(getChildAt(0))
3118                                                         .getAdapterPosition());
3119                                     }
3120                                     action.jumpTo(pendingScrollPosition + 2);
3121                                 }
3122                             }
3123                         };
3124                 ss.setTargetPosition(position);
3125                 startSmoothScroll(ss);
3126             }
3127 
3128             @Override
3129             public void scrollToPosition(int position) {
3130                 receivedScrollToPositions.add(position);
3131                 pendingScrollPosition = position;
3132                 requestLayout();
3133             }
3134         };
3135         final RecyclerView rv = new RecyclerView(getActivity());
3136         rv.setAdapter(testAdapter);
3137         rv.setLayoutManager(tlm);
3138 
3139         tlm.expectLayouts(1);
3140         setRecyclerView(rv);
3141         tlm.waitForLayout(2);
3142 
3143         runTestOnUiThread(new Runnable() {
3144             @Override
3145             public void run() {
3146                 rv.smoothScrollToPosition(150);
3147             }
3148         });
3149         int limit = 100;
3150         while (rv.getLayoutManager().isSmoothScrolling() && --limit > 0) {
3151             Thread.sleep(200);
3152             checkForMainThreadException();
3153         }
3154         checkForMainThreadException();
3155         assertTrue(limit > 0);
3156         for (int i = 1; i < 100; i+=2) {
3157             assertTrue("scroll positions must include " + i, receivedScrollToPositions.contains(i));
3158         }
3159 
3160         assertEquals(succeed, mTargetFound.get());
3161 
3162     }
3163 
3164     private static class TestViewHolder2 extends RecyclerView.ViewHolder {
3165 
3166         Object mData;
3167 
3168         public TestViewHolder2(View itemView) {
3169             super(itemView);
3170         }
3171     }
3172 
3173     private static class TestAdapter2 extends RecyclerView.Adapter<TestViewHolder2> {
3174 
3175         List<Item> mItems;
3176 
3177         private TestAdapter2(int count) {
3178             mItems = new ArrayList<Item>(count);
3179             for (int i = 0; i < count; i++) {
3180                 mItems.add(new Item(i, "Item " + i));
3181             }
3182         }
3183 
3184         @Override
3185         public TestViewHolder2 onCreateViewHolder(ViewGroup parent,
3186                 int viewType) {
3187             return new TestViewHolder2(new TextView(parent.getContext()));
3188         }
3189 
3190         @Override
3191         public void onBindViewHolder(TestViewHolder2 holder, int position) {
3192             final Item item = mItems.get(position);
3193             ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")");
3194         }
3195 
3196         @Override
3197         public int getItemCount() {
3198             return mItems.size();
3199         }
3200     }
3201 
3202     private static interface AdapterRunnable {
3203 
3204         public void run(TestAdapter adapter) throws Throwable;
3205     }
3206 
3207 }
3208