1 package androidx.leanback.widget;
2 
3 import static org.junit.Assert.assertEquals;
4 import static org.junit.Assert.assertNotNull;
5 import static org.mockito.ArgumentMatchers.anyInt;
6 import static org.mockito.Mockito.mock;
7 import static org.mockito.Mockito.times;
8 import static org.mockito.Mockito.verify;
9 import static org.mockito.Mockito.when;
10 
11 import android.content.Context;
12 import android.os.Parcelable;
13 import android.view.View;
14 import android.view.ViewGroup;
15 
16 import androidx.recyclerview.widget.RecyclerView;
17 import androidx.test.core.app.ApplicationProvider;
18 import androidx.test.ext.junit.runners.AndroidJUnit4;
19 import androidx.test.filters.MediumTest;
20 
21 import org.junit.Test;
22 import org.junit.runner.RunWith;
23 
24 import java.util.ArrayList;
25 
26 @MediumTest
27 @RunWith(AndroidJUnit4.class)
28 public class GridWidgetPrefetchTest {
29 
getContext()30     private Context getContext() {
31         return ApplicationProvider.getApplicationContext();
32     }
33 
layout(View view, int width, int height)34     private void layout(View view, int width, int height) {
35         view.measure(
36                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
37                 View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
38         view.layout(0, 0, width, height);
39     }
40 
validatePrefetch(BaseGridView gridView, int scrollX, int scrollY, Integer[]... positionData)41     public void validatePrefetch(BaseGridView gridView, int scrollX, int scrollY,
42             Integer[]... positionData) {
43         // duplicates logic in support.v7.widget.CacheUtils#verifyPositionsPrefetched
44         RecyclerView.State state = mock(RecyclerView.State.class);
45         when(state.getItemCount()).thenReturn(gridView.getAdapter().getItemCount());
46         RecyclerView.LayoutManager.LayoutPrefetchRegistry registry
47                 = mock(RecyclerView.LayoutManager.LayoutPrefetchRegistry.class);
48 
49         gridView.getLayoutManager().collectAdjacentPrefetchPositions(scrollX, scrollY,
50                 state, registry);
51 
52         verify(registry, times(positionData.length)).addPosition(anyInt(), anyInt());
53         for (Integer[] aPositionData : positionData) {
54             verify(registry).addPosition(aPositionData[0], aPositionData[1]);
55         }
56     }
57 
createBoxAdapter()58     private RecyclerView.Adapter createBoxAdapter() {
59         return new RecyclerView.Adapter() {
60             @Override
61             public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
62                 View view = new View(getContext());
63                 view.setMinimumWidth(100);
64                 view.setMinimumHeight(100);
65                 return new RecyclerView.ViewHolder(view) {};
66             }
67 
68             @Override
69             public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
70                 // noop
71             }
72 
73             @Override
74             public int getItemCount() {
75                 return 100;
76             }
77         };
78     }
79 
80     @Test
81     public void prefetch() {
82         HorizontalGridView gridView = new HorizontalGridView(getContext());
83         gridView.setNumRows(1);
84         gridView.setRowHeight(100);
85         gridView.setAdapter(createBoxAdapter());
86 
87         layout(gridView, 150, 100);
88 
89         // validate 2 children in viewport
90         assertEquals(2, gridView.getChildCount());
91         assertEquals(0, gridView.getLayoutManager().findViewByPosition(0).getLeft());
92         assertEquals(100, gridView.getLayoutManager().findViewByPosition(1).getLeft());
93 
94         validatePrefetch(gridView, -50, 0); // no view to left
95         validatePrefetch(gridView, 50, 0, new Integer[] {2, 50}); // next view 50 pixels to right
96 
97         // scroll to position 5, and layout
98         gridView.scrollToPosition(5);
99         layout(gridView, 150, 100);
100 
101         /* Visual representation, each number column represents 25 pixels:
102          *              |           |
103          * ... 3 3 4 4 4|4 5 5 5 5 6|6 6 6 7 7 ...
104          *              |           |
105          */
106 
107         // validate the 3 children in the viewport, and their positions
108         assertEquals(3, gridView.getChildCount());
109         assertNotNull(gridView.getLayoutManager().findViewByPosition(4));
110         assertNotNull(gridView.getLayoutManager().findViewByPosition(5));
111         assertNotNull(gridView.getLayoutManager().findViewByPosition(6));
112         assertEquals(-75, gridView.getLayoutManager().findViewByPosition(4).getLeft());
113         assertEquals(25, gridView.getLayoutManager().findViewByPosition(5).getLeft());
114         assertEquals(125, gridView.getLayoutManager().findViewByPosition(6).getLeft());
115 
116         // next views are 75 pixels to right and left:
117         validatePrefetch(gridView, -50, 0, new Integer[] {3, 75});
118         validatePrefetch(gridView, 50, 0, new Integer[] {7, 75});
119 
120         // no views returned for vertical prefetch:
121         validatePrefetch(gridView, 0, 10);
122         validatePrefetch(gridView, 0, -10);
123 
124         // test minor offset
125         gridView.scrollBy(5, 0);
126         validatePrefetch(gridView, -50, 0, new Integer[] {3, 80});
127         validatePrefetch(gridView, 50, 0, new Integer[] {7, 70});
128     }
129 
130     @Test
131     public void prefetchRtl() {
132         HorizontalGridView gridView = new HorizontalGridView(getContext());
133         gridView.setNumRows(1);
134         gridView.setRowHeight(100);
135         gridView.setAdapter(createBoxAdapter());
136         gridView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
137 
138         layout(gridView, 150, 100);
139 
140         // validate 2 children in viewport
141         assertEquals(2, gridView.getChildCount());
142         assertEquals(50, gridView.getLayoutManager().findViewByPosition(0).getLeft());
143         assertEquals(-50, gridView.getLayoutManager().findViewByPosition(1).getLeft());
144 
145         validatePrefetch(gridView, 50, 0); // no view to right
146         validatePrefetch(gridView, -10, 0, new Integer[] {2, 50}); // next view 50 pixels to right
147 
148 
149         // scroll to position 5, and layout
150         gridView.scrollToPosition(5);
151         layout(gridView, 150, 100);
152 
153 
154         /* Visual representation, each number column represents 25 pixels:
155          *              |           |
156          * ... 7 7 6 6 6|6 5 5 5 5 4|4 4 4 3 3 ...
157          *              |           |
158          */
159         // validate 3 children in the viewport
160         assertEquals(3, gridView.getChildCount());
161         assertNotNull(gridView.getLayoutManager().findViewByPosition(6));
162         assertNotNull(gridView.getLayoutManager().findViewByPosition(5));
163         assertNotNull(gridView.getLayoutManager().findViewByPosition(4));
164         assertEquals(-75, gridView.getLayoutManager().findViewByPosition(6).getLeft());
165         assertEquals(25, gridView.getLayoutManager().findViewByPosition(5).getLeft());
166         assertEquals(125, gridView.getLayoutManager().findViewByPosition(4).getLeft());
167 
168         // next views are 75 pixels to right and left:
169         validatePrefetch(gridView, 50, 0, new Integer[] {3, 75});
170         validatePrefetch(gridView, -50, 0, new Integer[] {7, 75});
171 
172         // no views returned for vertical prefetch:
173         validatePrefetch(gridView, 0, 10);
174         validatePrefetch(gridView, 0, -10);
175 
176         // test minor offset
177         gridView.scrollBy(-5, 0);
178         validatePrefetch(gridView, 50, 0, new Integer[] {3, 80});
179         validatePrefetch(gridView, -50, 0, new Integer[] {7, 70});
180     }
181 
182 
183     class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
184         OuterAdapter() {
185             for (int i = 0; i < getItemCount(); i++) {
186                 mAdapters.add(createBoxAdapter());
187                 mSavedStates.add(null);
188             }
189         }
190 
191         class ViewHolder extends RecyclerView.ViewHolder {
192             private final RecyclerView mRecyclerView;
193             ViewHolder(RecyclerView itemView) {
194                 super(itemView);
195                 mRecyclerView = itemView;
196             }
197         }
198 
199         ArrayList<RecyclerView.Adapter> mAdapters = new ArrayList<>();
200         ArrayList<Parcelable> mSavedStates = new ArrayList<>();
201         RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
202 
203         @Override
204         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
205             HorizontalGridView gridView = new HorizontalGridView(getContext());
206             gridView.setNumRows(1);
207             gridView.setRowHeight(100);
208             gridView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
209             gridView.setLayoutParams(new GridLayoutManager.LayoutParams(350, 100));
210             gridView.setRecycledViewPool(mSharedPool);
211             return new ViewHolder(gridView);
212         }
213 
214         @Override
215         public void onBindViewHolder(ViewHolder holder, int position) {
216             holder.mRecyclerView.swapAdapter(mAdapters.get(position), true);
217 
218             Parcelable savedState = mSavedStates.get(position);
219             if (savedState != null) {
220                 holder.mRecyclerView.getLayoutManager().onRestoreInstanceState(savedState);
221                 mSavedStates.set(position, null);
222             }
223         }
224 
225         @Override
226         public int getItemCount() {
227             return 100;
228         }
229     };
230 
231     public void validateInitialPrefetch(BaseGridView gridView,
232             int... positionData) {
233         RecyclerView.LayoutManager.LayoutPrefetchRegistry registry
234                 = mock(RecyclerView.LayoutManager.LayoutPrefetchRegistry.class);
235         gridView.getLayoutManager().collectInitialPrefetchPositions(
236                 gridView.getAdapter().getItemCount(), registry);
237 
238         verify(registry, times(positionData.length)).addPosition(anyInt(), anyInt());
239         for (int position : positionData) {
240             verify(registry).addPosition(position, 0);
241         }
242     }
243 
244     @Test
245     public void prefetchInitialFocusTest() {
246         VerticalGridView view = new VerticalGridView(getContext());
247         view.setNumColumns(1);
248         view.setColumnWidth(350);
249         view.setAdapter(createBoxAdapter());
250 
251         // check default
252         assertEquals(4, view.getInitialPrefetchItemCount());
253 
254         // check setter behavior
255         view.setInitialPrefetchItemCount(0);
256         assertEquals(0, view.getInitialPrefetchItemCount());
257 
258         // check positions fetched, relative to focus
259         view.scrollToPosition(2);
260         view.setInitialPrefetchItemCount(5);
261         validateInitialPrefetch(view, 0, 1, 2, 3, 4);
262 
263         view.setInitialPrefetchItemCount(3);
264         validateInitialPrefetch(view, 1, 2, 3);
265 
266         view.scrollToPosition(0);
267         view.setInitialPrefetchItemCount(4);
268         validateInitialPrefetch(view, 0, 1, 2, 3);
269 
270         view.scrollToPosition(98);
271         view.setInitialPrefetchItemCount(5);
272         validateInitialPrefetch(view, 95, 96, 97, 98, 99);
273 
274         view.setInitialPrefetchItemCount(7);
275         validateInitialPrefetch(view, 93, 94, 95, 96, 97, 98, 99);
276 
277         // implementation detail - rounds up
278         view.scrollToPosition(50);
279         view.setInitialPrefetchItemCount(4);
280         validateInitialPrefetch(view, 49, 50, 51, 52);
281     }
282 
283     @Test
284     public void prefetchNested() {
285         VerticalGridView gridView = new VerticalGridView(getContext());
286         gridView.setNumColumns(1);
287         gridView.setColumnWidth(350);
288         OuterAdapter outerAdapter = new OuterAdapter();
289         gridView.setAdapter(outerAdapter);
290         gridView.setItemViewCacheSize(1); // enough to cache child 0 while offscreen
291 
292         layout(gridView, 350, 150);
293 
294         // validate 2 top level children in viewport
295         assertEquals(2, gridView.getChildCount());
296         for (int y = 0; y < 2; y++) {
297             View child = gridView.getLayoutManager().findViewByPosition(y);
298             assertEquals(y * 100, child.getTop());
299             // each has 4 children
300 
301             HorizontalGridView inner = (HorizontalGridView) child;
302             for (int x = 0; x < 4; x++) {
303                 assertEquals(x * 100, inner.getLayoutManager().findViewByPosition(x).getLeft());
304             }
305         }
306 
307         // center child 0 at position 10
308         HorizontalGridView offsetChild =
309                 (HorizontalGridView) gridView.getLayoutManager().findViewByPosition(0);
310         offsetChild.scrollToPosition(10);
311 
312         // scroll to position 2, and layout
313         gridView.scrollToPosition(2);
314         layout(gridView, 350, 150);
315 
316         // now, offset by 175, centered around row 2. Validate 3 top level children in viewport
317         assertEquals(3, gridView.getChildCount());
318         for (int y = 1; y < 4; y++) {
319             assertEquals(y * 100 - 175, gridView.getLayoutManager().findViewByPosition(y).getTop());
320         }
321 
322         validatePrefetch(gridView, 0, -5, new Integer[] {0, 75});
323         validatePrefetch(gridView, 0, 5, new Integer[] {4, 75});
324 
325         // assume offsetChild still bound, in cache, just not attached...
326         validateInitialPrefetch(offsetChild, 9, 10, 11, 12);
327     }
328 }
329