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