• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 package android.support.v7.widget;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotEquals;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertSame;
24 import static org.junit.Assert.assertTrue;
25 import static org.mockito.Matchers.any;
26 import static org.mockito.Matchers.anyInt;
27 import static org.mockito.Matchers.argThat;
28 import static org.mockito.Mockito.mock;
29 import static org.mockito.Mockito.never;
30 import static org.mockito.Mockito.times;
31 import static org.mockito.Mockito.verify;
32 import static org.mockito.Mockito.when;
33 
34 import android.content.Context;
35 import android.os.Build;
36 import android.os.Parcelable;
37 import android.os.SystemClock;
38 import android.support.test.InstrumentationRegistry;
39 import android.support.test.filters.SdkSuppress;
40 import android.support.test.filters.SmallTest;
41 import android.support.test.runner.AndroidJUnit4;
42 import android.view.MotionEvent;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.widget.FrameLayout;
46 
47 import org.hamcrest.BaseMatcher;
48 import org.hamcrest.Description;
49 import org.junit.After;
50 import org.junit.Before;
51 import org.junit.Test;
52 import org.junit.runner.RunWith;
53 import org.mockito.invocation.InvocationOnMock;
54 import org.mockito.stubbing.Answer;
55 
56 import java.util.ArrayList;
57 import java.util.List;
58 import java.util.concurrent.TimeUnit;
59 
60 @SmallTest
61 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
62 @RunWith(AndroidJUnit4.class)
63 public class RecyclerViewCacheTest {
64     TimeMockingRecyclerView mRecyclerView;
65     RecyclerView.Recycler mRecycler;
66 
67     private class TimeMockingRecyclerView extends RecyclerView {
68         private long mMockNanoTime = 0;
69 
TimeMockingRecyclerView(Context context)70         TimeMockingRecyclerView(Context context) {
71             super(context);
72         }
73 
registerTimePassingMs(long ms)74         public void registerTimePassingMs(long ms) {
75             mMockNanoTime += TimeUnit.MILLISECONDS.toNanos(ms);
76         }
77 
78         @Override
getNanoTime()79         long getNanoTime() {
80             return mMockNanoTime;
81         }
82 
83         @Override
getWindowVisibility()84         public int getWindowVisibility() {
85             // Pretend to be visible to avoid being filtered out
86             return View.VISIBLE;
87         }
88     }
89 
90     @Before
setup()91     public void setup() throws Exception {
92         mRecyclerView = new TimeMockingRecyclerView(getContext());
93         mRecyclerView.onAttachedToWindow();
94         mRecycler = mRecyclerView.mRecycler;
95     }
96 
97     @After
teardown()98     public void teardown() throws Exception {
99         if (mRecyclerView.isAttachedToWindow()) {
100             mRecyclerView.onDetachedFromWindow();
101         }
102         GapWorker gapWorker = GapWorker.sGapWorker.get();
103         if (gapWorker != null) {
104             assertTrue(gapWorker.mRecyclerViews.isEmpty());
105         }
106     }
107 
getContext()108     private Context getContext() {
109         return InstrumentationRegistry.getContext();
110     }
111 
layout(int width, int height)112     private void layout(int width, int height) {
113         mRecyclerView.measure(
114                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
115                 View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
116         mRecyclerView.layout(0, 0, width, height);
117     }
118 
119     @Test
prefetchReusesCacheItems()120     public void prefetchReusesCacheItems() {
121         RecyclerView.LayoutManager prefetchingLayoutManager = new RecyclerView.LayoutManager() {
122             @Override
123             public RecyclerView.LayoutParams generateDefaultLayoutParams() {
124                 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
125                         ViewGroup.LayoutParams.WRAP_CONTENT);
126             }
127 
128             @Override
129             public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
130                     LayoutPrefetchRegistry prefetchManager) {
131                 prefetchManager.addPosition(0, 0);
132                 prefetchManager.addPosition(1, 0);
133                 prefetchManager.addPosition(2, 0);
134             }
135 
136             @Override
137             public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
138             }
139         };
140         mRecyclerView.setLayoutManager(prefetchingLayoutManager);
141 
142         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
143         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
144                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
145                     @Override
146                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
147                             throws Throwable {
148                         return new RecyclerView.ViewHolder(new View(getContext())) {};
149                     }
150                 });
151         when(mockAdapter.getItemCount()).thenReturn(10);
152         mRecyclerView.setAdapter(mockAdapter);
153 
154         layout(320, 320);
155 
156         verify(mockAdapter, never()).onCreateViewHolder(any(ViewGroup.class), anyInt());
157         verify(mockAdapter, never()).onBindViewHolder(
158                 any(RecyclerView.ViewHolder.class), anyInt(), any(List.class));
159         assertTrue(mRecycler.mCachedViews.isEmpty());
160 
161         // Prefetch multiple times...
162         for (int i = 0; i < 4; i++) {
163             mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
164 
165             // ...but should only see the same three items fetched/bound once each
166             verify(mockAdapter, times(3)).onCreateViewHolder(any(ViewGroup.class), anyInt());
167             verify(mockAdapter, times(3)).onBindViewHolder(
168                     any(RecyclerView.ViewHolder.class), anyInt(), any(List.class));
169 
170             assertTrue(mRecycler.mCachedViews.size() == 3);
171             CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 0, 1, 2);
172         }
173     }
174 
175     @Test
prefetchItemsNotEvictedWithInserts()176     public void prefetchItemsNotEvictedWithInserts() {
177         mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));
178 
179         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
180         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
181                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
182                     @Override
183                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
184                             throws Throwable {
185                         View view = new View(getContext());
186                         view.setMinimumWidth(100);
187                         view.setMinimumHeight(100);
188                         return new RecyclerView.ViewHolder(view) {};
189                     }
190                 });
191         when(mockAdapter.getItemCount()).thenReturn(100);
192         mRecyclerView.setAdapter(mockAdapter);
193 
194         layout(300, 100);
195 
196         assertEquals(2, mRecyclerView.mRecycler.mViewCacheMax);
197         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
198         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
199         assertEquals(5, mRecyclerView.mRecycler.mViewCacheMax);
200 
201         CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 3, 4, 5);
202 
203         // further views recycled, as though from scrolling, shouldn't evict prefetched views:
204         mRecycler.recycleView(mRecycler.getViewForPosition(10));
205         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 10);
206 
207         mRecycler.recycleView(mRecycler.getViewForPosition(20));
208         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 10, 20);
209 
210         mRecycler.recycleView(mRecycler.getViewForPosition(30));
211         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 20, 30);
212 
213         mRecycler.recycleView(mRecycler.getViewForPosition(40));
214         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 3, 4, 5, 30, 40);
215 
216         // After clearing the cache, the prefetch priorities should be cleared as well:
217         mRecyclerView.mRecycler.recycleAndClearCachedViews();
218         for (int i : new int[] {3, 4, 5, 50, 60, 70, 80, 90}) {
219             mRecycler.recycleView(mRecycler.getViewForPosition(i));
220         }
221 
222         // cache only contains most recent positions, no priority for previous prefetches:
223         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 50, 60, 70, 80, 90);
224     }
225 
226     @Test
prefetchItemsNotEvictedOnScroll()227     public void prefetchItemsNotEvictedOnScroll() {
228         mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));
229 
230         // 100x100 pixel views
231         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
232         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
233                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
234                     @Override
235                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
236                             throws Throwable {
237                         View view = new View(getContext());
238                         view.setMinimumWidth(100);
239                         view.setMinimumHeight(100);
240                         return new RecyclerView.ViewHolder(view) {};
241                     }
242                 });
243         when(mockAdapter.getItemCount()).thenReturn(100);
244         mRecyclerView.setAdapter(mockAdapter);
245 
246         // NOTE: requested cache size must be smaller than span count so two rows cannot fit
247         mRecyclerView.setItemViewCacheSize(2);
248 
249         layout(300, 150);
250         mRecyclerView.scrollBy(0, 75);
251         assertTrue(mRecycler.mCachedViews.isEmpty());
252 
253         // rows 0, 1, and 2 are all attached and visible. Prefetch row 3:
254         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
255         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
256 
257         // row 3 is cached:
258         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 9, 10, 11);
259         assertTrue(mRecycler.mCachedViews.size() == 3);
260 
261         // Scroll so 1 falls off (though 3 is still not on screen)
262         mRecyclerView.scrollBy(0, 50);
263 
264         // row 3 is still cached, with a couple other recycled views:
265         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 9, 10, 11);
266         assertTrue(mRecycler.mCachedViews.size() == 5);
267     }
268 
269     @Test
prefetchIsComputingLayout()270     public void prefetchIsComputingLayout() {
271         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
272 
273         // 100x100 pixel views
274         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
275         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
276                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
277                     @Override
278                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
279                             throws Throwable {
280                         View view = new View(getContext());
281                         view.setMinimumWidth(100);
282                         view.setMinimumHeight(100);
283                         assertTrue(mRecyclerView.isComputingLayout());
284                         return new RecyclerView.ViewHolder(view) {};
285                     }
286                 });
287         when(mockAdapter.getItemCount()).thenReturn(100);
288         mRecyclerView.setAdapter(mockAdapter);
289 
290         layout(100, 100);
291 
292         verify(mockAdapter, times(1)).onCreateViewHolder(mRecyclerView, 0);
293 
294         // prefetch an item, should still observe isComputingLayout in that create
295         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
296         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
297 
298         verify(mockAdapter, times(2)).onCreateViewHolder(mRecyclerView, 0);
299     }
300 
301     @Test
prefetchDrag()302     public void prefetchDrag() {
303         // event dispatch requires a parent
304         ViewGroup parent = new FrameLayout(getContext());
305         parent.addView(mRecyclerView);
306 
307 
308         mRecyclerView.setLayoutManager(
309                 new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
310 
311         // 1000x1000 pixel views
312         RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
313             @Override
314             public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
315                 mRecyclerView.registerTimePassingMs(5);
316                 View view = new View(getContext());
317                 view.setMinimumWidth(1000);
318                 view.setMinimumHeight(1000);
319                 return new RecyclerView.ViewHolder(view) {};
320             }
321 
322             @Override
323             public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
324                 mRecyclerView.registerTimePassingMs(5);
325             }
326 
327             @Override
328             public int getItemCount() {
329                 return 100;
330             }
331         };
332         mRecyclerView.setAdapter(adapter);
333 
334         layout(1000, 1000);
335 
336         long time = SystemClock.uptimeMillis();
337         mRecyclerView.onTouchEvent(
338                 MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 500, 1000, 0));
339 
340         assertEquals(0, mRecyclerView.mPrefetchRegistry.mPrefetchDx);
341         assertEquals(0, mRecyclerView.mPrefetchRegistry.mPrefetchDy);
342 
343         // Consume slop
344         mRecyclerView.onTouchEvent(
345                 MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 50, 500, 0));
346 
347         // move by 0,30
348         mRecyclerView.onTouchEvent(
349                 MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 50, 470, 0));
350         assertEquals(0, mRecyclerView.mPrefetchRegistry.mPrefetchDx);
351         assertEquals(30, mRecyclerView.mPrefetchRegistry.mPrefetchDy);
352 
353         // move by 10,15
354         mRecyclerView.onTouchEvent(
355                 MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 40, 455, 0));
356         assertEquals(10, mRecyclerView.mPrefetchRegistry.mPrefetchDx);
357         assertEquals(15, mRecyclerView.mPrefetchRegistry.mPrefetchDy);
358 
359         // move by 0,0 - IGNORED
360         mRecyclerView.onTouchEvent(
361                 MotionEvent.obtain(time, time, MotionEvent.ACTION_MOVE, 40, 455, 0));
362         assertEquals(10, mRecyclerView.mPrefetchRegistry.mPrefetchDx); // same as prev
363         assertEquals(15, mRecyclerView.mPrefetchRegistry.mPrefetchDy); // same as prev
364     }
365 
366     @Test
prefetchItemsRespectDeadline()367     public void prefetchItemsRespectDeadline() {
368         mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));
369 
370         // 100x100 pixel views
371         RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
372             @Override
373             public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
374                 mRecyclerView.registerTimePassingMs(5);
375                 View view = new View(getContext());
376                 view.setMinimumWidth(100);
377                 view.setMinimumHeight(100);
378                 return new RecyclerView.ViewHolder(view) {};
379             }
380 
381             @Override
382             public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
383                 mRecyclerView.registerTimePassingMs(5);
384             }
385 
386             @Override
387             public int getItemCount() {
388                 return 100;
389             }
390         };
391         mRecyclerView.setAdapter(adapter);
392 
393         layout(300, 300);
394 
395         // offset scroll so that no prefetch-able views are directly adjacent to viewport
396         mRecyclerView.scrollBy(0, 50);
397 
398         assertTrue(mRecycler.mCachedViews.size() == 0);
399         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);
400 
401         // Should take 15 ms to inflate, bind, inflate, so give 19 to be safe
402         final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(19);
403 
404         // Timed prefetch
405         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
406         mRecyclerView.mGapWorker.prefetch(deadlineNs);
407 
408         // will have enough time to inflate/bind one view, and inflate another
409         assertTrue(mRecycler.mCachedViews.size() == 1);
410         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 1);
411         // Note: order/view below is an implementation detail
412         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 12);
413 
414 
415         // Unbounded prefetch this time
416         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
417 
418         // Should finish all work
419         assertTrue(mRecycler.mCachedViews.size() == 3);
420         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);
421         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 12, 13, 14);
422     }
423 
424     @Test
partialPrefetchAvoidsViewRecycledCallback()425     public void partialPrefetchAvoidsViewRecycledCallback() {
426         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
427 
428         // 100x100 pixel views
429         RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
430             @Override
431             public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
432                 mRecyclerView.registerTimePassingMs(5);
433                 View view = new View(getContext());
434                 view.setMinimumWidth(100);
435                 view.setMinimumHeight(100);
436                 return new RecyclerView.ViewHolder(view) {};
437             }
438 
439             @Override
440             public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
441                 mRecyclerView.registerTimePassingMs(5);
442             }
443 
444             @Override
445             public int getItemCount() {
446                 return 100;
447             }
448 
449             @Override
450             public void onViewRecycled(RecyclerView.ViewHolder holder) {
451                 // verify unbound view doesn't get
452                 assertNotEquals(RecyclerView.NO_POSITION, holder.getAdapterPosition());
453             }
454         };
455         mRecyclerView.setAdapter(adapter);
456 
457         layout(100, 300);
458 
459         // offset scroll so that no prefetch-able views are directly adjacent to viewport
460         mRecyclerView.scrollBy(0, 50);
461 
462         assertTrue(mRecycler.mCachedViews.size() == 0);
463         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);
464 
465         // Should take 10 ms to inflate + bind, so just give it 9 so it doesn't have time to bind
466         final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(9);
467 
468         // Timed prefetch
469         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
470         mRecyclerView.mGapWorker.prefetch(deadlineNs);
471 
472         // will have enough time to inflate but not bind one view
473         assertTrue(mRecycler.mCachedViews.size() == 0);
474         assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 1);
475         RecyclerView.ViewHolder pooledHolder = mRecyclerView.getRecycledViewPool()
476                 .mScrap.get(0).mScrapHeap.get(0);
477         assertEquals(RecyclerView.NO_POSITION, pooledHolder.getAdapterPosition());
478     }
479 
480     @Test
prefetchStaggeredItemsPriority()481     public void prefetchStaggeredItemsPriority() {
482         StaggeredGridLayoutManager sglm =
483                 new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
484         mRecyclerView.setLayoutManager(sglm);
485 
486         // first view 50x100 pixels, rest are 100x100 so second column is offset
487         mRecyclerView.setAdapter(new RecyclerView.Adapter() {
488             @Override
489             public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
490                 return new RecyclerView.ViewHolder(new View(getContext())) {};
491             }
492 
493             @Override
494             public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
495                 holder.itemView.setMinimumWidth(100);
496                 holder.itemView.setMinimumHeight(position == 0 ? 50 : 100);
497             }
498 
499             @Override
500             public int getItemCount() {
501                 return 100;
502             }
503         });
504 
505         layout(200, 200);
506 
507         /* Each row is 50 pixels:
508          * ------------- *
509          *   0   |   1   *
510          *   2   |   1   *
511          *   2   |   3   *
512          *___4___|___3___*
513          *   4   |   5   *
514          *   6   |   5   *
515          *      ...      *
516          */
517         assertEquals(5, mRecyclerView.getChildCount());
518         assertEquals(0, sglm.getFirstChildPosition());
519         assertEquals(4, sglm.getLastChildPosition());
520 
521         // prefetching down shows 5 at 0 pixels away, 6 at 50 pixels away
522         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10,
523                 new Integer[] {5, 0}, new Integer[] {6, 50});
524 
525         // Prefetch upward shows nothing
526         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10);
527 
528         mRecyclerView.scrollBy(0, 100);
529 
530         /* Each row is 50 pixels:
531          * ------------- *
532          *   0   |   1   *
533          *___2___|___1___*
534          *   2   |   3   *
535          *   4   |   3   *
536          *   4   |   5   *
537          *___6___|___5___*
538          *   6   |   7   *
539          *   8   |   7   *
540          *      ...      *
541          */
542 
543         assertEquals(5, mRecyclerView.getChildCount());
544         assertEquals(2, sglm.getFirstChildPosition());
545         assertEquals(6, sglm.getLastChildPosition());
546 
547         // prefetching down shows 7 at 0 pixels away, 8 at 50 pixels away
548         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10,
549                 new Integer[] {7, 0}, new Integer[] {8, 50});
550 
551         // prefetching up shows 1 is 0 pixels away, 0 at 50 pixels away
552         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10,
553                 new Integer[] {1, 0}, new Integer[] {0, 50});
554     }
555 
556     @Test
prefetchStaggeredPastBoundary()557     public void prefetchStaggeredPastBoundary() {
558         StaggeredGridLayoutManager sglm =
559                 new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
560         mRecyclerView.setLayoutManager(sglm);
561         mRecyclerView.setAdapter(new RecyclerView.Adapter() {
562             @Override
563             public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
564                 return new RecyclerView.ViewHolder(new View(getContext())) {};
565             }
566 
567             @Override
568             public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
569                 holder.itemView.setMinimumWidth(100);
570                 holder.itemView.setMinimumHeight(position == 0 ? 100 : 200);
571             }
572 
573             @Override
574             public int getItemCount() {
575                 return 2;
576             }
577         });
578 
579         layout(200, 100);
580         mRecyclerView.scrollBy(0, 50);
581 
582         /* Each row is 50 pixels:
583          * ------------- *
584          *___0___|___1___*
585          *   0   |   1   *
586          *_______|___1___*
587          *       |   1   *
588          */
589         assertEquals(2, mRecyclerView.getChildCount());
590         assertEquals(0, sglm.getFirstChildPosition());
591         assertEquals(1, sglm.getLastChildPosition());
592 
593         // prefetch upward gets nothing
594         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10);
595 
596         // prefetch downward gets nothing (and doesn't crash...)
597         CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10);
598     }
599 
600     @Test
prefetchItemsSkipAnimations()601     public void prefetchItemsSkipAnimations() {
602         LinearLayoutManager llm = new LinearLayoutManager(getContext());
603         mRecyclerView.setLayoutManager(llm);
604         final int[] expandedPosition = new int[] {-1};
605 
606         final RecyclerView.Adapter adapter = new RecyclerView.Adapter() {
607             @Override
608             public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
609                 return new RecyclerView.ViewHolder(new View(parent.getContext())) {};
610             }
611 
612             @Override
613             public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
614                 int height = expandedPosition[0] == position ? 400 : 100;
615                 holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(200, height));
616             }
617 
618             @Override
619             public int getItemCount() {
620                 return 10;
621             }
622         };
623 
624         // make move duration long enough to be able to see the effects
625         RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
626         itemAnimator.setMoveDuration(10000);
627         mRecyclerView.setAdapter(adapter);
628 
629         layout(200, 400);
630 
631         expandedPosition[0] = 1;
632         // insert payload to avoid creating a new view
633         adapter.notifyItemChanged(1, new Object());
634 
635         layout(200, 400);
636         layout(200, 400);
637 
638         assertTrue(itemAnimator.isRunning());
639         assertEquals(2, llm.getChildCount());
640         assertEquals(4, mRecyclerView.getChildCount());
641 
642         // animating view should be observable as hidden, uncached...
643         CacheUtils.verifyCacheDoesNotContainPositions(mRecyclerView, 2);
644         assertNotNull("Animating view should be found, hidden",
645                 mRecyclerView.mChildHelper.findHiddenNonRemovedView(2));
646         assertTrue(GapWorker.isPrefetchPositionAttached(mRecyclerView, 2));
647 
648         // ...but must not be removed for prefetch
649         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
650         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
651 
652         assertEquals("Prefetch must target one view", 1, mRecyclerView.mPrefetchRegistry.mCount);
653         int prefetchTarget = mRecyclerView.mPrefetchRegistry.mPrefetchArray[0];
654         assertEquals("Prefetch must target view 2", 2, prefetchTarget);
655 
656         // animating view still observable as hidden, uncached
657         CacheUtils.verifyCacheDoesNotContainPositions(mRecyclerView, 2);
658         assertNotNull("Animating view should be found, hidden",
659                 mRecyclerView.mChildHelper.findHiddenNonRemovedView(2));
660         assertTrue(GapWorker.isPrefetchPositionAttached(mRecyclerView, 2));
661 
662         assertTrue(itemAnimator.isRunning());
663         assertEquals(2, llm.getChildCount());
664         assertEquals(4, mRecyclerView.getChildCount());
665     }
666 
667     @Test
viewHolderFindsNestedRecyclerViews()668     public void viewHolderFindsNestedRecyclerViews() {
669         LinearLayoutManager llm = new LinearLayoutManager(getContext());
670         mRecyclerView.setLayoutManager(llm);
671 
672         RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
673         when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
674                 .thenAnswer(new Answer<RecyclerView.ViewHolder>() {
675                     @Override
676                     public RecyclerView.ViewHolder answer(InvocationOnMock invocation)
677                             throws Throwable {
678                         View view = new RecyclerView(getContext());
679                         view.setLayoutParams(new RecyclerView.LayoutParams(100, 100));
680                         return new RecyclerView.ViewHolder(view) {};
681                     }
682                 });
683         when(mockAdapter.getItemCount()).thenReturn(100);
684         mRecyclerView.setAdapter(mockAdapter);
685 
686         layout(100, 200);
687 
688         verify(mockAdapter, times(2)).onCreateViewHolder(any(ViewGroup.class), anyInt());
689         verify(mockAdapter, times(2)).onBindViewHolder(
690                 argThat(new BaseMatcher<RecyclerView.ViewHolder>() {
691                     @Override
692                     public boolean matches(Object item) {
693                         RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder) item;
694                         return holder.itemView == holder.mNestedRecyclerView.get();
695                     }
696 
697                     @Override
698                     public void describeTo(Description description) { }
699                 }),
700                 anyInt(),
701                 any(List.class));
702     }
703 
704     class InnerAdapter extends RecyclerView.Adapter<InnerAdapter.ViewHolder> {
705         private static final int INNER_ITEM_COUNT = 20;
706         int mItemsBound = 0;
707 
708         class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View itemView)709             ViewHolder(View itemView) {
710                 super(itemView);
711             }
712         }
713 
InnerAdapter()714         InnerAdapter() {}
715 
716         @Override
onCreateViewHolder(ViewGroup parent, int viewType)717         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
718             mRecyclerView.registerTimePassingMs(5);
719             View view = new View(parent.getContext());
720             view.setLayoutParams(new RecyclerView.LayoutParams(100, 100));
721             return new ViewHolder(view);
722         }
723 
724         @Override
onBindViewHolder(ViewHolder holder, int position)725         public void onBindViewHolder(ViewHolder holder, int position) {
726             mRecyclerView.registerTimePassingMs(5);
727             mItemsBound++;
728         }
729 
730         @Override
getItemCount()731         public int getItemCount() {
732             return INNER_ITEM_COUNT;
733         }
734     }
735 
736     class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
737 
738         private boolean mReverseInner;
739 
740         class ViewHolder extends RecyclerView.ViewHolder {
741             private final RecyclerView mRecyclerView;
ViewHolder(RecyclerView itemView)742             ViewHolder(RecyclerView itemView) {
743                 super(itemView);
744                 mRecyclerView = itemView;
745             }
746         }
747 
748         ArrayList<InnerAdapter> mAdapters = new ArrayList<>();
749         ArrayList<Parcelable> mSavedStates = new ArrayList<>();
750         RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
751 
OuterAdapter()752         OuterAdapter() {
753             this(false);
754         }
755 
OuterAdapter(boolean reverseInner)756         OuterAdapter(boolean reverseInner) {
757             this(reverseInner, 10);
758         }
759 
OuterAdapter(boolean reverseInner, int itemCount)760         OuterAdapter(boolean reverseInner, int itemCount) {
761             mReverseInner = reverseInner;
762             for (int i = 0; i < itemCount; i++) {
763                 mAdapters.add(new InnerAdapter());
764                 mSavedStates.add(null);
765             }
766         }
767 
addItem()768         void addItem() {
769             int index = getItemCount();
770             mAdapters.add(new InnerAdapter());
771             mSavedStates.add(null);
772             notifyItemInserted(index);
773         }
774 
775         @Override
onCreateViewHolder(ViewGroup parent, int viewType)776         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
777             mRecyclerView.registerTimePassingMs(5);
778 
779             RecyclerView rv = new RecyclerView(parent.getContext()) {
780                 @Override
781                 public int getWindowVisibility() {
782                     // Pretend to be visible to avoid being filtered out
783                     return View.VISIBLE;
784                 }
785             };
786             rv.setLayoutManager(new LinearLayoutManager(parent.getContext(),
787                     LinearLayoutManager.HORIZONTAL, mReverseInner));
788             rv.setRecycledViewPool(mSharedPool);
789             rv.setLayoutParams(new RecyclerView.LayoutParams(200, 100));
790             return new ViewHolder(rv);
791         }
792 
793         @Override
onBindViewHolder(ViewHolder holder, int position)794         public void onBindViewHolder(ViewHolder holder, int position) {
795             mRecyclerView.registerTimePassingMs(5);
796 
797             // Tests may rely on bound holders not being shared between inner adapters,
798             // since we force recycle here
799             holder.mRecyclerView.swapAdapter(mAdapters.get(position), true);
800 
801             Parcelable savedState = mSavedStates.get(position);
802             if (savedState != null) {
803                 holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState);
804                 mSavedStates.set(position, null);
805             }
806         }
807 
808         @Override
onViewRecycled(ViewHolder holder)809         public void onViewRecycled(ViewHolder holder) {
810             mSavedStates.set(holder.getAdapterPosition(),
811                     holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
812         }
813 
814         @Override
getItemCount()815         public int getItemCount() {
816             return mAdapters.size();
817         }
818     }
819 
820     @Test
nestedPrefetchSimple()821     public void nestedPrefetchSimple() {
822         LinearLayoutManager llm = new LinearLayoutManager(getContext());
823         assertEquals(2, llm.getInitialPrefetchItemCount());
824 
825         mRecyclerView.setLayoutManager(llm);
826         mRecyclerView.setAdapter(new OuterAdapter());
827 
828         layout(200, 200);
829         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
830 
831         // prefetch 2 (default)
832         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
833         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
834         assertNotNull(holder);
835         assertNotNull(holder.mNestedRecyclerView);
836         RecyclerView innerView = holder.mNestedRecyclerView.get();
837         CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
838 
839         // prefetch 4
840         ((LinearLayoutManager) innerView.getLayoutManager())
841                 .setInitialPrefetchItemCount(4);
842         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
843         CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1, 2, 3);
844     }
845 
846     @Test
nestedPrefetchNotClearInnerStructureChangeFlag()847     public void nestedPrefetchNotClearInnerStructureChangeFlag() {
848         LinearLayoutManager llm = new LinearLayoutManager(getContext());
849         assertEquals(2, llm.getInitialPrefetchItemCount());
850 
851         mRecyclerView.setLayoutManager(llm);
852         mRecyclerView.setAdapter(new OuterAdapter());
853 
854         layout(200, 200);
855         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
856 
857         // prefetch 2 (default)
858         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
859         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
860         assertNotNull(holder);
861         assertNotNull(holder.mNestedRecyclerView);
862         RecyclerView innerView = holder.mNestedRecyclerView.get();
863         RecyclerView.Adapter innerAdapter = innerView.getAdapter();
864         CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
865         // mStructureChanged is initially true before first layout pass.
866         assertTrue(innerView.mState.mStructureChanged);
867         assertTrue(innerView.hasPendingAdapterUpdates());
868 
869         // layout position 2 and clear mStructureChanged
870         mRecyclerView.scrollToPosition(2);
871         layout(200, 200);
872         mRecyclerView.scrollToPosition(0);
873         layout(200, 200);
874         assertFalse(innerView.mState.mStructureChanged);
875         assertFalse(innerView.hasPendingAdapterUpdates());
876 
877         // notify change on the cached innerView.
878         innerAdapter.notifyDataSetChanged();
879         assertTrue(innerView.mState.mStructureChanged);
880         assertTrue(innerView.hasPendingAdapterUpdates());
881 
882         // prefetch again
883         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
884         ((LinearLayoutManager) innerView.getLayoutManager())
885                 .setInitialPrefetchItemCount(2);
886         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
887         CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
888 
889         // The re-prefetch is not necessary get the same inner view but we will get same Adapter
890         holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
891         innerView = holder.mNestedRecyclerView.get();
892         assertSame(innerAdapter, innerView.getAdapter());
893         // prefetch shouldn't clear the mStructureChanged flag
894         assertTrue(innerView.mState.mStructureChanged);
895         assertTrue(innerView.hasPendingAdapterUpdates());
896     }
897 
898     @Test
nestedPrefetchReverseInner()899     public void nestedPrefetchReverseInner() {
900         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
901         mRecyclerView.setAdapter(new OuterAdapter(/* reverseInner = */ true));
902 
903         layout(200, 200);
904         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
905 
906         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
907         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
908 
909         // anchor from right side, should see last two positions
910         CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView.get(), 18, 19);
911     }
912 
913     @Test
nestedPrefetchOffset()914     public void nestedPrefetchOffset() {
915         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
916         mRecyclerView.setAdapter(new OuterAdapter());
917 
918         layout(200, 200);
919 
920         // Scroll top row by 5.5 items, verify positions 5, 6, 7 showing
921         RecyclerView inner = (RecyclerView) mRecyclerView.getChildAt(0);
922         inner.scrollBy(550, 0);
923         assertEquals(5, RecyclerView.getChildViewHolderInt(inner.getChildAt(0)).mPosition);
924         assertEquals(6, RecyclerView.getChildViewHolderInt(inner.getChildAt(1)).mPosition);
925         assertEquals(7, RecyclerView.getChildViewHolderInt(inner.getChildAt(2)).mPosition);
926 
927         // scroll down 4 rows, up 3 so row 0 is adjacent but uncached
928         mRecyclerView.scrollBy(0, 400);
929         mRecyclerView.scrollBy(0, -300);
930 
931         // top row no longer present
932         CacheUtils.verifyCacheDoesNotContainPositions(mRecyclerView, 0);
933 
934         // prefetch upward, and validate that we've gotten the top row with correct offsets
935         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, -1);
936         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
937         inner = (RecyclerView) CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 0).itemView;
938         CacheUtils.verifyCacheContainsPrefetchedPositions(inner, 5, 6);
939 
940         // prefetch 4
941         ((LinearLayoutManager) inner.getLayoutManager()).setInitialPrefetchItemCount(4);
942         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
943         CacheUtils.verifyCacheContainsPrefetchedPositions(inner, 5, 6, 7, 8);
944     }
945 
946     @Test
nestedPrefetchNotReset()947     public void nestedPrefetchNotReset() {
948         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
949         OuterAdapter outerAdapter = new OuterAdapter();
950         mRecyclerView.setAdapter(outerAdapter);
951 
952         layout(200, 200);
953 
954         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
955 
956         // prefetch row 2, items 0 & 1
957         assertEquals(0, outerAdapter.mAdapters.get(2).mItemsBound);
958         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
959         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
960         RecyclerView innerRecyclerView = holder.mNestedRecyclerView.get();
961 
962         assertNotNull(innerRecyclerView);
963         CacheUtils.verifyCacheContainsPrefetchedPositions(innerRecyclerView, 0, 1);
964         assertEquals(2, outerAdapter.mAdapters.get(2).mItemsBound);
965 
966         // new row comes on, triggers layout...
967         mRecyclerView.scrollBy(0, 50);
968 
969         // ... which shouldn't require new items to be bound,
970         // as prefetch has already done that work
971         assertEquals(2, outerAdapter.mAdapters.get(2).mItemsBound);
972     }
973 
validateRvChildrenValid(RecyclerView recyclerView, int childCount)974     static void validateRvChildrenValid(RecyclerView recyclerView, int childCount) {
975         ChildHelper childHelper = recyclerView.mChildHelper;
976 
977         assertEquals(childCount, childHelper.getUnfilteredChildCount());
978         for (int i = 0; i < childHelper.getUnfilteredChildCount(); i++) {
979             assertFalse(recyclerView.getChildViewHolder(
980                     childHelper.getUnfilteredChildAt(i)).isInvalid());
981         }
982     }
983 
984     @Test
nestedPrefetchCacheNotTouched()985     public void nestedPrefetchCacheNotTouched() {
986         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
987         OuterAdapter outerAdapter = new OuterAdapter();
988         mRecyclerView.setAdapter(outerAdapter);
989 
990         layout(200, 200);
991         mRecyclerView.scrollBy(0, 100);
992 
993         // item 0 is cached
994         assertEquals(2, outerAdapter.mAdapters.get(0).mItemsBound);
995         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 0);
996         validateRvChildrenValid(holder.mNestedRecyclerView.get(), 2);
997 
998         // try and prefetch it
999         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, -1);
1000         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1001 
1002         // make sure cache's inner items aren't rebound unnecessarily
1003         assertEquals(2, outerAdapter.mAdapters.get(0).mItemsBound);
1004         validateRvChildrenValid(holder.mNestedRecyclerView.get(), 2);
1005     }
1006 
1007     @Test
nestedRemoveAnimatingView()1008     public void nestedRemoveAnimatingView() {
1009         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
1010         OuterAdapter outerAdapter = new OuterAdapter(false, 1);
1011         mRecyclerView.setAdapter(outerAdapter);
1012         mRecyclerView.getItemAnimator().setAddDuration(TimeUnit.MILLISECONDS.toNanos(30));
1013 
1014         layout(200, 200);
1015 
1016         // Insert 3 items - only first one in viewport, so only it animates
1017         for (int i = 0; i < 3; i++) {
1018             outerAdapter.addItem();
1019         }
1020         layout(200, 200); // layout again to kick off animation
1021 
1022 
1023         // item 1 is animating, so scroll it out of viewport
1024         mRecyclerView.scrollBy(0, 200);
1025 
1026         // 2 items attached, 1 cached (pos 0), but item animating pos 1 not accounted for...
1027         assertEquals(2, mRecyclerView.mChildHelper.getUnfilteredChildCount());
1028         assertEquals(1, mRecycler.mCachedViews.size());
1029         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 0);
1030         assertEquals(0, mRecyclerView.getRecycledViewPool().getRecycledViewCount(0));
1031 
1032         // until animation ends
1033         mRecyclerView.getItemAnimator().endAnimations();
1034         assertEquals(2, mRecyclerView.mChildHelper.getUnfilteredChildCount());
1035         assertEquals(2, mRecycler.mCachedViews.size());
1036         CacheUtils.verifyCacheContainsPositions(mRecyclerView, 0, 1);
1037         assertEquals(0, mRecyclerView.getRecycledViewPool().getRecycledViewCount(0));
1038 
1039         for (RecyclerView.ViewHolder viewHolder : mRecycler.mCachedViews) {
1040             assertNotNull(viewHolder.mNestedRecyclerView);
1041         }
1042     }
1043 
1044     @Test
nestedExpandCacheCorrectly()1045     public void nestedExpandCacheCorrectly() {
1046         final int DEFAULT_CACHE_SIZE = RecyclerView.Recycler.DEFAULT_CACHE_SIZE;
1047 
1048         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
1049         OuterAdapter outerAdapter = new OuterAdapter();
1050         mRecyclerView.setAdapter(outerAdapter);
1051 
1052         layout(200, 200);
1053 
1054         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
1055         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1056 
1057         // after initial prefetch, view cache max expanded by number of inner items prefetched (2)
1058         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
1059         RecyclerView innerView = holder.mNestedRecyclerView.get();
1060         assertTrue(innerView.getLayoutManager().mPrefetchMaxObservedInInitialPrefetch);
1061         assertEquals(2, innerView.getLayoutManager().mPrefetchMaxCountObserved);
1062         assertEquals(2 + DEFAULT_CACHE_SIZE, innerView.mRecycler.mViewCacheMax);
1063 
1064         try {
1065             // Note: As a hack, we not only must manually dispatch attachToWindow(), but we
1066             // also have to be careful to call innerView.mGapWorker below. mRecyclerView.mGapWorker
1067             // is registered to the wrong thread, since @setup is called on a different thread
1068             // from @Test. Assert this, so this test can be fixed when setup == test thread.
1069             assertEquals(1, mRecyclerView.mGapWorker.mRecyclerViews.size());
1070             assertFalse(innerView.isAttachedToWindow());
1071             innerView.onAttachedToWindow();
1072 
1073             // bring prefetch view into viewport, at which point it shouldn't have cache expanded...
1074             mRecyclerView.scrollBy(0, 100);
1075             assertFalse(innerView.getLayoutManager().mPrefetchMaxObservedInInitialPrefetch);
1076             assertEquals(0, innerView.getLayoutManager().mPrefetchMaxCountObserved);
1077             assertEquals(DEFAULT_CACHE_SIZE, innerView.mRecycler.mViewCacheMax);
1078 
1079             // until a valid horizontal prefetch caches an item, and expands view count by one
1080             innerView.mPrefetchRegistry.setPrefetchVector(1, 0);
1081             innerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS); // NB: must be innerView.mGapWorker
1082             assertFalse(innerView.getLayoutManager().mPrefetchMaxObservedInInitialPrefetch);
1083             assertEquals(1, innerView.getLayoutManager().mPrefetchMaxCountObserved);
1084             assertEquals(1 + DEFAULT_CACHE_SIZE, innerView.mRecycler.mViewCacheMax);
1085         } finally {
1086             if (innerView.isAttachedToWindow()) {
1087                 innerView.onDetachedFromWindow();
1088             }
1089         }
1090     }
1091 
1092     /**
1093      * Similar to OuterAdapter above, but uses notifyDataSetChanged() instead of set/swapAdapter
1094      * to update data for the inner RecyclerViews when containing ViewHolder is bound.
1095      */
1096     class OuterNotifyAdapter extends RecyclerView.Adapter<OuterNotifyAdapter.ViewHolder> {
1097         private static final int OUTER_ITEM_COUNT = 10;
1098 
1099         private boolean mReverseInner;
1100 
1101         class ViewHolder extends RecyclerView.ViewHolder {
1102             private final RecyclerView mRecyclerView;
1103             private final InnerAdapter mAdapter;
ViewHolder(RecyclerView itemView)1104             ViewHolder(RecyclerView itemView) {
1105                 super(itemView);
1106                 mRecyclerView = itemView;
1107                 mAdapter = new InnerAdapter();
1108                 mRecyclerView.setAdapter(mAdapter);
1109             }
1110         }
1111 
1112         ArrayList<Parcelable> mSavedStates = new ArrayList<>();
1113         RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
1114 
OuterNotifyAdapter()1115         OuterNotifyAdapter() {
1116             this(false);
1117         }
1118 
OuterNotifyAdapter(boolean reverseInner)1119         OuterNotifyAdapter(boolean reverseInner) {
1120             mReverseInner = reverseInner;
1121             for (int i = 0; i <= OUTER_ITEM_COUNT; i++) {
1122                 mSavedStates.add(null);
1123             }
1124         }
1125 
1126         @Override
onCreateViewHolder(ViewGroup parent, int viewType)1127         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
1128             mRecyclerView.registerTimePassingMs(5);
1129             RecyclerView rv = new RecyclerView(parent.getContext());
1130             rv.setLayoutManager(new LinearLayoutManager(parent.getContext(),
1131                     LinearLayoutManager.HORIZONTAL, mReverseInner));
1132             rv.setRecycledViewPool(mSharedPool);
1133             rv.setLayoutParams(new RecyclerView.LayoutParams(200, 100));
1134             return new ViewHolder(rv);
1135         }
1136 
1137         @Override
onBindViewHolder(ViewHolder holder, int position)1138         public void onBindViewHolder(ViewHolder holder, int position) {
1139             mRecyclerView.registerTimePassingMs(5);
1140             // if we had actual data to put into our adapter, this is where we'd do it...
1141 
1142             // ... then notify the adapter that it has new content:
1143             holder.mAdapter.notifyDataSetChanged();
1144 
1145             Parcelable savedState = mSavedStates.get(position);
1146             if (savedState != null) {
1147                 holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState);
1148                 mSavedStates.set(position, null);
1149             }
1150         }
1151 
1152         @Override
onViewRecycled(ViewHolder holder)1153         public void onViewRecycled(ViewHolder holder) {
1154             if (holder.getAdapterPosition() >= 0) {
1155                 mSavedStates.set(holder.getAdapterPosition(),
1156                         holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
1157             }
1158         }
1159 
1160         @Override
getItemCount()1161         public int getItemCount() {
1162             return OUTER_ITEM_COUNT;
1163         }
1164     }
1165 
1166     @Test
nestedPrefetchDiscardStaleChildren()1167     public void nestedPrefetchDiscardStaleChildren() {
1168         LinearLayoutManager llm = new LinearLayoutManager(getContext());
1169         assertEquals(2, llm.getInitialPrefetchItemCount());
1170 
1171         mRecyclerView.setLayoutManager(llm);
1172         OuterNotifyAdapter outerAdapter = new OuterNotifyAdapter();
1173         mRecyclerView.setAdapter(outerAdapter);
1174 
1175         // zero cache, so item we prefetch can't already be ready
1176         mRecyclerView.setItemViewCacheSize(0);
1177 
1178         // layout 3 items, then resize to 2...
1179         layout(200, 300);
1180         layout(200, 200);
1181 
1182         // so 1 item is evicted into the RecycledViewPool (bypassing cache)
1183         assertEquals(1, mRecycler.mRecyclerPool.getRecycledViewCount(0));
1184         assertEquals(0, mRecycler.mCachedViews.size());
1185 
1186         // This is a simple imitation of other behavior (namely, varied types in the outer adapter)
1187         // that results in the same initial state to test: items in the pool with attached children
1188         for (RecyclerView.ViewHolder holder : mRecycler.mRecyclerPool.mScrap.get(0).mScrapHeap) {
1189             // verify that children are attached and valid, since the RVs haven't been rebound
1190             assertNotNull(holder.mNestedRecyclerView);
1191             assertFalse(holder.mNestedRecyclerView.get().mDataSetHasChangedAfterLayout);
1192             validateRvChildrenValid(holder.mNestedRecyclerView.get(), 2);
1193         }
1194 
1195         // prefetch the outer item bind, but without enough time to do any inner binds
1196         final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(9);
1197         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
1198         mRecyclerView.mGapWorker.prefetch(deadlineNs);
1199 
1200         // 2 is prefetched without children
1201         CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 2);
1202         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
1203         assertNotNull(holder);
1204         assertNotNull(holder.mNestedRecyclerView);
1205         assertEquals(0, holder.mNestedRecyclerView.get().mChildHelper.getUnfilteredChildCount());
1206         assertEquals(0, holder.mNestedRecyclerView.get().mRecycler.mCachedViews.size());
1207 
1208         // but if we give it more time to bind items, it'll now acquire its inner items
1209         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1210         CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView.get(), 0, 1);
1211     }
1212 
1213 
1214     @Test
nestedPrefetchDiscardStalePrefetch()1215     public void nestedPrefetchDiscardStalePrefetch() {
1216         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
1217         OuterNotifyAdapter outerAdapter = new OuterNotifyAdapter();
1218         mRecyclerView.setAdapter(outerAdapter);
1219 
1220         // zero cache, so item we prefetch can't already be ready
1221         mRecyclerView.setItemViewCacheSize(0);
1222 
1223         // layout as 2x2, starting on row index 2, with empty cache
1224         layout(200, 200);
1225         mRecyclerView.scrollBy(0, 200);
1226 
1227         // no views cached, or previously used (so we can trust number in mItemsBound)
1228         mRecycler.mRecyclerPool.clear();
1229         assertEquals(0, mRecycler.mRecyclerPool.getRecycledViewCount(0));
1230         assertEquals(0, mRecycler.mCachedViews.size());
1231 
1232         // prefetch the outer item and its inner children
1233         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
1234         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1235 
1236         // 4 is prefetched with 2 inner children, first two binds
1237         CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);
1238         RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4);
1239         assertNotNull(holder);
1240         assertNotNull(holder.mNestedRecyclerView);
1241         RecyclerView innerRecyclerView = holder.mNestedRecyclerView.get();
1242         assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
1243         assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());
1244         assertEquals(2, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);
1245 
1246         // notify data set changed, so any previously prefetched items invalid, and re-prefetch
1247         innerRecyclerView.getAdapter().notifyDataSetChanged();
1248         mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
1249         mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
1250 
1251         // 4 is prefetched again...
1252         CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);
1253 
1254         // reusing the same instance with 2 inner children...
1255         assertSame(holder, CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4));
1256         assertSame(innerRecyclerView, holder.mNestedRecyclerView.get());
1257         assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
1258         assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());
1259 
1260         // ... but there should be two new binds
1261         assertEquals(4, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);
1262     }
1263 }
1264