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