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