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