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