1/* 2 * Copyright (C) 2014 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 18package android.support.v7.widget; 19 20import static android.support.v7.widget.RecyclerView.NO_POSITION; 21import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING; 22import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE; 23import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING; 24import static android.support.v7.widget.RecyclerView.getChildViewHolderInt; 25 26import static org.hamcrest.CoreMatchers.is; 27import static org.hamcrest.CoreMatchers.not; 28import static org.hamcrest.CoreMatchers.sameInstance; 29import static org.junit.Assert.assertEquals; 30import static org.junit.Assert.assertFalse; 31import static org.junit.Assert.assertNotNull; 32import static org.junit.Assert.assertNotSame; 33import static org.junit.Assert.assertNull; 34import static org.junit.Assert.assertSame; 35import static org.junit.Assert.assertThat; 36import static org.junit.Assert.assertTrue; 37import static org.junit.Assert.fail; 38import static org.mockito.Mockito.any; 39import static org.mockito.Mockito.anyInt; 40import static org.mockito.Mockito.eq; 41import static org.mockito.Mockito.mock; 42import static org.mockito.Mockito.never; 43import static org.mockito.Mockito.spy; 44import static org.mockito.Mockito.times; 45import static org.mockito.Mockito.verify; 46 47import android.content.Context; 48import android.graphics.Color; 49import android.graphics.PointF; 50import android.graphics.Rect; 51import android.os.Build; 52import android.os.SystemClock; 53import android.support.annotation.Nullable; 54import android.support.test.filters.SdkSuppress; 55import android.support.test.runner.AndroidJUnit4; 56import android.support.v4.view.ViewCompat; 57import android.support.v7.util.TouchUtils; 58import android.test.suitebuilder.annotation.MediumTest; 59import android.util.AttributeSet; 60import android.util.Log; 61import android.view.Gravity; 62import android.view.MotionEvent; 63import android.view.View; 64import android.view.ViewConfiguration; 65import android.view.ViewGroup; 66import android.view.ViewTreeObserver; 67import android.widget.LinearLayout; 68import android.widget.TextView; 69 70import org.hamcrest.CoreMatchers; 71import org.hamcrest.MatcherAssert; 72import org.junit.Test; 73import org.junit.runner.RunWith; 74 75import java.util.ArrayList; 76import java.util.HashMap; 77import java.util.List; 78import java.util.Map; 79import java.util.concurrent.CountDownLatch; 80import java.util.concurrent.TimeUnit; 81import java.util.concurrent.atomic.AtomicBoolean; 82import java.util.concurrent.atomic.AtomicInteger; 83 84@RunWith(AndroidJUnit4.class) 85@MediumTest 86public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest { 87 private static final int FLAG_HORIZONTAL = 1; 88 private static final int FLAG_VERTICAL = 1 << 1; 89 private static final int FLAG_FLING = 1 << 2; 90 91 private static final boolean DEBUG = false; 92 93 private static final String TAG = "RecyclerViewLayoutTest"; 94 95 public RecyclerViewLayoutTest() { 96 super(DEBUG); 97 } 98 99 @Test 100 public void detachAttachGetReadyWithoutChanges() throws Throwable { 101 detachAttachGetReady(false, false, false); 102 } 103 104 @Test 105 public void detachAttachGetReadyRequireLayout() throws Throwable { 106 detachAttachGetReady(true, false, false); 107 } 108 109 @Test 110 public void detachAttachGetReadyRemoveAdapter() throws Throwable { 111 detachAttachGetReady(false, true, false); 112 } 113 114 @Test 115 public void detachAttachGetReadyRemoveLayoutManager() throws Throwable { 116 detachAttachGetReady(false, false, true); 117 } 118 119 private void detachAttachGetReady(final boolean requestLayoutOnDetach, 120 final boolean removeAdapter, final boolean removeLayoutManager) throws Throwable { 121 final LinearLayout ll1 = new LinearLayout(getActivity()); 122 final LinearLayout ll2 = new LinearLayout(getActivity()); 123 final LinearLayout ll3 = new LinearLayout(getActivity()); 124 125 final RecyclerView rv = new RecyclerView(getActivity()); 126 ll1.addView(ll2); 127 ll2.addView(ll3); 128 ll3.addView(rv); 129 TestLayoutManager layoutManager = new TestLayoutManager() { 130 @Override 131 public void onLayoutCompleted(RecyclerView.State state) { 132 super.onLayoutCompleted(state); 133 layoutLatch.countDown(); 134 } 135 136 @Override 137 public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 138 super.onDetachedFromWindow(view, recycler); 139 if (requestLayoutOnDetach) { 140 view.requestLayout(); 141 } 142 } 143 }; 144 rv.setLayoutManager(layoutManager); 145 rv.setAdapter(new TestAdapter(10)); 146 layoutManager.expectLayouts(1); 147 runTestOnUiThread(new Runnable() { 148 @Override 149 public void run() { 150 getActivity().getContainer().addView(ll1); 151 } 152 }); 153 layoutManager.waitForLayout(2); 154 runTestOnUiThread(new Runnable() { 155 @Override 156 public void run() { 157 ll1.removeView(ll2); 158 } 159 }); 160 getInstrumentation().waitForIdleSync(); 161 if (removeLayoutManager) { 162 rv.setLayoutManager(null); 163 rv.setLayoutManager(layoutManager); 164 } 165 if (removeAdapter) { 166 rv.setAdapter(null); 167 rv.setAdapter(new TestAdapter(10)); 168 } 169 final boolean requireLayout = requestLayoutOnDetach || removeAdapter || removeLayoutManager; 170 layoutManager.expectLayouts(1); 171 runTestOnUiThread(new Runnable() { 172 @Override 173 public void run() { 174 ll1.addView(ll2); 175 if (requireLayout) { 176 assertTrue(rv.hasPendingAdapterUpdates()); 177 assertFalse(rv.mFirstLayoutComplete); 178 } else { 179 assertFalse(rv.hasPendingAdapterUpdates()); 180 assertTrue(rv.mFirstLayoutComplete); 181 } 182 } 183 }); 184 if (requireLayout) { 185 layoutManager.waitForLayout(2); 186 } else { 187 layoutManager.assertNoLayout("nothing is invalid, layout should not happen", 2); 188 } 189 } 190 191 @Test 192 public void detachRvAndLayoutManagerProperly() throws Throwable { 193 final RecyclerView rv = new RecyclerView(getActivity()); 194 final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true) { 195 @Override 196 public void onAttachedToWindow(RecyclerView view) { 197 super.onAttachedToWindow(view); 198 assertThat(view.mLayout, is((RecyclerView.LayoutManager) this)); 199 } 200 201 @Override 202 public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 203 super.onDetachedFromWindow(view, recycler); 204 assertThat(view.mLayout, is((RecyclerView.LayoutManager) this)); 205 } 206 }; 207 final Runnable check = new Runnable() { 208 @Override 209 public void run() { 210 assertThat("bound between the RV and the LM should be disconnected at the" 211 + " same time", rv.mLayout == lm, is(lm.mRecyclerView == rv)); 212 } 213 }; 214 final AtomicInteger detachCounter = new AtomicInteger(0); 215 rv.setAdapter(new TestAdapter(10) { 216 @Override 217 public void onBindViewHolder(TestViewHolder holder, 218 int position) { 219 super.onBindViewHolder(holder, position); 220 holder.itemView.setFocusable(true); 221 holder.itemView.setFocusableInTouchMode(true); 222 } 223 224 @Override 225 public void onViewDetachedFromWindow(TestViewHolder holder) { 226 super.onViewDetachedFromWindow(holder); 227 detachCounter.incrementAndGet(); 228 check.run(); 229 } 230 231 @Override 232 public void onViewRecycled(TestViewHolder holder) { 233 super.onViewRecycled(holder); 234 check.run(); 235 } 236 }); 237 rv.setLayoutManager(lm); 238 lm.expectLayouts(1); 239 setRecyclerView(rv); 240 lm.waitForLayout(2); 241 assertThat("test sanity", rv.getChildCount(), is(10)); 242 243 final TestLayoutManager replacement = new LayoutAllLayoutManager(true); 244 replacement.expectLayouts(1); 245 runTestOnUiThread(new Runnable() { 246 @Override 247 public void run() { 248 rv.setLayoutManager(replacement); 249 } 250 }); 251 replacement.waitForLayout(2); 252 assertThat("test sanity", rv.getChildCount(), is(10)); 253 assertThat("all initial views should be detached", detachCounter.get(), is(10)); 254 checkForMainThreadException(); 255 } 256 257 @Test 258 public void focusSearchWithOtherFocusables() throws Throwable { 259 final LinearLayout container = new LinearLayout(getActivity()); 260 container.setOrientation(LinearLayout.VERTICAL); 261 RecyclerView rv = new RecyclerView(getActivity()); 262 mRecyclerView = rv; 263 rv.setAdapter(new TestAdapter(10) { 264 @Override 265 public void onBindViewHolder(TestViewHolder holder, 266 int position) { 267 super.onBindViewHolder(holder, position); 268 holder.itemView.setFocusableInTouchMode(true); 269 holder.itemView.setLayoutParams( 270 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 271 ViewGroup.LayoutParams.WRAP_CONTENT)); 272 } 273 }); 274 TestLayoutManager tlm = new TestLayoutManager() { 275 @Override 276 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 277 detachAndScrapAttachedViews(recycler); 278 layoutRange(recycler, 0, 1); 279 layoutLatch.countDown(); 280 } 281 282 @Nullable 283 @Override 284 public View onFocusSearchFailed(View focused, int direction, 285 RecyclerView.Recycler recycler, 286 RecyclerView.State state) { 287 assertEquals(View.FOCUS_FORWARD, direction); 288 assertEquals(1, getChildCount()); 289 View child0 = getChildAt(0); 290 View view = recycler.getViewForPosition(1); 291 addView(view); 292 measureChild(view, 0, 0); 293 layoutDecorated(view, 0, child0.getBottom(), getDecoratedMeasuredWidth(view), 294 child0.getBottom() + getDecoratedMeasuredHeight(view)); 295 return view; 296 } 297 }; 298 tlm.setAutoMeasureEnabled(true); 299 rv.setLayoutManager(tlm); 300 TextView viewAbove = new TextView(getActivity()); 301 viewAbove.setText("view above"); 302 viewAbove.setFocusableInTouchMode(true); 303 container.addView(viewAbove); 304 container.addView(rv); 305 TextView viewBelow = new TextView(getActivity()); 306 viewBelow.setText("view below"); 307 viewBelow.setFocusableInTouchMode(true); 308 container.addView(viewBelow); 309 tlm.expectLayouts(1); 310 runTestOnUiThread(new Runnable() { 311 @Override 312 public void run() { 313 getActivity().getContainer().addView(container); 314 } 315 }); 316 317 tlm.waitForLayout(2); 318 requestFocus(viewAbove, true); 319 assertTrue(viewAbove.hasFocus()); 320 View newFocused = focusSearch(viewAbove, View.FOCUS_FORWARD); 321 assertThat(newFocused, sameInstance(rv.getChildAt(0))); 322 newFocused = focusSearch(rv.getChildAt(0), View.FOCUS_FORWARD); 323 assertThat(newFocused, sameInstance(rv.getChildAt(1))); 324 } 325 326 @Test 327 public void boundingBoxNoTranslation() throws Throwable { 328 transformedBoundingBoxTest(new ViewRunnable() { 329 @Override 330 public void run(View view) throws RuntimeException { 331 view.layout(10, 10, 30, 50); 332 assertThat(getTransformedBoundingBox(view), is(new Rect(10, 10, 30, 50))); 333 } 334 }); 335 } 336 337 @Test 338 public void boundingBoxTranslateX() throws Throwable { 339 transformedBoundingBoxTest(new ViewRunnable() { 340 @Override 341 public void run(View view) throws RuntimeException { 342 view.layout(10, 10, 30, 50); 343 ViewCompat.setTranslationX(view, 10); 344 assertThat(getTransformedBoundingBox(view), is(new Rect(20, 10, 40, 50))); 345 } 346 }); 347 } 348 349 @Test 350 public void boundingBoxTranslateY() throws Throwable { 351 transformedBoundingBoxTest(new ViewRunnable() { 352 @Override 353 public void run(View view) throws RuntimeException { 354 view.layout(10, 10, 30, 50); 355 ViewCompat.setTranslationY(view, 10); 356 assertThat(getTransformedBoundingBox(view), is(new Rect(10, 20, 30, 60))); 357 } 358 }); 359 } 360 361 @Test 362 public void boundingBoxScaleX() throws Throwable { 363 transformedBoundingBoxTest(new ViewRunnable() { 364 @Override 365 public void run(View view) throws RuntimeException { 366 view.layout(10, 10, 30, 50); 367 ViewCompat.setScaleX(view, 2); 368 assertThat(getTransformedBoundingBox(view), is(new Rect(0, 10, 40, 50))); 369 } 370 }); 371 } 372 373 @Test 374 public void boundingBoxScaleY() throws Throwable { 375 transformedBoundingBoxTest(new ViewRunnable() { 376 @Override 377 public void run(View view) throws RuntimeException { 378 view.layout(10, 10, 30, 50); 379 ViewCompat.setScaleY(view, 2); 380 assertThat(getTransformedBoundingBox(view), is(new Rect(10, -10, 30, 70))); 381 } 382 }); 383 } 384 385 @Test 386 public void boundingBoxRotated() throws Throwable { 387 transformedBoundingBoxTest(new ViewRunnable() { 388 @Override 389 public void run(View view) throws RuntimeException { 390 view.layout(10, 10, 30, 50); 391 ViewCompat.setRotation(view, 90); 392 assertThat(getTransformedBoundingBox(view), is(new Rect(0, 20, 40, 40))); 393 } 394 }); 395 } 396 397 @Test 398 public void boundingBoxRotatedWithDecorOffsets() throws Throwable { 399 final RecyclerView recyclerView = new RecyclerView(getActivity()); 400 final TestAdapter adapter = new TestAdapter(1); 401 recyclerView.setAdapter(adapter); 402 recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { 403 @Override 404 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 405 RecyclerView.State state) { 406 outRect.set(1, 2, 3, 4); 407 } 408 }); 409 TestLayoutManager layoutManager = new TestLayoutManager() { 410 @Override 411 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 412 detachAndScrapAttachedViews(recycler); 413 View view = recycler.getViewForPosition(0); 414 addView(view); 415 view.measure( 416 View.MeasureSpec.makeMeasureSpec(20, View.MeasureSpec.EXACTLY), 417 View.MeasureSpec.makeMeasureSpec(40, View.MeasureSpec.EXACTLY) 418 ); 419 // trigger decor offsets calculation 420 calculateItemDecorationsForChild(view, new Rect()); 421 view.layout(10, 10, 30, 50); 422 ViewCompat.setRotation(view, 90); 423 assertThat(RecyclerViewLayoutTest.this.getTransformedBoundingBox(view), 424 is(new Rect(-4, 19, 42, 43))); 425 426 layoutLatch.countDown(); 427 } 428 }; 429 recyclerView.setLayoutManager(layoutManager); 430 layoutManager.expectLayouts(1); 431 setRecyclerView(recyclerView); 432 layoutManager.waitForLayout(2); 433 checkForMainThreadException(); 434 } 435 436 private Rect getTransformedBoundingBox(View child) { 437 Rect rect = new Rect(); 438 mRecyclerView.getLayoutManager().getTransformedBoundingBox(child, true, rect); 439 return rect; 440 } 441 442 public void transformedBoundingBoxTest(final ViewRunnable layout) throws Throwable { 443 final RecyclerView recyclerView = new RecyclerView(getActivity()); 444 final TestAdapter adapter = new TestAdapter(1); 445 recyclerView.setAdapter(adapter); 446 TestLayoutManager layoutManager = new TestLayoutManager() { 447 @Override 448 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 449 detachAndScrapAttachedViews(recycler); 450 View view = recycler.getViewForPosition(0); 451 addView(view); 452 view.measure( 453 View.MeasureSpec.makeMeasureSpec(20, View.MeasureSpec.EXACTLY), 454 View.MeasureSpec.makeMeasureSpec(40, View.MeasureSpec.EXACTLY) 455 ); 456 layout.run(view); 457 layoutLatch.countDown(); 458 } 459 }; 460 recyclerView.setLayoutManager(layoutManager); 461 layoutManager.expectLayouts(1); 462 setRecyclerView(recyclerView); 463 layoutManager.waitForLayout(2); 464 checkForMainThreadException(); 465 } 466 467 @Test 468 public void flingFrozen() throws Throwable { 469 testScrollFrozen(true); 470 } 471 472 @Test 473 public void dragFrozen() throws Throwable { 474 testScrollFrozen(false); 475 } 476 477 @Test 478 public void requestRectOnScreenWithScrollOffset() throws Throwable { 479 final RecyclerView recyclerView = new RecyclerView(getActivity()); 480 final LayoutAllLayoutManager tlm = spy(new LayoutAllLayoutManager()); 481 final int scrollY = 50; 482 RecyclerView.Adapter adapter = new RecyclerView.Adapter() { 483 @Override 484 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 485 View view = new View(parent.getContext()); 486 view.setScrollY(scrollY); 487 return new RecyclerView.ViewHolder(view) { 488 }; 489 } 490 @Override 491 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {} 492 @Override 493 public int getItemCount() { 494 return 1; 495 } 496 }; 497 recyclerView.setAdapter(adapter); 498 recyclerView.setLayoutManager(tlm); 499 tlm.expectLayouts(1); 500 setRecyclerView(recyclerView); 501 tlm.waitForLayout(1); 502 final View child = recyclerView.getChildAt(0); 503 assertThat(child.getScrollY(), CoreMatchers.is(scrollY)); 504 runTestOnUiThread(new Runnable() { 505 @Override 506 public void run() { 507 recyclerView.requestChildRectangleOnScreen(child, new Rect(3, 4, 5, 6), true); 508 verify(tlm, times(1)).scrollVerticallyBy(eq(-46), any(RecyclerView.Recycler.class), 509 any(RecyclerView.State.class)); 510 } 511 }); 512 } 513 514 @Test 515 public void reattachAndScrollCrash() throws Throwable { 516 final RecyclerView recyclerView = new RecyclerView(getActivity()); 517 final TestLayoutManager tlm = new TestLayoutManager() { 518 519 @Override 520 public boolean canScrollVertically() { 521 return true; 522 } 523 524 @Override 525 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 526 layoutRange(recycler, 0, Math.min(state.getItemCount(), 10)); 527 } 528 529 @Override 530 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 531 RecyclerView.State state) { 532 // Access views in the state (that might have been deleted). 533 for (int i = 10; i < state.getItemCount(); i++) { 534 recycler.getViewForPosition(i); 535 } 536 return dy; 537 } 538 }; 539 540 final TestAdapter adapter = new TestAdapter(12); 541 542 recyclerView.setAdapter(adapter); 543 recyclerView.setLayoutManager(tlm); 544 545 setRecyclerView(recyclerView); 546 547 runTestOnUiThread(new Runnable() { 548 @Override 549 public void run() { 550 getActivity().getContainer().removeView(recyclerView); 551 getActivity().getContainer().addView(recyclerView); 552 try { 553 adapter.deleteAndNotify(1, adapter.getItemCount() - 1); 554 } catch (Throwable throwable) { 555 postExceptionToInstrumentation(throwable); 556 } 557 recyclerView.scrollBy(0, 10); 558 } 559 }); 560 } 561 562 private void testScrollFrozen(boolean fling) throws Throwable { 563 RecyclerView recyclerView = new RecyclerView(getActivity()); 564 565 final int horizontalScrollCount = 3; 566 final int verticalScrollCount = 3; 567 final int horizontalVelocity = 1000; 568 final int verticalVelocity = 1000; 569 final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount); 570 final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount); 571 TestLayoutManager tlm = new TestLayoutManager() { 572 @Override 573 public boolean canScrollHorizontally() { 574 return true; 575 } 576 577 @Override 578 public boolean canScrollVertically() { 579 return true; 580 } 581 582 @Override 583 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 584 layoutRange(recycler, 0, 10); 585 layoutLatch.countDown(); 586 } 587 588 @Override 589 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 590 RecyclerView.State state) { 591 if (verticalCounter.get() > 0) { 592 verticalCounter.decrementAndGet(); 593 return dy; 594 } 595 return 0; 596 } 597 598 @Override 599 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 600 RecyclerView.State state) { 601 if (horizontalCounter.get() > 0) { 602 horizontalCounter.decrementAndGet(); 603 return dx; 604 } 605 return 0; 606 } 607 }; 608 TestAdapter adapter = new TestAdapter(100); 609 recyclerView.setAdapter(adapter); 610 recyclerView.setLayoutManager(tlm); 611 tlm.expectLayouts(1); 612 setRecyclerView(recyclerView); 613 tlm.waitForLayout(2); 614 615 freezeLayout(true); 616 617 if (fling) { 618 assertFalse("fling should be blocked", fling(horizontalVelocity, verticalVelocity)); 619 } else { // drag 620 TouchUtils.dragViewTo(getInstrumentation(), recyclerView, 621 Gravity.LEFT | Gravity.TOP, 622 mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); 623 } 624 assertEquals("rv's horizontal scroll cb must not run", horizontalScrollCount, 625 horizontalCounter.get()); 626 assertEquals("rv's vertical scroll cb must not run", verticalScrollCount, 627 verticalCounter.get()); 628 629 freezeLayout(false); 630 631 if (fling) { 632 assertTrue("fling should be started", fling(horizontalVelocity, verticalVelocity)); 633 } else { // drag 634 TouchUtils.dragViewTo(getInstrumentation(), recyclerView, 635 Gravity.LEFT | Gravity.TOP, 636 mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); 637 } 638 assertEquals("rv's horizontal scroll cb must finishes", 0, horizontalCounter.get()); 639 assertEquals("rv's vertical scroll cb must finishes", 0, verticalCounter.get()); 640 } 641 642 @Test 643 public void testFocusSearchAfterChangedData() throws Throwable { 644 final RecyclerView recyclerView = new RecyclerView(getActivity()); 645 TestLayoutManager tlm = new TestLayoutManager() { 646 @Override 647 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 648 layoutRange(recycler, 0, 2); 649 layoutLatch.countDown(); 650 } 651 652 @Nullable 653 @Override 654 public View onFocusSearchFailed(View focused, int direction, 655 RecyclerView.Recycler recycler, 656 RecyclerView.State state) { 657 try { 658 recycler.getViewForPosition(state.getItemCount() - 1); 659 } catch (Throwable t) { 660 postExceptionToInstrumentation(t); 661 } 662 return null; 663 } 664 }; 665 recyclerView.setLayoutManager(tlm); 666 final TestAdapter adapter = new TestAdapter(10) { 667 @Override 668 public void onBindViewHolder(TestViewHolder holder, int position) { 669 super.onBindViewHolder(holder, position); 670 holder.itemView.setFocusable(false); 671 holder.itemView.setFocusableInTouchMode(false); 672 } 673 }; 674 recyclerView.setAdapter(adapter); 675 tlm.expectLayouts(1); 676 setRecyclerView(recyclerView); 677 tlm.waitForLayout(1); 678 runTestOnUiThread(new Runnable() { 679 @Override 680 public void run() { 681 adapter.mItems.remove(9); 682 adapter.notifyItemRemoved(9); 683 recyclerView.focusSearch(recyclerView.getChildAt(1), View.FOCUS_DOWN); 684 } 685 }); 686 checkForMainThreadException(); 687 } 688 689 @Test 690 public void testFocusSearchWithRemovedFocusedItem() throws Throwable { 691 final RecyclerView recyclerView = new RecyclerView(getActivity()); 692 recyclerView.setItemAnimator(null); 693 TestLayoutManager tlm = new LayoutAllLayoutManager(); 694 recyclerView.setLayoutManager(tlm); 695 final TestAdapter adapter = new TestAdapter(10) { 696 @Override 697 public void onBindViewHolder(TestViewHolder holder, int position) { 698 super.onBindViewHolder(holder, position); 699 holder.itemView.setFocusable(true); 700 holder.itemView.setFocusableInTouchMode(true); 701 } 702 }; 703 recyclerView.setAdapter(adapter); 704 tlm.expectLayouts(1); 705 setRecyclerView(recyclerView); 706 tlm.waitForLayout(1); 707 final RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(9); 708 requestFocus(toFocus.itemView, true); 709 assertThat("test sanity", toFocus.itemView.hasFocus(), is(true)); 710 runTestOnUiThread(new Runnable() { 711 @Override 712 public void run() { 713 adapter.mItems.remove(9); 714 adapter.notifyItemRemoved(9); 715 recyclerView.focusSearch(toFocus.itemView, View.FOCUS_DOWN); 716 } 717 }); 718 checkForMainThreadException(); 719 } 720 721 722 @Test 723 public void testFocusSearchFailFrozen() throws Throwable { 724 RecyclerView recyclerView = new RecyclerView(getActivity()); 725 final CountDownLatch focusLatch = new CountDownLatch(1); 726 final AtomicInteger focusSearchCalled = new AtomicInteger(0); 727 TestLayoutManager tlm = new TestLayoutManager() { 728 @Override 729 public boolean canScrollHorizontally() { 730 return true; 731 } 732 733 @Override 734 public boolean canScrollVertically() { 735 return true; 736 } 737 738 @Override 739 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 740 layoutRange(recycler, 0, 10); 741 layoutLatch.countDown(); 742 } 743 744 @Override 745 public View onFocusSearchFailed(View focused, int direction, 746 RecyclerView.Recycler recycler, RecyclerView.State state) { 747 focusSearchCalled.addAndGet(1); 748 focusLatch.countDown(); 749 return null; 750 } 751 }; 752 TestAdapter adapter = new TestAdapter(100); 753 recyclerView.setAdapter(adapter); 754 recyclerView.setLayoutManager(tlm); 755 tlm.expectLayouts(1); 756 setRecyclerView(recyclerView); 757 tlm.waitForLayout(2); 758 final View c = recyclerView.getChildAt(recyclerView.getChildCount() - 1); 759 assertTrue("test sanity", requestFocus(c, true)); 760 assertTrue("test sanity", c.hasFocus()); 761 freezeLayout(true); 762 focusSearch(recyclerView, c, View.FOCUS_DOWN); 763 assertEquals("onFocusSearchFailed should not be called when layout is frozen", 764 0, focusSearchCalled.get()); 765 freezeLayout(false); 766 focusSearch(c, View.FOCUS_DOWN); 767 assertTrue(focusLatch.await(2, TimeUnit.SECONDS)); 768 assertEquals(1, focusSearchCalled.get()); 769 } 770 771 public View focusSearch(final ViewGroup parent, final View focused, final int direction) 772 throws Throwable { 773 final View[] result = new View[1]; 774 runTestOnUiThread(new Runnable() { 775 @Override 776 public void run() { 777 result[0] = parent.focusSearch(focused, direction); 778 } 779 }); 780 return result[0]; 781 } 782 783 @Test 784 public void frozenAndChangeAdapter() throws Throwable { 785 RecyclerView recyclerView = new RecyclerView(getActivity()); 786 787 final AtomicInteger focusSearchCalled = new AtomicInteger(0); 788 TestLayoutManager tlm = new TestLayoutManager() { 789 @Override 790 public boolean canScrollHorizontally() { 791 return true; 792 } 793 794 @Override 795 public boolean canScrollVertically() { 796 return true; 797 } 798 799 @Override 800 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 801 layoutRange(recycler, 0, 10); 802 layoutLatch.countDown(); 803 } 804 805 @Override 806 public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, 807 RecyclerView.State state) { 808 focusSearchCalled.addAndGet(1); 809 return null; 810 } 811 }; 812 TestAdapter adapter = new TestAdapter(100); 813 recyclerView.setAdapter(adapter); 814 recyclerView.setLayoutManager(tlm); 815 tlm.expectLayouts(1); 816 setRecyclerView(recyclerView); 817 tlm.waitForLayout(2); 818 819 freezeLayout(true); 820 TestAdapter adapter2 = new TestAdapter(1000); 821 setAdapter(adapter2); 822 assertFalse(recyclerView.isLayoutFrozen()); 823 assertSame(adapter2, recyclerView.getAdapter()); 824 825 freezeLayout(true); 826 TestAdapter adapter3 = new TestAdapter(1000); 827 swapAdapter(adapter3, true); 828 assertFalse(recyclerView.isLayoutFrozen()); 829 assertSame(adapter3, recyclerView.getAdapter()); 830 } 831 832 @Test 833 public void noLayoutIf0ItemsAreChanged() throws Throwable { 834 unnecessaryNotifyEvents(new AdapterRunnable() { 835 @Override 836 public void run(TestAdapter adapter) throws Throwable { 837 adapter.notifyItemRangeChanged(3, 0); 838 } 839 }); 840 } 841 842 @Test 843 public void noLayoutIf0ItemsAreChangedWithPayload() throws Throwable { 844 unnecessaryNotifyEvents(new AdapterRunnable() { 845 @Override 846 public void run(TestAdapter adapter) throws Throwable { 847 adapter.notifyItemRangeChanged(0, 0, new Object()); 848 } 849 }); 850 } 851 852 @Test 853 public void noLayoutIf0ItemsAreAdded() throws Throwable { 854 unnecessaryNotifyEvents(new AdapterRunnable() { 855 @Override 856 public void run(TestAdapter adapter) throws Throwable { 857 adapter.notifyItemRangeInserted(3, 0); 858 } 859 }); 860 } 861 862 @Test 863 public void noLayoutIf0ItemsAreRemoved() throws Throwable { 864 unnecessaryNotifyEvents(new AdapterRunnable() { 865 @Override 866 public void run(TestAdapter adapter) throws Throwable { 867 adapter.notifyItemRangeRemoved(3, 0); 868 } 869 }); 870 } 871 872 @Test 873 public void noLayoutIfItemMovedIntoItsOwnPlace() throws Throwable { 874 unnecessaryNotifyEvents(new AdapterRunnable() { 875 @Override 876 public void run(TestAdapter adapter) throws Throwable { 877 adapter.notifyItemMoved(3, 3); 878 } 879 }); 880 } 881 882 public void unnecessaryNotifyEvents(final AdapterRunnable action) throws Throwable { 883 final RecyclerView recyclerView = new RecyclerView(getActivity()); 884 final TestAdapter adapter = new TestAdapter(5); 885 TestLayoutManager tlm = new TestLayoutManager() { 886 @Override 887 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 888 super.onLayoutChildren(recycler, state); 889 layoutLatch.countDown(); 890 } 891 }; 892 recyclerView.setLayoutManager(tlm); 893 recyclerView.setAdapter(adapter); 894 tlm.expectLayouts(1); 895 setRecyclerView(recyclerView); 896 tlm.waitForLayout(1); 897 // ready 898 tlm.expectLayouts(1); 899 runTestOnUiThread(new Runnable() { 900 @Override 901 public void run() { 902 try { 903 action.run(adapter); 904 } catch (Throwable throwable) { 905 postExceptionToInstrumentation(throwable); 906 } 907 } 908 }); 909 tlm.assertNoLayout("dummy event should not trigger a layout", 1); 910 checkForMainThreadException(); 911 } 912 913 @Test 914 public void scrollToPositionCallback() throws Throwable { 915 RecyclerView recyclerView = new RecyclerView(getActivity()); 916 TestLayoutManager tlm = new TestLayoutManager() { 917 int scrollPos = RecyclerView.NO_POSITION; 918 919 @Override 920 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 921 layoutLatch.countDown(); 922 if (scrollPos == RecyclerView.NO_POSITION) { 923 layoutRange(recycler, 0, 10); 924 } else { 925 layoutRange(recycler, scrollPos, scrollPos + 10); 926 } 927 } 928 929 @Override 930 public void scrollToPosition(int position) { 931 scrollPos = position; 932 requestLayout(); 933 } 934 }; 935 recyclerView.setLayoutManager(tlm); 936 TestAdapter adapter = new TestAdapter(100); 937 recyclerView.setAdapter(adapter); 938 final AtomicInteger rvCounter = new AtomicInteger(0); 939 final AtomicInteger viewGroupCounter = new AtomicInteger(0); 940 941 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 942 @Override 943 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 944 rvCounter.incrementAndGet(); 945 super.onScrolled(recyclerView, dx, dy); 946 } 947 }); 948 949 getRecyclerViewContainer().getViewTreeObserver().addOnScrollChangedListener( 950 new ViewTreeObserver.OnScrollChangedListener() { 951 @Override 952 public void onScrollChanged() { 953 viewGroupCounter.incrementAndGet(); 954 } 955 }); 956 957 tlm.expectLayouts(1); 958 setRecyclerView(recyclerView); 959 tlm.waitForLayout(2); 960 961 assertEquals("RV on scroll should be called for initialization", 1, rvCounter.get()); 962 assertEquals("VTO on scroll should be called for initialization", 1, 963 viewGroupCounter.get()); 964 tlm.expectLayouts(1); 965 freezeLayout(true); 966 scrollToPosition(3); 967 tlm.assertNoLayout("scrollToPosition should be ignored", 2); 968 freezeLayout(false); 969 scrollToPosition(3); 970 tlm.waitForLayout(2); 971 assertEquals("RV on scroll should be called", 2, rvCounter.get()); 972 assertEquals("VTO on scroll should be called", 2, viewGroupCounter.get()); 973 tlm.expectLayouts(1); 974 requestLayoutOnUIThread(recyclerView); 975 tlm.waitForLayout(2); 976 // wait for draw :/ 977 Thread.sleep(1000); 978 assertEquals("on scroll should NOT be called", 2, rvCounter.get()); 979 assertEquals("on scroll should NOT be called", 2, viewGroupCounter.get()); 980 } 981 982 @Test 983 public void scrollCallbackFromEmptyToSome() throws Throwable { 984 scrollCallbackOnVisibleRangeChange(1, new int[]{0, 0}, new int[]{0, 1}); 985 } 986 987 @Test 988 public void scrollCallbackOnVisibleRangeExpand() throws Throwable { 989 scrollCallbackOnVisibleRangeChange(10, new int[]{3, 5}, new int[]{3, 6}); 990 } 991 992 @Test 993 public void scrollCallbackOnVisibleRangeShrink() throws Throwable { 994 scrollCallbackOnVisibleRangeChange(10, new int[]{3, 6}, new int[]{3, 5}); 995 } 996 997 @Test 998 public void scrollCallbackOnVisibleRangeExpand2() throws Throwable { 999 scrollCallbackOnVisibleRangeChange(10, new int[]{3, 5}, new int[]{2, 5}); 1000 } 1001 1002 @Test 1003 public void scrollCallbackOnVisibleRangeShrink2() throws Throwable { 1004 scrollCallbackOnVisibleRangeChange(10, new int[]{3, 6}, new int[]{2, 6}); 1005 } 1006 1007 private void scrollCallbackOnVisibleRangeChange(int itemCount, final int[] beforeRange, 1008 final int[] afterRange) throws Throwable { 1009 RecyclerView recyclerView = new RecyclerView(getActivity()) { 1010 @Override 1011 void dispatchLayout() { 1012 super.dispatchLayout(); 1013 ((TestLayoutManager) getLayoutManager()).layoutLatch.countDown(); 1014 } 1015 }; 1016 final AtomicBoolean beforeState = new AtomicBoolean(true); 1017 TestLayoutManager tlm = new TestLayoutManager() { 1018 @Override 1019 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1020 detachAndScrapAttachedViews(recycler); 1021 int[] range = beforeState.get() ? beforeRange : afterRange; 1022 layoutRange(recycler, range[0], range[1]); 1023 } 1024 }; 1025 recyclerView.setLayoutManager(tlm); 1026 final TestAdapter adapter = new TestAdapter(itemCount); 1027 recyclerView.setAdapter(adapter); 1028 tlm.expectLayouts(1); 1029 setRecyclerView(recyclerView); 1030 tlm.waitForLayout(1); 1031 1032 RecyclerView.OnScrollListener mockListener = mock(RecyclerView.OnScrollListener.class); 1033 recyclerView.addOnScrollListener(mockListener); 1034 verify(mockListener, never()).onScrolled(any(RecyclerView.class), anyInt(), anyInt()); 1035 1036 tlm.expectLayouts(1); 1037 beforeState.set(false); 1038 requestLayoutOnUIThread(recyclerView); 1039 tlm.waitForLayout(2); 1040 checkForMainThreadException(); 1041 verify(mockListener).onScrolled(recyclerView, 0, 0); 1042 } 1043 1044 @Test 1045 public void addItemOnScroll() throws Throwable { 1046 RecyclerView recyclerView = new RecyclerView(getActivity()); 1047 final AtomicInteger start = new AtomicInteger(0); 1048 TestLayoutManager tlm = new TestLayoutManager() { 1049 @Override 1050 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1051 layoutRange(recycler, start.get(), start.get() + 10); 1052 layoutLatch.countDown(); 1053 } 1054 }; 1055 recyclerView.setLayoutManager(tlm); 1056 final TestAdapter adapter = new TestAdapter(100); 1057 recyclerView.setAdapter(adapter); 1058 tlm.expectLayouts(1); 1059 setRecyclerView(recyclerView); 1060 tlm.waitForLayout(1); 1061 final Throwable[] error = new Throwable[1]; 1062 final AtomicBoolean calledOnScroll = new AtomicBoolean(false); 1063 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 1064 @Override 1065 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 1066 super.onScrolled(recyclerView, dx, dy); 1067 calledOnScroll.set(true); 1068 try { 1069 adapter.addAndNotify(5, 20); 1070 } catch (Throwable throwable) { 1071 error[0] = throwable; 1072 } 1073 } 1074 }); 1075 start.set(4); 1076 MatcherAssert.assertThat("test sanity", calledOnScroll.get(), CoreMatchers.is(false)); 1077 tlm.expectLayouts(1); 1078 requestLayoutOnUIThread(recyclerView); 1079 tlm.waitForLayout(2); 1080 checkForMainThreadException(); 1081 MatcherAssert.assertThat("test sanity", calledOnScroll.get(), CoreMatchers.is(true)); 1082 MatcherAssert.assertThat(error[0], CoreMatchers.nullValue()); 1083 } 1084 1085 @Test 1086 public void scrollInBothDirectionEqual() throws Throwable { 1087 scrollInBothDirection(3, 3, 1000, 1000); 1088 } 1089 1090 @Test 1091 public void scrollInBothDirectionMoreVertical() throws Throwable { 1092 scrollInBothDirection(2, 3, 1000, 1000); 1093 } 1094 1095 @Test 1096 public void scrollInBothDirectionMoreHorizontal() throws Throwable { 1097 scrollInBothDirection(3, 2, 1000, 1000); 1098 } 1099 1100 @Test 1101 public void scrollHorizontalOnly() throws Throwable { 1102 scrollInBothDirection(3, 0, 1000, 0); 1103 } 1104 1105 @Test 1106 public void scrollVerticalOnly() throws Throwable { 1107 scrollInBothDirection(0, 3, 0, 1000); 1108 } 1109 1110 @Test 1111 public void scrollInBothDirectionEqualReverse() throws Throwable { 1112 scrollInBothDirection(3, 3, -1000, -1000); 1113 } 1114 1115 @Test 1116 public void scrollInBothDirectionMoreVerticalReverse() throws Throwable { 1117 scrollInBothDirection(2, 3, -1000, -1000); 1118 } 1119 1120 @Test 1121 public void scrollInBothDirectionMoreHorizontalReverse() throws Throwable { 1122 scrollInBothDirection(3, 2, -1000, -1000); 1123 } 1124 1125 @Test 1126 public void scrollHorizontalOnlyReverse() throws Throwable { 1127 scrollInBothDirection(3, 0, -1000, 0); 1128 } 1129 1130 @Test 1131 public void scrollVerticalOnlyReverse() throws Throwable { 1132 scrollInBothDirection(0, 3, 0, -1000); 1133 } 1134 1135 public void scrollInBothDirection(int horizontalScrollCount, int verticalScrollCount, 1136 int horizontalVelocity, int verticalVelocity) 1137 throws Throwable { 1138 RecyclerView recyclerView = new RecyclerView(getActivity()); 1139 final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount); 1140 final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount); 1141 TestLayoutManager tlm = new TestLayoutManager() { 1142 @Override 1143 public boolean canScrollHorizontally() { 1144 return true; 1145 } 1146 1147 @Override 1148 public boolean canScrollVertically() { 1149 return true; 1150 } 1151 1152 @Override 1153 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1154 layoutRange(recycler, 0, 10); 1155 layoutLatch.countDown(); 1156 } 1157 1158 @Override 1159 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 1160 RecyclerView.State state) { 1161 if (verticalCounter.get() > 0) { 1162 verticalCounter.decrementAndGet(); 1163 return dy; 1164 } 1165 return 0; 1166 } 1167 1168 @Override 1169 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 1170 RecyclerView.State state) { 1171 if (horizontalCounter.get() > 0) { 1172 horizontalCounter.decrementAndGet(); 1173 return dx; 1174 } 1175 return 0; 1176 } 1177 }; 1178 TestAdapter adapter = new TestAdapter(100); 1179 recyclerView.setAdapter(adapter); 1180 recyclerView.setLayoutManager(tlm); 1181 tlm.expectLayouts(1); 1182 setRecyclerView(recyclerView); 1183 tlm.waitForLayout(2); 1184 assertTrue("test sanity, fling must run", fling(horizontalVelocity, verticalVelocity)); 1185 assertEquals("rv's horizontal scroll cb must run " + horizontalScrollCount + " times'", 0, 1186 horizontalCounter.get()); 1187 assertEquals("rv's vertical scroll cb must run " + verticalScrollCount + " times'", 0, 1188 verticalCounter.get()); 1189 } 1190 1191 @Test 1192 public void dragHorizontal() throws Throwable { 1193 scrollInOtherOrientationTest(FLAG_HORIZONTAL); 1194 } 1195 1196 @Test 1197 public void dragVertical() throws Throwable { 1198 scrollInOtherOrientationTest(FLAG_VERTICAL); 1199 } 1200 1201 @Test 1202 public void flingHorizontal() throws Throwable { 1203 scrollInOtherOrientationTest(FLAG_HORIZONTAL | FLAG_FLING); 1204 } 1205 1206 @Test 1207 public void flingVertical() throws Throwable { 1208 scrollInOtherOrientationTest(FLAG_VERTICAL | FLAG_FLING); 1209 } 1210 1211 @Test 1212 public void nestedDragVertical() throws Throwable { 1213 TestedFrameLayout tfl = getActivity().getContainer(); 1214 tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 1215 tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 1216 scrollInOtherOrientationTest(FLAG_VERTICAL, 0); 1217 } 1218 1219 @Test 1220 public void nestedDragHorizontal() throws Throwable { 1221 TestedFrameLayout tfl = getActivity().getContainer(); 1222 tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 1223 tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 1224 scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0); 1225 } 1226 1227 @Test 1228 public void nestedDragHorizontalCallsStopNestedScroll() throws Throwable { 1229 TestedFrameLayout tfl = getActivity().getContainer(); 1230 tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 1231 tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 1232 scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0); 1233 assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled()); 1234 } 1235 1236 @Test 1237 public void nestedDragVerticalCallsStopNestedScroll() throws Throwable { 1238 TestedFrameLayout tfl = getActivity().getContainer(); 1239 tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 1240 tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 1241 scrollInOtherOrientationTest(FLAG_VERTICAL, 0); 1242 assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled()); 1243 } 1244 1245 private void scrollInOtherOrientationTest(int flags) 1246 throws Throwable { 1247 scrollInOtherOrientationTest(flags, flags); 1248 } 1249 1250 private void scrollInOtherOrientationTest(final int flags, int expectedFlags) throws Throwable { 1251 RecyclerView recyclerView = new RecyclerView(getActivity()); 1252 final AtomicBoolean scrolledHorizontal = new AtomicBoolean(false); 1253 final AtomicBoolean scrolledVertical = new AtomicBoolean(false); 1254 1255 final TestLayoutManager tlm = new TestLayoutManager() { 1256 @Override 1257 public boolean canScrollHorizontally() { 1258 return (flags & FLAG_HORIZONTAL) != 0; 1259 } 1260 1261 @Override 1262 public boolean canScrollVertically() { 1263 return (flags & FLAG_VERTICAL) != 0; 1264 } 1265 1266 @Override 1267 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1268 layoutRange(recycler, 0, 10); 1269 layoutLatch.countDown(); 1270 } 1271 1272 @Override 1273 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 1274 RecyclerView.State state) { 1275 scrolledVertical.set(true); 1276 return super.scrollVerticallyBy(dy, recycler, state); 1277 } 1278 1279 @Override 1280 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 1281 RecyclerView.State state) { 1282 scrolledHorizontal.set(true); 1283 return super.scrollHorizontallyBy(dx, recycler, state); 1284 } 1285 }; 1286 TestAdapter adapter = new TestAdapter(100); 1287 recyclerView.setAdapter(adapter); 1288 recyclerView.setLayoutManager(tlm); 1289 tlm.expectLayouts(1); 1290 setRecyclerView(recyclerView); 1291 tlm.waitForLayout(2); 1292 if ( (flags & FLAG_FLING) != 0 ) { 1293 int flingVelocity = (mRecyclerView.getMaxFlingVelocity() + 1294 mRecyclerView.getMinFlingVelocity()) / 2; 1295 assertEquals("fling started", (expectedFlags & FLAG_FLING) != 0, 1296 fling(flingVelocity, flingVelocity)); 1297 } else { // drag 1298 TouchUtils.dragViewTo(getInstrumentation(), recyclerView, Gravity.LEFT | Gravity.TOP, 1299 mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); 1300 } 1301 assertEquals("horizontally scrolled: " + tlm.mScrollHorizontallyAmount, 1302 (expectedFlags & FLAG_HORIZONTAL) != 0, scrolledHorizontal.get()); 1303 assertEquals("vertically scrolled: " + tlm.mScrollVerticallyAmount, 1304 (expectedFlags & FLAG_VERTICAL) != 0, scrolledVertical.get()); 1305 } 1306 1307 private boolean fling(final int velocityX, final int velocityY) throws Throwable { 1308 final AtomicBoolean didStart = new AtomicBoolean(false); 1309 runTestOnUiThread(new Runnable() { 1310 @Override 1311 public void run() { 1312 boolean result = mRecyclerView.fling(velocityX, velocityY); 1313 didStart.set(result); 1314 } 1315 }); 1316 if (!didStart.get()) { 1317 return false; 1318 } 1319 waitForIdleScroll(mRecyclerView); 1320 return true; 1321 } 1322 1323 private void assertPendingUpdatesAndLayoutTest(final AdapterRunnable runnable) throws Throwable { 1324 RecyclerView recyclerView = new RecyclerView(getActivity()); 1325 TestLayoutManager layoutManager = new DumbLayoutManager(); 1326 final TestAdapter testAdapter = new TestAdapter(10); 1327 setupBasic(recyclerView, layoutManager, testAdapter, false); 1328 layoutManager.expectLayouts(1); 1329 runTestOnUiThread(new Runnable() { 1330 @Override 1331 public void run() { 1332 try { 1333 runnable.run(testAdapter); 1334 } catch (Throwable throwable) { 1335 fail("runnable has thrown an exception"); 1336 } 1337 assertTrue(mRecyclerView.hasPendingAdapterUpdates()); 1338 } 1339 }); 1340 layoutManager.waitForLayout(1); 1341 assertFalse(mRecyclerView.hasPendingAdapterUpdates()); 1342 checkForMainThreadException(); 1343 } 1344 1345 private void setupBasic(RecyclerView recyclerView, TestLayoutManager tlm, 1346 TestAdapter adapter, boolean waitForFirstLayout) throws Throwable { 1347 recyclerView.setLayoutManager(tlm); 1348 recyclerView.setAdapter(adapter); 1349 if (waitForFirstLayout) { 1350 tlm.expectLayouts(1); 1351 setRecyclerView(recyclerView); 1352 tlm.waitForLayout(1); 1353 } else { 1354 setRecyclerView(recyclerView); 1355 } 1356 } 1357 1358 @Test 1359 public void hasPendingUpdatesBeforeFirstLayout() throws Throwable { 1360 RecyclerView recyclerView = new RecyclerView(getActivity()); 1361 TestLayoutManager layoutManager = new DumbLayoutManager(); 1362 TestAdapter testAdapter = new TestAdapter(10); 1363 setupBasic(recyclerView, layoutManager, testAdapter, false); 1364 assertTrue(mRecyclerView.hasPendingAdapterUpdates()); 1365 } 1366 1367 @Test 1368 public void noPendingUpdatesAfterLayout() throws Throwable { 1369 RecyclerView recyclerView = new RecyclerView(getActivity()); 1370 TestLayoutManager layoutManager = new DumbLayoutManager(); 1371 TestAdapter testAdapter = new TestAdapter(10); 1372 setupBasic(recyclerView, layoutManager, testAdapter, true); 1373 assertFalse(mRecyclerView.hasPendingAdapterUpdates()); 1374 } 1375 1376 @Test 1377 public void hasPendingUpdatesAfterItemIsRemoved() throws Throwable { 1378 assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { 1379 @Override 1380 public void run(TestAdapter testAdapter) throws Throwable { 1381 testAdapter.deleteAndNotify(1, 1); 1382 } 1383 }); 1384 } 1385 @Test 1386 public void hasPendingUpdatesAfterItemIsInserted() throws Throwable { 1387 assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { 1388 @Override 1389 public void run(TestAdapter testAdapter) throws Throwable { 1390 testAdapter.addAndNotify(2, 1); 1391 } 1392 }); 1393 } 1394 @Test 1395 public void hasPendingUpdatesAfterItemIsMoved() throws Throwable { 1396 assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { 1397 @Override 1398 public void run(TestAdapter testAdapter) throws Throwable { 1399 testAdapter.moveItem(2, 3, true); 1400 } 1401 }); 1402 } 1403 @Test 1404 public void hasPendingUpdatesAfterItemIsChanged() throws Throwable { 1405 assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { 1406 @Override 1407 public void run(TestAdapter testAdapter) throws Throwable { 1408 testAdapter.changeAndNotify(2, 1); 1409 } 1410 }); 1411 } 1412 @Test 1413 public void hasPendingUpdatesAfterDataSetIsChanged() throws Throwable { 1414 assertPendingUpdatesAndLayoutTest(new AdapterRunnable() { 1415 @Override 1416 public void run(TestAdapter testAdapter) { 1417 mRecyclerView.getAdapter().notifyDataSetChanged(); 1418 } 1419 }); 1420 } 1421 1422 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) 1423 @Test 1424 public void transientStateRecycleViaAdapter() throws Throwable { 1425 transientStateRecycleTest(true, false); 1426 } 1427 1428 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) 1429 @Test 1430 public void transientStateRecycleViaTransientStateCleanup() throws Throwable { 1431 transientStateRecycleTest(false, true); 1432 } 1433 1434 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) 1435 @Test 1436 public void transientStateDontRecycle() throws Throwable { 1437 transientStateRecycleTest(false, false); 1438 } 1439 1440 public void transientStateRecycleTest(final boolean succeed, final boolean unsetTransientState) 1441 throws Throwable { 1442 final List<View> failedToRecycle = new ArrayList<>(); 1443 final List<View> recycled = new ArrayList<>(); 1444 TestAdapter testAdapter = new TestAdapter(10) { 1445 @Override 1446 public boolean onFailedToRecycleView( 1447 TestViewHolder holder) { 1448 failedToRecycle.add(holder.itemView); 1449 if (unsetTransientState) { 1450 setHasTransientState(holder.itemView, false); 1451 } 1452 return succeed; 1453 } 1454 1455 @Override 1456 public void onViewRecycled(TestViewHolder holder) { 1457 recycled.add(holder.itemView); 1458 super.onViewRecycled(holder); 1459 } 1460 }; 1461 TestLayoutManager tlm = new TestLayoutManager() { 1462 @Override 1463 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1464 if (getChildCount() == 0) { 1465 detachAndScrapAttachedViews(recycler); 1466 layoutRange(recycler, 0, 5); 1467 } else { 1468 removeAndRecycleAllViews(recycler); 1469 } 1470 if (layoutLatch != null) { 1471 layoutLatch.countDown(); 1472 } 1473 } 1474 }; 1475 RecyclerView recyclerView = new RecyclerView(getActivity()); 1476 recyclerView.setAdapter(testAdapter); 1477 recyclerView.setLayoutManager(tlm); 1478 recyclerView.setItemAnimator(null); 1479 setRecyclerView(recyclerView); 1480 getInstrumentation().waitForIdleSync(); 1481 // make sure we have enough views after this position so that we'll receive the on recycled 1482 // callback 1483 View view = recyclerView.getChildAt(3);//this has to be greater than def cache size. 1484 setHasTransientState(view, true); 1485 tlm.expectLayouts(1); 1486 requestLayoutOnUIThread(recyclerView); 1487 tlm.waitForLayout(2); 1488 1489 assertTrue(failedToRecycle.contains(view)); 1490 assertEquals(succeed || unsetTransientState, recycled.contains(view)); 1491 } 1492 1493 @Test 1494 public void adapterPositionInvalidation() throws Throwable { 1495 final RecyclerView recyclerView = new RecyclerView(getActivity()); 1496 final TestAdapter adapter = new TestAdapter(10); 1497 final TestLayoutManager tlm = new TestLayoutManager() { 1498 @Override 1499 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1500 layoutRange(recycler, 0, state.getItemCount()); 1501 layoutLatch.countDown(); 1502 } 1503 }; 1504 recyclerView.setAdapter(adapter); 1505 recyclerView.setLayoutManager(tlm); 1506 tlm.expectLayouts(1); 1507 setRecyclerView(recyclerView); 1508 tlm.waitForLayout(1); 1509 runTestOnUiThread(new Runnable() { 1510 @Override 1511 public void run() { 1512 for (int i = 0; i < tlm.getChildCount(); i++) { 1513 assertNotSame("adapter positions should not be undefined", 1514 recyclerView.getChildAdapterPosition(tlm.getChildAt(i)), 1515 RecyclerView.NO_POSITION); 1516 } 1517 adapter.notifyDataSetChanged(); 1518 for (int i = 0; i < tlm.getChildCount(); i++) { 1519 assertSame("adapter positions should be undefined", 1520 recyclerView.getChildAdapterPosition(tlm.getChildAt(i)), 1521 RecyclerView.NO_POSITION); 1522 } 1523 } 1524 }); 1525 } 1526 1527 @Test 1528 public void adapterPositionsBasic() throws Throwable { 1529 adapterPositionsTest(null); 1530 } 1531 1532 @Test 1533 public void adapterPositionsRemoveItems() throws Throwable { 1534 adapterPositionsTest(new AdapterRunnable() { 1535 @Override 1536 public void run(TestAdapter adapter) throws Throwable { 1537 adapter.deleteAndNotify(3, 4); 1538 } 1539 }); 1540 } 1541 1542 @Test 1543 public void adapterPositionsRemoveItemsBefore() throws Throwable { 1544 adapterPositionsTest(new AdapterRunnable() { 1545 @Override 1546 public void run(TestAdapter adapter) throws Throwable { 1547 adapter.deleteAndNotify(0, 1); 1548 } 1549 }); 1550 } 1551 1552 @Test 1553 public void adapterPositionsAddItemsBefore() throws Throwable { 1554 adapterPositionsTest(new AdapterRunnable() { 1555 @Override 1556 public void run(TestAdapter adapter) throws Throwable { 1557 adapter.addAndNotify(0, 5); 1558 } 1559 }); 1560 } 1561 1562 @Test 1563 public void adapterPositionsAddItemsInside() throws Throwable { 1564 adapterPositionsTest(new AdapterRunnable() { 1565 @Override 1566 public void run(TestAdapter adapter) throws Throwable { 1567 adapter.addAndNotify(3, 2); 1568 } 1569 }); 1570 } 1571 1572 @Test 1573 public void adapterPositionsMoveItems() throws Throwable { 1574 adapterPositionsTest(new AdapterRunnable() { 1575 @Override 1576 public void run(TestAdapter adapter) throws Throwable { 1577 adapter.moveAndNotify(3, 5); 1578 } 1579 }); 1580 } 1581 1582 @Test 1583 public void adapterPositionsNotifyDataSetChanged() throws Throwable { 1584 adapterPositionsTest(new AdapterRunnable() { 1585 @Override 1586 public void run(TestAdapter adapter) throws Throwable { 1587 adapter.mItems.clear(); 1588 for (int i = 0; i < 20; i++) { 1589 adapter.mItems.add(new Item(i, "added item")); 1590 } 1591 adapter.notifyDataSetChanged(); 1592 } 1593 }); 1594 } 1595 1596 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) // transientState is API 16 1597 @Test 1598 public void avoidLeakingRecyclerViewIfViewIsNotRecycled() throws Throwable { 1599 final AtomicBoolean failedToRecycle = new AtomicBoolean(false); 1600 final AtomicInteger recycledViewCount = new AtomicInteger(0); 1601 RecyclerView rv = new RecyclerView(getActivity()); 1602 TestLayoutManager tlm = new TestLayoutManager() { 1603 @Override 1604 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1605 detachAndScrapAttachedViews(recycler); 1606 layoutRange(recycler, 0, state.getItemCount()); 1607 layoutLatch.countDown(); 1608 } 1609 }; 1610 TestAdapter adapter = new TestAdapter(10) { 1611 @Override 1612 public boolean onFailedToRecycleView( 1613 TestViewHolder holder) { 1614 failedToRecycle.set(true); 1615 return false; 1616 } 1617 1618 @Override 1619 public void onViewRecycled(TestViewHolder holder) { 1620 recycledViewCount.incrementAndGet(); 1621 super.onViewRecycled(holder); 1622 } 1623 }; 1624 rv.setAdapter(adapter); 1625 rv.setLayoutManager(tlm); 1626 tlm.expectLayouts(1); 1627 setRecyclerView(rv); 1628 tlm.waitForLayout(1); 1629 final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0)); 1630 runTestOnUiThread(new Runnable() { 1631 @Override 1632 public void run() { 1633 ViewCompat.setHasTransientState(vh.itemView, true); 1634 } 1635 }); 1636 tlm.expectLayouts(1); 1637 adapter.deleteAndNotify(0, 10); 1638 tlm.waitForLayout(2); 1639 final CountDownLatch animationsLatch = new CountDownLatch(1); 1640 rv.getItemAnimator().isRunning( 1641 new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 1642 @Override 1643 public void onAnimationsFinished() { 1644 animationsLatch.countDown(); 1645 } 1646 }); 1647 assertTrue(animationsLatch.await(2, TimeUnit.SECONDS)); 1648 assertThat(recycledViewCount.get(), is(9)); 1649 assertTrue(failedToRecycle.get()); 1650 assertNull(vh.mOwnerRecyclerView); 1651 checkForMainThreadException(); 1652 } 1653 1654 @Test 1655 public void avoidLeakingRecyclerViewViaViewHolder() throws Throwable { 1656 RecyclerView rv = new RecyclerView(getActivity()); 1657 TestLayoutManager tlm = new TestLayoutManager() { 1658 @Override 1659 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1660 detachAndScrapAttachedViews(recycler); 1661 layoutRange(recycler, 0, state.getItemCount()); 1662 layoutLatch.countDown(); 1663 } 1664 }; 1665 TestAdapter adapter = new TestAdapter(10); 1666 rv.setAdapter(adapter); 1667 rv.setLayoutManager(tlm); 1668 tlm.expectLayouts(1); 1669 setRecyclerView(rv); 1670 tlm.waitForLayout(1); 1671 final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0)); 1672 tlm.expectLayouts(1); 1673 adapter.deleteAndNotify(0, 10); 1674 tlm.waitForLayout(2); 1675 final CountDownLatch animationsLatch = new CountDownLatch(1); 1676 rv.getItemAnimator().isRunning( 1677 new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 1678 @Override 1679 public void onAnimationsFinished() { 1680 animationsLatch.countDown(); 1681 } 1682 }); 1683 assertTrue(animationsLatch.await(2, TimeUnit.SECONDS)); 1684 assertNull(vh.mOwnerRecyclerView); 1685 checkForMainThreadException(); 1686 } 1687 1688 @Test 1689 public void duplicateAdapterPositionTest() throws Throwable { 1690 final TestAdapter testAdapter = new TestAdapter(10); 1691 final TestLayoutManager tlm = new TestLayoutManager() { 1692 @Override 1693 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1694 detachAndScrapAttachedViews(recycler); 1695 layoutRange(recycler, 0, state.getItemCount()); 1696 if (!state.isPreLayout()) { 1697 while (!recycler.getScrapList().isEmpty()) { 1698 RecyclerView.ViewHolder viewHolder = recycler.getScrapList().get(0); 1699 addDisappearingView(viewHolder.itemView, 0); 1700 } 1701 } 1702 layoutLatch.countDown(); 1703 } 1704 1705 @Override 1706 public boolean supportsPredictiveItemAnimations() { 1707 return true; 1708 } 1709 }; 1710 final DefaultItemAnimator animator = new DefaultItemAnimator(); 1711 animator.setSupportsChangeAnimations(true); 1712 animator.setChangeDuration(10000); 1713 testAdapter.setHasStableIds(true); 1714 final TestRecyclerView recyclerView = new TestRecyclerView(getActivity()); 1715 recyclerView.setLayoutManager(tlm); 1716 recyclerView.setAdapter(testAdapter); 1717 recyclerView.setItemAnimator(animator); 1718 1719 tlm.expectLayouts(1); 1720 setRecyclerView(recyclerView); 1721 tlm.waitForLayout(2); 1722 1723 tlm.expectLayouts(2); 1724 testAdapter.mItems.get(2).mType += 2; 1725 final int itemId = testAdapter.mItems.get(2).mId; 1726 testAdapter.changeAndNotify(2, 1); 1727 tlm.waitForLayout(2); 1728 1729 runTestOnUiThread(new Runnable() { 1730 @Override 1731 public void run() { 1732 assertThat("test sanity", recyclerView.getChildCount(), CoreMatchers.is(11)); 1733 // now mangle the order and run the test 1734 RecyclerView.ViewHolder hidden = null; 1735 RecyclerView.ViewHolder updated = null; 1736 for (int i = 0; i < recyclerView.getChildCount(); i ++) { 1737 View view = recyclerView.getChildAt(i); 1738 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); 1739 if (vh.getAdapterPosition() == 2) { 1740 if (mRecyclerView.mChildHelper.isHidden(view)) { 1741 assertThat(hidden, CoreMatchers.nullValue()); 1742 hidden = vh; 1743 } else { 1744 assertThat(updated, CoreMatchers.nullValue()); 1745 updated = vh; 1746 } 1747 } 1748 } 1749 assertThat(hidden, CoreMatchers.notNullValue()); 1750 assertThat(updated, CoreMatchers.notNullValue()); 1751 1752 mRecyclerView.eatRequestLayout(); 1753 1754 // first put the hidden child back 1755 int index1 = mRecyclerView.indexOfChild(hidden.itemView); 1756 int index2 = mRecyclerView.indexOfChild(updated.itemView); 1757 if (index1 < index2) { 1758 // swap views 1759 swapViewsAtIndices(recyclerView, index1, index2); 1760 } 1761 assertThat(tlm.findViewByPosition(2), CoreMatchers.sameInstance(updated.itemView)); 1762 1763 assertThat(recyclerView.findViewHolderForAdapterPosition(2), 1764 CoreMatchers.sameInstance(updated)); 1765 assertThat(recyclerView.findViewHolderForLayoutPosition(2), 1766 CoreMatchers.sameInstance(updated)); 1767 assertThat(recyclerView.findViewHolderForItemId(itemId), 1768 CoreMatchers.sameInstance(updated)); 1769 1770 // now swap back 1771 swapViewsAtIndices(recyclerView, index1, index2); 1772 1773 assertThat(tlm.findViewByPosition(2), CoreMatchers.sameInstance(updated.itemView)); 1774 assertThat(recyclerView.findViewHolderForAdapterPosition(2), 1775 CoreMatchers.sameInstance(updated)); 1776 assertThat(recyclerView.findViewHolderForLayoutPosition(2), 1777 CoreMatchers.sameInstance(updated)); 1778 assertThat(recyclerView.findViewHolderForItemId(itemId), 1779 CoreMatchers.sameInstance(updated)); 1780 1781 // now remove updated. re-assert fallback to the hidden one 1782 tlm.removeView(updated.itemView); 1783 1784 assertThat(tlm.findViewByPosition(2), CoreMatchers.nullValue()); 1785 assertThat(recyclerView.findViewHolderForAdapterPosition(2), 1786 CoreMatchers.sameInstance(hidden)); 1787 assertThat(recyclerView.findViewHolderForLayoutPosition(2), 1788 CoreMatchers.sameInstance(hidden)); 1789 assertThat(recyclerView.findViewHolderForItemId(itemId), 1790 CoreMatchers.sameInstance(hidden)); 1791 } 1792 }); 1793 1794 } 1795 1796 private void swapViewsAtIndices(TestRecyclerView recyclerView, int index1, int index2) { 1797 if (index1 == index2) { 1798 return; 1799 } 1800 if (index2 < index1) { 1801 int tmp = index1; 1802 index1 = index2; 1803 index2 = tmp; 1804 } 1805 final View v1 = recyclerView.getChildAt(index1); 1806 final View v2 = recyclerView.getChildAt(index2); 1807 boolean v1Hidden = recyclerView.mChildHelper.isHidden(v1); 1808 boolean v2Hidden = recyclerView.mChildHelper.isHidden(v2); 1809 // must un-hide before swap otherwise bucket indices will become invalid. 1810 if (v1Hidden) { 1811 mRecyclerView.mChildHelper.unhide(v1); 1812 } 1813 if (v2Hidden) { 1814 mRecyclerView.mChildHelper.unhide(v2); 1815 } 1816 recyclerView.detachViewFromParent(index2); 1817 recyclerView.attachViewToParent(v2, index1, v2.getLayoutParams()); 1818 recyclerView.detachViewFromParent(index1 + 1); 1819 recyclerView.attachViewToParent(v1, index2, v1.getLayoutParams()); 1820 1821 if (v1Hidden) { 1822 mRecyclerView.mChildHelper.hide(v1); 1823 } 1824 if (v2Hidden) { 1825 mRecyclerView.mChildHelper.hide(v2); 1826 } 1827 } 1828 1829 public void adapterPositionsTest(final AdapterRunnable adapterChanges) throws Throwable { 1830 final TestAdapter testAdapter = new TestAdapter(10); 1831 TestLayoutManager tlm = new TestLayoutManager() { 1832 @Override 1833 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1834 try { 1835 layoutRange(recycler, Math.min(state.getItemCount(), 2) 1836 , Math.min(state.getItemCount(), 7)); 1837 layoutLatch.countDown(); 1838 } catch (Throwable t) { 1839 postExceptionToInstrumentation(t); 1840 } 1841 } 1842 }; 1843 final RecyclerView recyclerView = new RecyclerView(getActivity()); 1844 recyclerView.setLayoutManager(tlm); 1845 recyclerView.setAdapter(testAdapter); 1846 tlm.expectLayouts(1); 1847 setRecyclerView(recyclerView); 1848 tlm.waitForLayout(1); 1849 runTestOnUiThread(new Runnable() { 1850 @Override 1851 public void run() { 1852 try { 1853 final int count = recyclerView.getChildCount(); 1854 Map<View, Integer> layoutPositions = new HashMap<>(); 1855 assertTrue("test sanity", count > 0); 1856 for (int i = 0; i < count; i++) { 1857 View view = recyclerView.getChildAt(i); 1858 TestViewHolder vh = (TestViewHolder) recyclerView.getChildViewHolder(view); 1859 int index = testAdapter.mItems.indexOf(vh.mBoundItem); 1860 assertEquals("should be able to find VH with adapter position " + index, vh, 1861 recyclerView.findViewHolderForAdapterPosition(index)); 1862 assertEquals("get adapter position should return correct index", index, 1863 vh.getAdapterPosition()); 1864 layoutPositions.put(view, vh.mPosition); 1865 } 1866 if (adapterChanges != null) { 1867 adapterChanges.run(testAdapter); 1868 for (int i = 0; i < count; i++) { 1869 View view = recyclerView.getChildAt(i); 1870 TestViewHolder vh = (TestViewHolder) recyclerView 1871 .getChildViewHolder(view); 1872 int index = testAdapter.mItems.indexOf(vh.mBoundItem); 1873 if (index >= 0) { 1874 assertEquals("should be able to find VH with adapter position " 1875 + index, vh, 1876 recyclerView.findViewHolderForAdapterPosition(index)); 1877 } 1878 assertSame("get adapter position should return correct index", index, 1879 vh.getAdapterPosition()); 1880 assertSame("should be able to find view with layout position", 1881 vh, mRecyclerView.findViewHolderForLayoutPosition( 1882 layoutPositions.get(view))); 1883 } 1884 1885 } 1886 1887 } catch (Throwable t) { 1888 postExceptionToInstrumentation(t); 1889 } 1890 } 1891 }); 1892 checkForMainThreadException(); 1893 } 1894 1895 @Test 1896 public void scrollStateForSmoothScroll() throws Throwable { 1897 TestAdapter testAdapter = new TestAdapter(10); 1898 TestLayoutManager tlm = new TestLayoutManager(); 1899 RecyclerView recyclerView = new RecyclerView(getActivity()); 1900 recyclerView.setAdapter(testAdapter); 1901 recyclerView.setLayoutManager(tlm); 1902 setRecyclerView(recyclerView); 1903 getInstrumentation().waitForIdleSync(); 1904 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1905 final int[] stateCnts = new int[10]; 1906 final CountDownLatch latch = new CountDownLatch(2); 1907 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 1908 @Override 1909 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1910 stateCnts[newState] = stateCnts[newState] + 1; 1911 latch.countDown(); 1912 } 1913 }); 1914 runTestOnUiThread(new Runnable() { 1915 @Override 1916 public void run() { 1917 mRecyclerView.smoothScrollBy(0, 500); 1918 } 1919 }); 1920 latch.await(5, TimeUnit.SECONDS); 1921 assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); 1922 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 1923 assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); 1924 } 1925 1926 @Test 1927 public void scrollStateForSmoothScrollWithStop() throws Throwable { 1928 TestAdapter testAdapter = new TestAdapter(10); 1929 TestLayoutManager tlm = new TestLayoutManager(); 1930 RecyclerView recyclerView = new RecyclerView(getActivity()); 1931 recyclerView.setAdapter(testAdapter); 1932 recyclerView.setLayoutManager(tlm); 1933 setRecyclerView(recyclerView); 1934 getInstrumentation().waitForIdleSync(); 1935 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1936 final int[] stateCnts = new int[10]; 1937 final CountDownLatch latch = new CountDownLatch(1); 1938 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 1939 @Override 1940 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1941 stateCnts[newState] = stateCnts[newState] + 1; 1942 latch.countDown(); 1943 } 1944 }); 1945 runTestOnUiThread(new Runnable() { 1946 @Override 1947 public void run() { 1948 mRecyclerView.smoothScrollBy(0, 500); 1949 } 1950 }); 1951 latch.await(5, TimeUnit.SECONDS); 1952 runTestOnUiThread(new Runnable() { 1953 @Override 1954 public void run() { 1955 mRecyclerView.stopScroll(); 1956 } 1957 }); 1958 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1959 assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); 1960 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 1961 assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); 1962 } 1963 1964 @Test 1965 public void scrollStateForFling() throws Throwable { 1966 TestAdapter testAdapter = new TestAdapter(10); 1967 TestLayoutManager tlm = new TestLayoutManager(); 1968 RecyclerView recyclerView = new RecyclerView(getActivity()); 1969 recyclerView.setAdapter(testAdapter); 1970 recyclerView.setLayoutManager(tlm); 1971 setRecyclerView(recyclerView); 1972 getInstrumentation().waitForIdleSync(); 1973 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1974 final int[] stateCnts = new int[10]; 1975 final CountDownLatch latch = new CountDownLatch(2); 1976 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 1977 @Override 1978 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1979 stateCnts[newState] = stateCnts[newState] + 1; 1980 latch.countDown(); 1981 } 1982 }); 1983 final ViewConfiguration vc = ViewConfiguration.get(getActivity()); 1984 final float fling = vc.getScaledMinimumFlingVelocity() 1985 + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .1f; 1986 runTestOnUiThread(new Runnable() { 1987 @Override 1988 public void run() { 1989 mRecyclerView.fling(0, Math.round(fling)); 1990 } 1991 }); 1992 latch.await(5, TimeUnit.SECONDS); 1993 assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); 1994 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 1995 assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); 1996 } 1997 1998 @Test 1999 public void scrollStateForFlingWithStop() throws Throwable { 2000 TestAdapter testAdapter = new TestAdapter(10); 2001 TestLayoutManager tlm = new TestLayoutManager(); 2002 RecyclerView recyclerView = new RecyclerView(getActivity()); 2003 recyclerView.setAdapter(testAdapter); 2004 recyclerView.setLayoutManager(tlm); 2005 setRecyclerView(recyclerView); 2006 getInstrumentation().waitForIdleSync(); 2007 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 2008 final int[] stateCnts = new int[10]; 2009 final CountDownLatch latch = new CountDownLatch(1); 2010 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 2011 @Override 2012 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 2013 stateCnts[newState] = stateCnts[newState] + 1; 2014 latch.countDown(); 2015 } 2016 }); 2017 final ViewConfiguration vc = ViewConfiguration.get(getActivity()); 2018 final float fling = vc.getScaledMinimumFlingVelocity() 2019 + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .8f; 2020 runTestOnUiThread(new Runnable() { 2021 @Override 2022 public void run() { 2023 mRecyclerView.fling(0, Math.round(fling)); 2024 } 2025 }); 2026 latch.await(5, TimeUnit.SECONDS); 2027 runTestOnUiThread(new Runnable() { 2028 @Override 2029 public void run() { 2030 mRecyclerView.stopScroll(); 2031 } 2032 }); 2033 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 2034 assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); 2035 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 2036 assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); 2037 } 2038 2039 @Test 2040 public void scrollStateDrag() throws Throwable { 2041 TestAdapter testAdapter = new TestAdapter(10); 2042 TestLayoutManager tlm = new TestLayoutManager(); 2043 RecyclerView recyclerView = new RecyclerView(getActivity()); 2044 recyclerView.setAdapter(testAdapter); 2045 recyclerView.setLayoutManager(tlm); 2046 setRecyclerView(recyclerView); 2047 getInstrumentation().waitForIdleSync(); 2048 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 2049 final int[] stateCnts = new int[10]; 2050 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 2051 @Override 2052 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 2053 stateCnts[newState] = stateCnts[newState] + 1; 2054 } 2055 }); 2056 drag(mRecyclerView, 0, 0, 0, 500, 5); 2057 assertEquals(0, stateCnts[SCROLL_STATE_SETTLING]); 2058 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 2059 assertEquals(1, stateCnts[SCROLL_STATE_DRAGGING]); 2060 } 2061 2062 public void drag(ViewGroup view, float fromX, float toX, float fromY, float toY, 2063 int stepCount) throws Throwable { 2064 long downTime = SystemClock.uptimeMillis(); 2065 long eventTime = SystemClock.uptimeMillis(); 2066 2067 float y = fromY; 2068 float x = fromX; 2069 2070 float yStep = (toY - fromY) / stepCount; 2071 float xStep = (toX - fromX) / stepCount; 2072 2073 MotionEvent event = MotionEvent.obtain(downTime, eventTime, 2074 MotionEvent.ACTION_DOWN, x, y, 0); 2075 sendTouch(view, event); 2076 for (int i = 0; i < stepCount; ++i) { 2077 y += yStep; 2078 x += xStep; 2079 eventTime = SystemClock.uptimeMillis(); 2080 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); 2081 sendTouch(view, event); 2082 } 2083 2084 eventTime = SystemClock.uptimeMillis(); 2085 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); 2086 sendTouch(view, event); 2087 getInstrumentation().waitForIdleSync(); 2088 } 2089 2090 private void sendTouch(final ViewGroup view, final MotionEvent event) throws Throwable { 2091 runTestOnUiThread(new Runnable() { 2092 @Override 2093 public void run() { 2094 if (view.onInterceptTouchEvent(event)) { 2095 view.onTouchEvent(event); 2096 } 2097 } 2098 }); 2099 } 2100 2101 @Test 2102 public void recycleScrap() throws Throwable { 2103 recycleScrapTest(false); 2104 removeRecyclerView(); 2105 recycleScrapTest(true); 2106 } 2107 2108 public void recycleScrapTest(final boolean useRecycler) throws Throwable { 2109 TestAdapter testAdapter = new TestAdapter(10); 2110 final AtomicBoolean test = new AtomicBoolean(false); 2111 TestLayoutManager lm = new TestLayoutManager() { 2112 @Override 2113 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2114 ViewInfoStore infoStore = mRecyclerView.mViewInfoStore; 2115 if (test.get()) { 2116 try { 2117 detachAndScrapAttachedViews(recycler); 2118 for (int i = recycler.getScrapList().size() - 1; i >= 0; i--) { 2119 if (useRecycler) { 2120 recycler.recycleView(recycler.getScrapList().get(i).itemView); 2121 } else { 2122 removeAndRecycleView(recycler.getScrapList().get(i).itemView, 2123 recycler); 2124 } 2125 } 2126 if (infoStore.mOldChangedHolders != null) { 2127 for (int i = infoStore.mOldChangedHolders.size() - 1; i >= 0; i--) { 2128 if (useRecycler) { 2129 recycler.recycleView( 2130 infoStore.mOldChangedHolders.valueAt(i).itemView); 2131 } else { 2132 removeAndRecycleView( 2133 infoStore.mOldChangedHolders.valueAt(i).itemView, 2134 recycler); 2135 } 2136 } 2137 } 2138 assertEquals("no scrap should be left over", 0, recycler.getScrapCount()); 2139 assertEquals("pre layout map should be empty", 0, 2140 InfoStoreTrojan.sizeOfPreLayout(infoStore)); 2141 assertEquals("post layout map should be empty", 0, 2142 InfoStoreTrojan.sizeOfPostLayout(infoStore)); 2143 if (infoStore.mOldChangedHolders != null) { 2144 assertEquals("post old change map should be empty", 0, 2145 infoStore.mOldChangedHolders.size()); 2146 } 2147 } catch (Throwable t) { 2148 postExceptionToInstrumentation(t); 2149 } 2150 2151 } 2152 layoutRange(recycler, 0, 5); 2153 layoutLatch.countDown(); 2154 super.onLayoutChildren(recycler, state); 2155 } 2156 }; 2157 RecyclerView recyclerView = new RecyclerView(getActivity()); 2158 recyclerView.setAdapter(testAdapter); 2159 recyclerView.setLayoutManager(lm); 2160 ((SimpleItemAnimator)recyclerView.getItemAnimator()).setSupportsChangeAnimations(true); 2161 lm.expectLayouts(1); 2162 setRecyclerView(recyclerView); 2163 lm.waitForLayout(2); 2164 test.set(true); 2165 lm.expectLayouts(1); 2166 testAdapter.changeAndNotify(3, 1); 2167 lm.waitForLayout(2); 2168 checkForMainThreadException(); 2169 } 2170 2171 @Test 2172 public void aAccessRecyclerOnOnMeasureWithPredictive() throws Throwable { 2173 accessRecyclerOnOnMeasureTest(true); 2174 } 2175 2176 @Test 2177 public void accessRecyclerOnOnMeasureWithoutPredictive() throws Throwable { 2178 accessRecyclerOnOnMeasureTest(false); 2179 } 2180 2181 @Test 2182 public void smoothScrollWithRemovedItemsAndRemoveItem() throws Throwable { 2183 smoothScrollTest(true); 2184 } 2185 2186 @Test 2187 public void smoothScrollWithRemovedItems() throws Throwable { 2188 smoothScrollTest(false); 2189 } 2190 2191 public void smoothScrollTest(final boolean removeItem) throws Throwable { 2192 final LinearSmoothScroller[] lss = new LinearSmoothScroller[1]; 2193 final CountDownLatch calledOnStart = new CountDownLatch(1); 2194 final CountDownLatch calledOnStop = new CountDownLatch(1); 2195 final int visibleChildCount = 10; 2196 TestLayoutManager lm = new TestLayoutManager() { 2197 int start = 0; 2198 2199 @Override 2200 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2201 super.onLayoutChildren(recycler, state); 2202 layoutRange(recycler, start, visibleChildCount); 2203 layoutLatch.countDown(); 2204 } 2205 2206 @Override 2207 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 2208 RecyclerView.State state) { 2209 start++; 2210 if (DEBUG) { 2211 Log.d(TAG, "on scroll, remove and recycling. start:" + start + ", cnt:" 2212 + visibleChildCount); 2213 } 2214 removeAndRecycleAllViews(recycler); 2215 layoutRange(recycler, start, 2216 Math.max(state.getItemCount(), start + visibleChildCount)); 2217 return dy; 2218 } 2219 2220 @Override 2221 public boolean canScrollVertically() { 2222 return true; 2223 } 2224 2225 @Override 2226 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 2227 int position) { 2228 LinearSmoothScroller linearSmoothScroller = 2229 new LinearSmoothScroller(recyclerView.getContext()) { 2230 @Override 2231 public PointF computeScrollVectorForPosition(int targetPosition) { 2232 return new PointF(0, 1); 2233 } 2234 2235 @Override 2236 protected void onStart() { 2237 super.onStart(); 2238 calledOnStart.countDown(); 2239 } 2240 2241 @Override 2242 protected void onStop() { 2243 super.onStop(); 2244 calledOnStop.countDown(); 2245 } 2246 }; 2247 linearSmoothScroller.setTargetPosition(position); 2248 lss[0] = linearSmoothScroller; 2249 startSmoothScroll(linearSmoothScroller); 2250 } 2251 }; 2252 final RecyclerView rv = new RecyclerView(getActivity()); 2253 TestAdapter testAdapter = new TestAdapter(500); 2254 rv.setLayoutManager(lm); 2255 rv.setAdapter(testAdapter); 2256 lm.expectLayouts(1); 2257 setRecyclerView(rv); 2258 lm.waitForLayout(1); 2259 // regular scroll 2260 final int targetPosition = visibleChildCount * (removeItem ? 30 : 4); 2261 runTestOnUiThread(new Runnable() { 2262 @Override 2263 public void run() { 2264 rv.smoothScrollToPosition(targetPosition); 2265 } 2266 }); 2267 if (DEBUG) { 2268 Log.d(TAG, "scrolling to target position " + targetPosition); 2269 } 2270 assertTrue("on start should be called very soon", calledOnStart.await(2, TimeUnit.SECONDS)); 2271 if (removeItem) { 2272 final int newTarget = targetPosition - 10; 2273 testAdapter.deleteAndNotify(newTarget + 1, testAdapter.getItemCount() - newTarget - 1); 2274 final CountDownLatch targetCheck = new CountDownLatch(1); 2275 runTestOnUiThread(new Runnable() { 2276 @Override 2277 public void run() { 2278 ViewCompat.postOnAnimationDelayed(rv, new Runnable() { 2279 @Override 2280 public void run() { 2281 try { 2282 assertEquals("scroll position should be updated to next available", 2283 newTarget, lss[0].getTargetPosition()); 2284 } catch (Throwable t) { 2285 postExceptionToInstrumentation(t); 2286 } 2287 targetCheck.countDown(); 2288 } 2289 }, 50); 2290 } 2291 }); 2292 assertTrue("target position should be checked on time ", 2293 targetCheck.await(10, TimeUnit.SECONDS)); 2294 checkForMainThreadException(); 2295 assertTrue("on stop should be called", calledOnStop.await(30, TimeUnit.SECONDS)); 2296 checkForMainThreadException(); 2297 assertNotNull("should scroll to new target " + newTarget 2298 , rv.findViewHolderForLayoutPosition(newTarget)); 2299 if (DEBUG) { 2300 Log.d(TAG, "on stop has been called on time"); 2301 } 2302 } else { 2303 assertTrue("on stop should be called eventually", 2304 calledOnStop.await(30, TimeUnit.SECONDS)); 2305 assertNotNull("scroll to position should succeed", 2306 rv.findViewHolderForLayoutPosition(targetPosition)); 2307 } 2308 checkForMainThreadException(); 2309 } 2310 2311 @Test 2312 public void consecutiveSmoothScroll() throws Throwable { 2313 final AtomicInteger visibleChildCount = new AtomicInteger(10); 2314 final AtomicInteger totalScrolled = new AtomicInteger(0); 2315 final TestLayoutManager lm = new TestLayoutManager() { 2316 int start = 0; 2317 2318 @Override 2319 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2320 super.onLayoutChildren(recycler, state); 2321 layoutRange(recycler, start, visibleChildCount.get()); 2322 layoutLatch.countDown(); 2323 } 2324 2325 @Override 2326 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 2327 RecyclerView.State state) { 2328 totalScrolled.set(totalScrolled.get() + dy); 2329 return dy; 2330 } 2331 2332 @Override 2333 public boolean canScrollVertically() { 2334 return true; 2335 } 2336 }; 2337 final RecyclerView rv = new RecyclerView(getActivity()); 2338 TestAdapter testAdapter = new TestAdapter(500); 2339 rv.setLayoutManager(lm); 2340 rv.setAdapter(testAdapter); 2341 lm.expectLayouts(1); 2342 setRecyclerView(rv); 2343 lm.waitForLayout(1); 2344 runTestOnUiThread(new Runnable() { 2345 @Override 2346 public void run() { 2347 rv.smoothScrollBy(0, 2000); 2348 } 2349 }); 2350 Thread.sleep(250); 2351 final AtomicInteger scrollAmt = new AtomicInteger(); 2352 runTestOnUiThread(new Runnable() { 2353 @Override 2354 public void run() { 2355 final int soFar = totalScrolled.get(); 2356 scrollAmt.set(soFar); 2357 rv.smoothScrollBy(0, 5000 - soFar); 2358 } 2359 }); 2360 while (rv.getScrollState() != SCROLL_STATE_IDLE) { 2361 Thread.sleep(100); 2362 } 2363 final int soFar = totalScrolled.get(); 2364 assertEquals("second scroll should be competed properly", 5000, soFar); 2365 } 2366 2367 public void accessRecyclerOnOnMeasureTest(final boolean enablePredictiveAnimations) 2368 throws Throwable { 2369 TestAdapter testAdapter = new TestAdapter(10); 2370 final AtomicInteger expectedOnMeasureStateCount = new AtomicInteger(10); 2371 TestLayoutManager lm = new TestLayoutManager() { 2372 @Override 2373 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2374 super.onLayoutChildren(recycler, state); 2375 try { 2376 layoutRange(recycler, 0, state.getItemCount()); 2377 layoutLatch.countDown(); 2378 } catch (Throwable t) { 2379 postExceptionToInstrumentation(t); 2380 } finally { 2381 layoutLatch.countDown(); 2382 } 2383 } 2384 2385 @Override 2386 public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, 2387 int widthSpec, int heightSpec) { 2388 try { 2389 // make sure we access all views 2390 for (int i = 0; i < state.getItemCount(); i++) { 2391 View view = recycler.getViewForPosition(i); 2392 assertNotNull(view); 2393 assertEquals(i, getPosition(view)); 2394 } 2395 if (!state.isPreLayout()) { 2396 assertEquals(state.toString(), 2397 expectedOnMeasureStateCount.get(), state.getItemCount()); 2398 } 2399 } catch (Throwable t) { 2400 postExceptionToInstrumentation(t); 2401 } 2402 super.onMeasure(recycler, state, widthSpec, heightSpec); 2403 } 2404 2405 @Override 2406 public boolean supportsPredictiveItemAnimations() { 2407 return enablePredictiveAnimations; 2408 } 2409 }; 2410 RecyclerView recyclerView = new RecyclerView(getActivity()); 2411 recyclerView.setLayoutManager(lm); 2412 recyclerView.setAdapter(testAdapter); 2413 recyclerView.setLayoutManager(lm); 2414 lm.expectLayouts(1); 2415 setRecyclerView(recyclerView); 2416 lm.waitForLayout(2); 2417 checkForMainThreadException(); 2418 lm.expectLayouts(1); 2419 if (!enablePredictiveAnimations) { 2420 expectedOnMeasureStateCount.set(15); 2421 } 2422 testAdapter.addAndNotify(4, 5); 2423 lm.waitForLayout(2); 2424 checkForMainThreadException(); 2425 } 2426 2427 @Test 2428 public void setCompatibleAdapter() throws Throwable { 2429 compatibleAdapterTest(true, true); 2430 removeRecyclerView(); 2431 compatibleAdapterTest(false, true); 2432 removeRecyclerView(); 2433 compatibleAdapterTest(true, false); 2434 removeRecyclerView(); 2435 compatibleAdapterTest(false, false); 2436 removeRecyclerView(); 2437 } 2438 2439 private void compatibleAdapterTest(boolean useCustomPool, boolean removeAndRecycleExistingViews) 2440 throws Throwable { 2441 TestAdapter testAdapter = new TestAdapter(10); 2442 final AtomicInteger recycledViewCount = new AtomicInteger(); 2443 TestLayoutManager lm = new TestLayoutManager() { 2444 @Override 2445 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2446 try { 2447 layoutRange(recycler, 0, state.getItemCount()); 2448 layoutLatch.countDown(); 2449 } catch (Throwable t) { 2450 postExceptionToInstrumentation(t); 2451 } finally { 2452 layoutLatch.countDown(); 2453 } 2454 } 2455 }; 2456 RecyclerView recyclerView = new RecyclerView(getActivity()); 2457 recyclerView.setLayoutManager(lm); 2458 recyclerView.setAdapter(testAdapter); 2459 recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() { 2460 @Override 2461 public void onViewRecycled(RecyclerView.ViewHolder holder) { 2462 recycledViewCount.incrementAndGet(); 2463 } 2464 }); 2465 lm.expectLayouts(1); 2466 setRecyclerView(recyclerView, !useCustomPool); 2467 lm.waitForLayout(2); 2468 checkForMainThreadException(); 2469 lm.expectLayouts(1); 2470 swapAdapter(new TestAdapter(10), removeAndRecycleExistingViews); 2471 lm.waitForLayout(2); 2472 checkForMainThreadException(); 2473 if (removeAndRecycleExistingViews) { 2474 assertTrue("Previous views should be recycled", recycledViewCount.get() > 0); 2475 } else { 2476 assertEquals("No views should be recycled if adapters are compatible and developer " 2477 + "did not request a recycle", 0, recycledViewCount.get()); 2478 } 2479 } 2480 2481 @Test 2482 public void setIncompatibleAdapter() throws Throwable { 2483 incompatibleAdapterTest(true); 2484 incompatibleAdapterTest(false); 2485 } 2486 2487 public void incompatibleAdapterTest(boolean useCustomPool) throws Throwable { 2488 TestAdapter testAdapter = new TestAdapter(10); 2489 TestLayoutManager lm = new TestLayoutManager() { 2490 @Override 2491 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2492 super.onLayoutChildren(recycler, state); 2493 try { 2494 layoutRange(recycler, 0, state.getItemCount()); 2495 layoutLatch.countDown(); 2496 } catch (Throwable t) { 2497 postExceptionToInstrumentation(t); 2498 } finally { 2499 layoutLatch.countDown(); 2500 } 2501 } 2502 }; 2503 RecyclerView recyclerView = new RecyclerView(getActivity()); 2504 recyclerView.setLayoutManager(lm); 2505 recyclerView.setAdapter(testAdapter); 2506 recyclerView.setLayoutManager(lm); 2507 lm.expectLayouts(1); 2508 setRecyclerView(recyclerView, !useCustomPool); 2509 lm.waitForLayout(2); 2510 checkForMainThreadException(); 2511 lm.expectLayouts(1); 2512 setAdapter(new TestAdapter2(10)); 2513 lm.waitForLayout(2); 2514 checkForMainThreadException(); 2515 } 2516 2517 @Test 2518 public void recycleIgnored() throws Throwable { 2519 final TestAdapter adapter = new TestAdapter(10); 2520 final TestLayoutManager lm = new TestLayoutManager() { 2521 @Override 2522 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2523 layoutRange(recycler, 0, 5); 2524 layoutLatch.countDown(); 2525 } 2526 }; 2527 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2528 recyclerView.setAdapter(adapter); 2529 recyclerView.setLayoutManager(lm); 2530 lm.expectLayouts(1); 2531 setRecyclerView(recyclerView); 2532 lm.waitForLayout(2); 2533 runTestOnUiThread(new Runnable() { 2534 @Override 2535 public void run() { 2536 View child1 = lm.findViewByPosition(0); 2537 View child2 = lm.findViewByPosition(1); 2538 lm.ignoreView(child1); 2539 lm.ignoreView(child2); 2540 2541 lm.removeAndRecycleAllViews(recyclerView.mRecycler); 2542 assertEquals("ignored child should not be recycled or removed", 2, 2543 lm.getChildCount()); 2544 2545 Throwable[] throwables = new Throwable[1]; 2546 try { 2547 lm.removeAndRecycleView(child1, mRecyclerView.mRecycler); 2548 } catch (Throwable t) { 2549 throwables[0] = t; 2550 } 2551 assertTrue("Trying to recycle an ignored view should throw IllegalArgException " 2552 , throwables[0] instanceof IllegalArgumentException); 2553 lm.removeAllViews(); 2554 assertEquals("ignored child should be removed as well ", 0, lm.getChildCount()); 2555 } 2556 }); 2557 } 2558 2559 @Test 2560 public void findIgnoredByPosition() throws Throwable { 2561 final TestAdapter adapter = new TestAdapter(10); 2562 final TestLayoutManager lm = new TestLayoutManager() { 2563 @Override 2564 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2565 detachAndScrapAttachedViews(recycler); 2566 layoutRange(recycler, 0, 5); 2567 layoutLatch.countDown(); 2568 } 2569 }; 2570 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2571 recyclerView.setAdapter(adapter); 2572 recyclerView.setLayoutManager(lm); 2573 lm.expectLayouts(1); 2574 setRecyclerView(recyclerView); 2575 lm.waitForLayout(2); 2576 Thread.sleep(5000); 2577 final int pos = 1; 2578 final View[] ignored = new View[1]; 2579 runTestOnUiThread(new Runnable() { 2580 @Override 2581 public void run() { 2582 View child = lm.findViewByPosition(pos); 2583 lm.ignoreView(child); 2584 ignored[0] = child; 2585 } 2586 }); 2587 assertNotNull("ignored child should not be null", ignored[0]); 2588 assertNull("find view by position should not return ignored child", 2589 lm.findViewByPosition(pos)); 2590 lm.expectLayouts(1); 2591 requestLayoutOnUIThread(mRecyclerView); 2592 lm.waitForLayout(1); 2593 assertEquals("child count should be ", 6, lm.getChildCount()); 2594 View replacement = lm.findViewByPosition(pos); 2595 assertNotNull("re-layout should replace ignored child w/ another one", replacement); 2596 assertNotSame("replacement should be a different view", replacement, ignored[0]); 2597 } 2598 2599 @Test 2600 public void itemDecorsWithPredictive() throws Throwable { 2601 LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true); 2602 lm.setSupportsPredictive(true); 2603 final Object changePayload = new Object(); 2604 final TestAdapter adapter = new TestAdapter(10) { 2605 @Override 2606 public void onBindViewHolder(TestViewHolder holder, 2607 int position, List<Object> payloads) { 2608 super.onBindViewHolder(holder, position); 2609 holder.setData(payloads.isEmpty() ? null : payloads.get(0)); 2610 } 2611 }; 2612 final Map<Integer, Object> preLayoutData = new HashMap<>(); 2613 final Map<Integer, Object> postLayoutData = new HashMap<>(); 2614 2615 final RecyclerView.ItemDecoration decoration = new RecyclerView.ItemDecoration() { 2616 @Override 2617 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 2618 RecyclerView.State state) { 2619 try { 2620 TestViewHolder tvh = (TestViewHolder) parent.getChildViewHolder(view); 2621 Object data = tvh.getData(); 2622 int adapterPos = tvh.getAdapterPosition(); 2623 assertThat(adapterPos, is(not(NO_POSITION))); 2624 if (state.isPreLayout()) { 2625 preLayoutData.put(adapterPos, data); 2626 } else { 2627 postLayoutData.put(adapterPos, data); 2628 } 2629 } catch (Throwable t) { 2630 postExceptionToInstrumentation(t); 2631 } 2632 2633 } 2634 }; 2635 RecyclerView rv = new RecyclerView(getActivity()); 2636 rv.addItemDecoration(decoration); 2637 rv.setAdapter(adapter); 2638 rv.setLayoutManager(lm); 2639 lm.expectLayouts(1); 2640 setRecyclerView(rv); 2641 lm.waitForLayout(2); 2642 2643 preLayoutData.clear(); 2644 postLayoutData.clear(); 2645 lm.expectLayouts(2); 2646 runTestOnUiThread(new Runnable() { 2647 @Override 2648 public void run() { 2649 adapter.notifyItemChanged(3, changePayload); 2650 } 2651 }); 2652 lm.waitForLayout(2); 2653 assertThat(preLayoutData.containsKey(3), is(false)); 2654 assertThat(postLayoutData.get(3), is(changePayload)); 2655 assertThat(preLayoutData.size(), is(0)); 2656 assertThat(postLayoutData.size(), is(1)); 2657 checkForMainThreadException(); 2658 } 2659 2660 @Test 2661 public void invalidateAllDecorOffsets() throws Throwable { 2662 final TestAdapter adapter = new TestAdapter(10); 2663 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2664 final AtomicBoolean invalidatedOffsets = new AtomicBoolean(true); 2665 recyclerView.setAdapter(adapter); 2666 final AtomicInteger layoutCount = new AtomicInteger(4); 2667 final RecyclerView.ItemDecoration dummyItemDecoration = new RecyclerView.ItemDecoration() { 2668 }; 2669 TestLayoutManager testLayoutManager = new TestLayoutManager() { 2670 @Override 2671 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2672 try { 2673 // test 2674 for (int i = 0; i < getChildCount(); i++) { 2675 View child = getChildAt(i); 2676 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) 2677 child.getLayoutParams(); 2678 assertEquals( 2679 "Decor insets validation for VH should have expected value.", 2680 invalidatedOffsets.get(), lp.mInsetsDirty); 2681 } 2682 for (RecyclerView.ViewHolder vh : mRecyclerView.mRecycler.mCachedViews) { 2683 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) 2684 vh.itemView.getLayoutParams(); 2685 assertEquals( 2686 "Decor insets invalidation in cache for VH should have expected " 2687 + "value.", 2688 invalidatedOffsets.get(), lp.mInsetsDirty); 2689 } 2690 detachAndScrapAttachedViews(recycler); 2691 layoutRange(recycler, 0, layoutCount.get()); 2692 } catch (Throwable t) { 2693 postExceptionToInstrumentation(t); 2694 } finally { 2695 layoutLatch.countDown(); 2696 } 2697 } 2698 2699 @Override 2700 public boolean supportsPredictiveItemAnimations() { 2701 return false; 2702 } 2703 }; 2704 // first layout 2705 recyclerView.setItemViewCacheSize(5); 2706 recyclerView.setLayoutManager(testLayoutManager); 2707 testLayoutManager.expectLayouts(1); 2708 setRecyclerView(recyclerView, true, false); 2709 testLayoutManager.waitForLayout(2); 2710 checkForMainThreadException(); 2711 2712 // re-layout w/o any change 2713 invalidatedOffsets.set(false); 2714 testLayoutManager.expectLayouts(1); 2715 requestLayoutOnUIThread(recyclerView); 2716 testLayoutManager.waitForLayout(1); 2717 checkForMainThreadException(); 2718 2719 // invalidate w/o an item decorator 2720 2721 invalidateDecorOffsets(recyclerView); 2722 testLayoutManager.expectLayouts(1); 2723 invalidateDecorOffsets(recyclerView); 2724 testLayoutManager.assertNoLayout("layout should not happen", 2); 2725 checkForMainThreadException(); 2726 2727 // set item decorator, should invalidate 2728 invalidatedOffsets.set(true); 2729 testLayoutManager.expectLayouts(1); 2730 addItemDecoration(mRecyclerView, dummyItemDecoration); 2731 testLayoutManager.waitForLayout(1); 2732 checkForMainThreadException(); 2733 2734 // re-layout w/o any change 2735 invalidatedOffsets.set(false); 2736 testLayoutManager.expectLayouts(1); 2737 requestLayoutOnUIThread(recyclerView); 2738 testLayoutManager.waitForLayout(1); 2739 checkForMainThreadException(); 2740 2741 // invalidate w/ item decorator 2742 invalidatedOffsets.set(true); 2743 invalidateDecorOffsets(recyclerView); 2744 testLayoutManager.expectLayouts(1); 2745 invalidateDecorOffsets(recyclerView); 2746 testLayoutManager.waitForLayout(2); 2747 checkForMainThreadException(); 2748 2749 // trigger cache. 2750 layoutCount.set(3); 2751 invalidatedOffsets.set(false); 2752 testLayoutManager.expectLayouts(1); 2753 requestLayoutOnUIThread(mRecyclerView); 2754 testLayoutManager.waitForLayout(1); 2755 checkForMainThreadException(); 2756 assertEquals("a view should be cached", 1, mRecyclerView.mRecycler.mCachedViews.size()); 2757 2758 layoutCount.set(5); 2759 invalidatedOffsets.set(true); 2760 testLayoutManager.expectLayouts(1); 2761 invalidateDecorOffsets(recyclerView); 2762 testLayoutManager.waitForLayout(1); 2763 checkForMainThreadException(); 2764 2765 // remove item decorator 2766 invalidatedOffsets.set(true); 2767 testLayoutManager.expectLayouts(1); 2768 removeItemDecoration(mRecyclerView, dummyItemDecoration); 2769 testLayoutManager.waitForLayout(1); 2770 checkForMainThreadException(); 2771 } 2772 2773 public void addItemDecoration(final RecyclerView recyclerView, final 2774 RecyclerView.ItemDecoration itemDecoration) throws Throwable { 2775 runTestOnUiThread(new Runnable() { 2776 @Override 2777 public void run() { 2778 recyclerView.addItemDecoration(itemDecoration); 2779 } 2780 }); 2781 } 2782 2783 public void removeItemDecoration(final RecyclerView recyclerView, final 2784 RecyclerView.ItemDecoration itemDecoration) throws Throwable { 2785 runTestOnUiThread(new Runnable() { 2786 @Override 2787 public void run() { 2788 recyclerView.removeItemDecoration(itemDecoration); 2789 } 2790 }); 2791 } 2792 2793 public void invalidateDecorOffsets(final RecyclerView recyclerView) throws Throwable { 2794 runTestOnUiThread(new Runnable() { 2795 @Override 2796 public void run() { 2797 recyclerView.invalidateItemDecorations(); 2798 } 2799 }); 2800 } 2801 2802 @Test 2803 public void invalidateDecorOffsets() throws Throwable { 2804 final TestAdapter adapter = new TestAdapter(10); 2805 adapter.setHasStableIds(true); 2806 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2807 recyclerView.setAdapter(adapter); 2808 2809 final Map<Long, Boolean> changes = new HashMap<>(); 2810 2811 TestLayoutManager testLayoutManager = new TestLayoutManager() { 2812 @Override 2813 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2814 try { 2815 if (changes.size() > 0) { 2816 // test 2817 for (int i = 0; i < getChildCount(); i++) { 2818 View child = getChildAt(i); 2819 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) 2820 child.getLayoutParams(); 2821 RecyclerView.ViewHolder vh = lp.mViewHolder; 2822 if (!changes.containsKey(vh.getItemId())) { 2823 continue; //nothing to test 2824 } 2825 assertEquals( 2826 "Decor insets validation for VH should have expected value.", 2827 changes.get(vh.getItemId()), lp.mInsetsDirty); 2828 } 2829 } 2830 detachAndScrapAttachedViews(recycler); 2831 layoutRange(recycler, 0, state.getItemCount()); 2832 } catch (Throwable t) { 2833 postExceptionToInstrumentation(t); 2834 } finally { 2835 layoutLatch.countDown(); 2836 } 2837 } 2838 2839 @Override 2840 public boolean supportsPredictiveItemAnimations() { 2841 return false; 2842 } 2843 }; 2844 recyclerView.setLayoutManager(testLayoutManager); 2845 testLayoutManager.expectLayouts(1); 2846 setRecyclerView(recyclerView); 2847 testLayoutManager.waitForLayout(2); 2848 int itemAddedTo = 5; 2849 for (int i = 0; i < itemAddedTo; i++) { 2850 changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false); 2851 } 2852 for (int i = itemAddedTo; i < mRecyclerView.getChildCount(); i++) { 2853 changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true); 2854 } 2855 testLayoutManager.expectLayouts(1); 2856 adapter.addAndNotify(5, 1); 2857 testLayoutManager.waitForLayout(2); 2858 checkForMainThreadException(); 2859 2860 changes.clear(); 2861 int[] changedItems = new int[]{3, 5, 6}; 2862 for (int i = 0; i < adapter.getItemCount(); i++) { 2863 changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false); 2864 } 2865 for (int changedItem : changedItems) { 2866 changes.put(mRecyclerView.findViewHolderForLayoutPosition(changedItem).getItemId(), 2867 true); 2868 } 2869 testLayoutManager.expectLayouts(1); 2870 adapter.changePositionsAndNotify(changedItems); 2871 testLayoutManager.waitForLayout(2); 2872 checkForMainThreadException(); 2873 2874 for (int i = 0; i < adapter.getItemCount(); i++) { 2875 changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true); 2876 } 2877 testLayoutManager.expectLayouts(1); 2878 adapter.dispatchDataSetChanged(); 2879 testLayoutManager.waitForLayout(2); 2880 checkForMainThreadException(); 2881 } 2882 2883 @Test 2884 public void movingViaStableIds() throws Throwable { 2885 stableIdsMoveTest(true); 2886 removeRecyclerView(); 2887 stableIdsMoveTest(false); 2888 removeRecyclerView(); 2889 } 2890 2891 public void stableIdsMoveTest(final boolean supportsPredictive) throws Throwable { 2892 final TestAdapter testAdapter = new TestAdapter(10); 2893 testAdapter.setHasStableIds(true); 2894 final AtomicBoolean test = new AtomicBoolean(false); 2895 final int movedViewFromIndex = 3; 2896 final int movedViewToIndex = 6; 2897 final View[] movedView = new View[1]; 2898 TestLayoutManager lm = new TestLayoutManager() { 2899 @Override 2900 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2901 detachAndScrapAttachedViews(recycler); 2902 try { 2903 if (test.get()) { 2904 if (state.isPreLayout()) { 2905 View view = recycler.getViewForPosition(movedViewFromIndex, true); 2906 assertSame("In pre layout, should be able to get moved view w/ old " 2907 + "position", movedView[0], view); 2908 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); 2909 assertTrue("it should come from scrap", holder.wasReturnedFromScrap()); 2910 // clear scrap flag 2911 holder.clearReturnedFromScrapFlag(); 2912 } else { 2913 View view = recycler.getViewForPosition(movedViewToIndex, true); 2914 assertSame("In post layout, should be able to get moved view w/ new " 2915 + "position", movedView[0], view); 2916 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); 2917 assertTrue("it should come from scrap", holder.wasReturnedFromScrap()); 2918 // clear scrap flag 2919 holder.clearReturnedFromScrapFlag(); 2920 } 2921 } 2922 layoutRange(recycler, 0, state.getItemCount()); 2923 } catch (Throwable t) { 2924 postExceptionToInstrumentation(t); 2925 } finally { 2926 layoutLatch.countDown(); 2927 } 2928 2929 2930 } 2931 2932 @Override 2933 public boolean supportsPredictiveItemAnimations() { 2934 return supportsPredictive; 2935 } 2936 }; 2937 RecyclerView recyclerView = new RecyclerView(this.getActivity()); 2938 recyclerView.setAdapter(testAdapter); 2939 recyclerView.setLayoutManager(lm); 2940 lm.expectLayouts(1); 2941 setRecyclerView(recyclerView); 2942 lm.waitForLayout(1); 2943 2944 movedView[0] = recyclerView.getChildAt(movedViewFromIndex); 2945 test.set(true); 2946 lm.expectLayouts(supportsPredictive ? 2 : 1); 2947 runTestOnUiThread(new Runnable() { 2948 @Override 2949 public void run() { 2950 Item item = testAdapter.mItems.remove(movedViewFromIndex); 2951 testAdapter.mItems.add(movedViewToIndex, item); 2952 testAdapter.notifyItemRemoved(movedViewFromIndex); 2953 testAdapter.notifyItemInserted(movedViewToIndex); 2954 } 2955 }); 2956 lm.waitForLayout(2); 2957 checkForMainThreadException(); 2958 } 2959 2960 @Test 2961 public void adapterChangeDuringLayout() throws Throwable { 2962 adapterChangeInMainThreadTest("notifyDataSetChanged", new Runnable() { 2963 @Override 2964 public void run() { 2965 mRecyclerView.getAdapter().notifyDataSetChanged(); 2966 } 2967 }); 2968 2969 adapterChangeInMainThreadTest("notifyItemChanged", new Runnable() { 2970 @Override 2971 public void run() { 2972 mRecyclerView.getAdapter().notifyItemChanged(2); 2973 } 2974 }); 2975 2976 adapterChangeInMainThreadTest("notifyItemInserted", new Runnable() { 2977 @Override 2978 public void run() { 2979 mRecyclerView.getAdapter().notifyItemInserted(2); 2980 } 2981 }); 2982 adapterChangeInMainThreadTest("notifyItemRemoved", new Runnable() { 2983 @Override 2984 public void run() { 2985 mRecyclerView.getAdapter().notifyItemRemoved(2); 2986 } 2987 }); 2988 } 2989 2990 public void adapterChangeInMainThreadTest(String msg, 2991 final Runnable onLayoutRunnable) throws Throwable { 2992 setIgnoreMainThreadException(true); 2993 final AtomicBoolean doneFirstLayout = new AtomicBoolean(false); 2994 TestAdapter testAdapter = new TestAdapter(10); 2995 TestLayoutManager lm = new TestLayoutManager() { 2996 @Override 2997 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2998 super.onLayoutChildren(recycler, state); 2999 try { 3000 layoutRange(recycler, 0, state.getItemCount()); 3001 if (doneFirstLayout.get()) { 3002 onLayoutRunnable.run(); 3003 } 3004 } catch (Throwable t) { 3005 postExceptionToInstrumentation(t); 3006 } finally { 3007 layoutLatch.countDown(); 3008 } 3009 3010 } 3011 }; 3012 RecyclerView recyclerView = new RecyclerView(getActivity()); 3013 recyclerView.setLayoutManager(lm); 3014 recyclerView.setAdapter(testAdapter); 3015 lm.expectLayouts(1); 3016 setRecyclerView(recyclerView); 3017 lm.waitForLayout(2); 3018 doneFirstLayout.set(true); 3019 lm.expectLayouts(1); 3020 requestLayoutOnUIThread(recyclerView); 3021 lm.waitForLayout(2); 3022 removeRecyclerView(); 3023 assertTrue("Invalid data updates should be caught:" + msg, 3024 getMainThreadException() instanceof IllegalStateException); 3025 } 3026 3027 @Test 3028 public void adapterChangeDuringScroll() throws Throwable { 3029 for (int orientation : new int[]{OrientationHelper.HORIZONTAL, 3030 OrientationHelper.VERTICAL}) { 3031 adapterChangeDuringScrollTest("notifyDataSetChanged", orientation, 3032 new Runnable() { 3033 @Override 3034 public void run() { 3035 mRecyclerView.getAdapter().notifyDataSetChanged(); 3036 } 3037 }); 3038 adapterChangeDuringScrollTest("notifyItemChanged", orientation, new Runnable() { 3039 @Override 3040 public void run() { 3041 mRecyclerView.getAdapter().notifyItemChanged(2); 3042 } 3043 }); 3044 3045 adapterChangeDuringScrollTest("notifyItemInserted", orientation, new Runnable() { 3046 @Override 3047 public void run() { 3048 mRecyclerView.getAdapter().notifyItemInserted(2); 3049 } 3050 }); 3051 adapterChangeDuringScrollTest("notifyItemRemoved", orientation, new Runnable() { 3052 @Override 3053 public void run() { 3054 mRecyclerView.getAdapter().notifyItemRemoved(2); 3055 } 3056 }); 3057 } 3058 } 3059 3060 public void adapterChangeDuringScrollTest(String msg, final int orientation, 3061 final Runnable onScrollRunnable) throws Throwable { 3062 setIgnoreMainThreadException(true); 3063 TestAdapter testAdapter = new TestAdapter(100); 3064 TestLayoutManager lm = new TestLayoutManager() { 3065 @Override 3066 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3067 super.onLayoutChildren(recycler, state); 3068 try { 3069 layoutRange(recycler, 0, 10); 3070 } catch (Throwable t) { 3071 postExceptionToInstrumentation(t); 3072 } finally { 3073 layoutLatch.countDown(); 3074 } 3075 } 3076 3077 @Override 3078 public boolean canScrollVertically() { 3079 return orientation == OrientationHelper.VERTICAL; 3080 } 3081 3082 @Override 3083 public boolean canScrollHorizontally() { 3084 return orientation == OrientationHelper.HORIZONTAL; 3085 } 3086 3087 public int mockScroll() { 3088 try { 3089 onScrollRunnable.run(); 3090 } catch (Throwable t) { 3091 postExceptionToInstrumentation(t); 3092 } finally { 3093 layoutLatch.countDown(); 3094 } 3095 return 0; 3096 } 3097 3098 @Override 3099 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 3100 RecyclerView.State state) { 3101 return mockScroll(); 3102 } 3103 3104 @Override 3105 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 3106 RecyclerView.State state) { 3107 return mockScroll(); 3108 } 3109 }; 3110 RecyclerView recyclerView = new RecyclerView(getActivity()); 3111 recyclerView.setLayoutManager(lm); 3112 recyclerView.setAdapter(testAdapter); 3113 lm.expectLayouts(1); 3114 setRecyclerView(recyclerView); 3115 lm.waitForLayout(2); 3116 lm.expectLayouts(1); 3117 scrollBy(200); 3118 lm.waitForLayout(2); 3119 removeRecyclerView(); 3120 assertTrue("Invalid data updates should be caught:" + msg, 3121 getMainThreadException() instanceof IllegalStateException); 3122 } 3123 3124 @Test 3125 public void recycleOnDetach() throws Throwable { 3126 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3127 final TestAdapter testAdapter = new TestAdapter(10); 3128 final AtomicBoolean didRunOnDetach = new AtomicBoolean(false); 3129 final TestLayoutManager lm = new TestLayoutManager() { 3130 @Override 3131 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3132 super.onLayoutChildren(recycler, state); 3133 layoutRange(recycler, 0, state.getItemCount() - 1); 3134 layoutLatch.countDown(); 3135 } 3136 3137 @Override 3138 public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 3139 super.onDetachedFromWindow(view, recycler); 3140 didRunOnDetach.set(true); 3141 removeAndRecycleAllViews(recycler); 3142 } 3143 }; 3144 recyclerView.setAdapter(testAdapter); 3145 recyclerView.setLayoutManager(lm); 3146 lm.expectLayouts(1); 3147 setRecyclerView(recyclerView); 3148 lm.waitForLayout(2); 3149 removeRecyclerView(); 3150 assertTrue("When recycler view is removed, detach should run", didRunOnDetach.get()); 3151 assertEquals("All children should be recycled", recyclerView.getChildCount(), 0); 3152 } 3153 3154 @Test 3155 public void updatesWhileDetached() throws Throwable { 3156 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3157 final int initialAdapterSize = 20; 3158 final TestAdapter adapter = new TestAdapter(initialAdapterSize); 3159 final AtomicInteger layoutCount = new AtomicInteger(0); 3160 TestLayoutManager lm = new TestLayoutManager() { 3161 @Override 3162 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3163 super.onLayoutChildren(recycler, state); 3164 layoutRange(recycler, 0, 5); 3165 layoutCount.incrementAndGet(); 3166 layoutLatch.countDown(); 3167 } 3168 }; 3169 recyclerView.setAdapter(adapter); 3170 recyclerView.setLayoutManager(lm); 3171 recyclerView.setHasFixedSize(true); 3172 lm.expectLayouts(1); 3173 adapter.addAndNotify(4, 5); 3174 lm.assertNoLayout("When RV is not attached, layout should not happen", 1); 3175 } 3176 3177 @Test 3178 public void updatesAfterDetach() throws Throwable { 3179 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3180 final int initialAdapterSize = 20; 3181 final TestAdapter adapter = new TestAdapter(initialAdapterSize); 3182 final AtomicInteger layoutCount = new AtomicInteger(0); 3183 TestLayoutManager lm = new TestLayoutManager() { 3184 @Override 3185 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3186 super.onLayoutChildren(recycler, state); 3187 layoutRange(recycler, 0, 5); 3188 layoutCount.incrementAndGet(); 3189 layoutLatch.countDown(); 3190 } 3191 }; 3192 recyclerView.setAdapter(adapter); 3193 recyclerView.setLayoutManager(lm); 3194 lm.expectLayouts(1); 3195 recyclerView.setHasFixedSize(true); 3196 setRecyclerView(recyclerView); 3197 lm.waitForLayout(2); 3198 lm.expectLayouts(1); 3199 final int prevLayoutCount = layoutCount.get(); 3200 runTestOnUiThread(new Runnable() { 3201 @Override 3202 public void run() { 3203 try { 3204 adapter.addAndNotify(4, 5); 3205 removeRecyclerView(); 3206 } catch (Throwable throwable) { 3207 postExceptionToInstrumentation(throwable); 3208 } 3209 } 3210 }); 3211 checkForMainThreadException(); 3212 3213 lm.assertNoLayout("When RV is not attached, layout should not happen", 1); 3214 assertEquals("No extra layout should happen when detached", prevLayoutCount, 3215 layoutCount.get()); 3216 } 3217 3218 @Test 3219 public void notifyDataSetChangedWithStableIds() throws Throwable { 3220 final Map<Integer, Integer> oldPositionToNewPositionMapping = new HashMap<>(); 3221 final TestAdapter adapter = new TestAdapter(100) { 3222 @Override 3223 public long getItemId(int position) { 3224 return mItems.get(position).mId; 3225 } 3226 }; 3227 adapter.setHasStableIds(true); 3228 final ArrayList<Item> previousItems = new ArrayList<>(); 3229 previousItems.addAll(adapter.mItems); 3230 3231 final AtomicInteger layoutStart = new AtomicInteger(50); 3232 final AtomicBoolean validate = new AtomicBoolean(false); 3233 final int childCount = 10; 3234 final TestLayoutManager lm = new TestLayoutManager() { 3235 @Override 3236 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3237 try { 3238 super.onLayoutChildren(recycler, state); 3239 if (validate.get()) { 3240 assertEquals("Cached views should be kept", 5, recycler 3241 .mCachedViews.size()); 3242 for (RecyclerView.ViewHolder vh : recycler.mCachedViews) { 3243 TestViewHolder tvh = (TestViewHolder) vh; 3244 assertTrue("view holder should be marked for update", 3245 tvh.needsUpdate()); 3246 assertTrue("view holder should be marked as invalid", tvh.isInvalid()); 3247 } 3248 } 3249 detachAndScrapAttachedViews(recycler); 3250 if (validate.get()) { 3251 assertEquals("cache size should stay the same", 5, 3252 recycler.mCachedViews.size()); 3253 assertEquals("all views should be scrapped", childCount, 3254 recycler.getScrapList().size()); 3255 for (RecyclerView.ViewHolder vh : recycler.getScrapList()) { 3256 // TODO create test case for type change 3257 TestViewHolder tvh = (TestViewHolder) vh; 3258 assertTrue("view holder should be marked for update", 3259 tvh.needsUpdate()); 3260 assertTrue("view holder should be marked as invalid", tvh.isInvalid()); 3261 } 3262 } 3263 layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount); 3264 if (validate.get()) { 3265 for (int i = 0; i < getChildCount(); i++) { 3266 View view = getChildAt(i); 3267 TestViewHolder tvh = (TestViewHolder) mRecyclerView 3268 .getChildViewHolder(view); 3269 final int oldPos = previousItems.indexOf(tvh.mBoundItem); 3270 assertEquals("view holder's position should be correct", 3271 oldPositionToNewPositionMapping.get(oldPos).intValue(), 3272 tvh.getLayoutPosition()); 3273 } 3274 } 3275 } catch (Throwable t) { 3276 postExceptionToInstrumentation(t); 3277 } finally { 3278 layoutLatch.countDown(); 3279 } 3280 } 3281 }; 3282 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3283 recyclerView.setItemAnimator(null); 3284 recyclerView.setAdapter(adapter); 3285 recyclerView.setLayoutManager(lm); 3286 recyclerView.setItemViewCacheSize(10); 3287 lm.expectLayouts(1); 3288 setRecyclerView(recyclerView); 3289 lm.waitForLayout(2); 3290 checkForMainThreadException(); 3291 getInstrumentation().waitForIdleSync(); 3292 layoutStart.set(layoutStart.get() + 5);//55 3293 lm.expectLayouts(1); 3294 requestLayoutOnUIThread(recyclerView); 3295 lm.waitForLayout(2); 3296 validate.set(true); 3297 lm.expectLayouts(1); 3298 runTestOnUiThread(new Runnable() { 3299 @Override 3300 public void run() { 3301 try { 3302 adapter.moveItems(false, 3303 new int[]{50, 56}, new int[]{51, 1}, new int[]{52, 2}, 3304 new int[]{53, 54}, new int[]{60, 61}, new int[]{62, 64}, 3305 new int[]{75, 58}); 3306 for (int i = 0; i < previousItems.size(); i++) { 3307 Item item = previousItems.get(i); 3308 oldPositionToNewPositionMapping.put(i, adapter.mItems.indexOf(item)); 3309 } 3310 adapter.dispatchDataSetChanged(); 3311 } catch (Throwable throwable) { 3312 postExceptionToInstrumentation(throwable); 3313 } 3314 } 3315 }); 3316 lm.waitForLayout(2); 3317 checkForMainThreadException(); 3318 } 3319 3320 @Test 3321 public void callbacksDuringAdapterSwap() throws Throwable { 3322 callbacksDuringAdapterChange(true); 3323 } 3324 3325 @Test 3326 public void callbacksDuringAdapterSet() throws Throwable { 3327 callbacksDuringAdapterChange(false); 3328 } 3329 3330 public void callbacksDuringAdapterChange(boolean swap) throws Throwable { 3331 final TestAdapter2 adapter1 = swap ? createBinderCheckingAdapter() 3332 : createOwnerCheckingAdapter(); 3333 final TestAdapter2 adapter2 = swap ? createBinderCheckingAdapter() 3334 : createOwnerCheckingAdapter(); 3335 3336 TestLayoutManager tlm = new TestLayoutManager() { 3337 @Override 3338 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3339 try { 3340 layoutRange(recycler, 0, state.getItemCount()); 3341 } catch (Throwable t) { 3342 postExceptionToInstrumentation(t); 3343 } 3344 layoutLatch.countDown(); 3345 } 3346 }; 3347 RecyclerView rv = new RecyclerView(getActivity()); 3348 rv.setAdapter(adapter1); 3349 rv.setLayoutManager(tlm); 3350 tlm.expectLayouts(1); 3351 setRecyclerView(rv); 3352 tlm.waitForLayout(1); 3353 checkForMainThreadException(); 3354 tlm.expectLayouts(1); 3355 if (swap) { 3356 swapAdapter(adapter2, true); 3357 } else { 3358 setAdapter(adapter2); 3359 } 3360 checkForMainThreadException(); 3361 tlm.waitForLayout(1); 3362 checkForMainThreadException(); 3363 } 3364 3365 private TestAdapter2 createOwnerCheckingAdapter() { 3366 return new TestAdapter2(10) { 3367 @Override 3368 public void onViewRecycled(TestViewHolder2 holder) { 3369 assertSame("on recycled should be called w/ the creator adapter", this, 3370 holder.mData); 3371 super.onViewRecycled(holder); 3372 } 3373 3374 @Override 3375 public void onBindViewHolder(TestViewHolder2 holder, int position) { 3376 super.onBindViewHolder(holder, position); 3377 assertSame("on bind should be called w/ the creator adapter", this, holder.mData); 3378 } 3379 3380 @Override 3381 public TestViewHolder2 onCreateViewHolder(ViewGroup parent, 3382 int viewType) { 3383 final TestViewHolder2 vh = super.onCreateViewHolder(parent, viewType); 3384 vh.mData = this; 3385 return vh; 3386 } 3387 }; 3388 } 3389 3390 private TestAdapter2 createBinderCheckingAdapter() { 3391 return new TestAdapter2(10) { 3392 @Override 3393 public void onViewRecycled(TestViewHolder2 holder) { 3394 assertSame("on recycled should be called w/ the creator adapter", this, 3395 holder.mData); 3396 holder.mData = null; 3397 super.onViewRecycled(holder); 3398 } 3399 3400 @Override 3401 public void onBindViewHolder(TestViewHolder2 holder, int position) { 3402 super.onBindViewHolder(holder, position); 3403 holder.mData = this; 3404 } 3405 }; 3406 } 3407 3408 @Test 3409 public void findViewById() throws Throwable { 3410 findViewByIdTest(false); 3411 removeRecyclerView(); 3412 findViewByIdTest(true); 3413 } 3414 3415 public void findViewByIdTest(final boolean supportPredictive) throws Throwable { 3416 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3417 final int initialAdapterSize = 20; 3418 final TestAdapter adapter = new TestAdapter(initialAdapterSize); 3419 final int deleteStart = 6; 3420 final int deleteCount = 5; 3421 recyclerView.setAdapter(adapter); 3422 final AtomicBoolean assertPositions = new AtomicBoolean(false); 3423 TestLayoutManager lm = new TestLayoutManager() { 3424 @Override 3425 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3426 super.onLayoutChildren(recycler, state); 3427 if (assertPositions.get()) { 3428 if (state.isPreLayout()) { 3429 for (int i = 0; i < deleteStart; i++) { 3430 View view = findViewByPosition(i); 3431 assertNotNull("find view by position for existing items should work " 3432 + "fine", view); 3433 assertFalse("view should not be marked as removed", 3434 ((RecyclerView.LayoutParams) view.getLayoutParams()) 3435 .isItemRemoved()); 3436 } 3437 for (int i = 0; i < deleteCount; i++) { 3438 View view = findViewByPosition(i + deleteStart); 3439 assertNotNull("find view by position should work fine for removed " 3440 + "views in pre-layout", view); 3441 assertTrue("view should be marked as removed", 3442 ((RecyclerView.LayoutParams) view.getLayoutParams()) 3443 .isItemRemoved()); 3444 } 3445 for (int i = deleteStart + deleteCount; i < 20; i++) { 3446 View view = findViewByPosition(i); 3447 assertNotNull(view); 3448 assertFalse("view should not be marked as removed", 3449 ((RecyclerView.LayoutParams) view.getLayoutParams()) 3450 .isItemRemoved()); 3451 } 3452 } else { 3453 for (int i = 0; i < initialAdapterSize - deleteCount; i++) { 3454 View view = findViewByPosition(i); 3455 assertNotNull("find view by position for existing item " + i + 3456 " should work fine. child count:" + getChildCount(), view); 3457 TestViewHolder viewHolder = 3458 (TestViewHolder) mRecyclerView.getChildViewHolder(view); 3459 assertSame("should be the correct item " + viewHolder 3460 , viewHolder.mBoundItem, 3461 adapter.mItems.get(viewHolder.mPosition)); 3462 assertFalse("view should not be marked as removed", 3463 ((RecyclerView.LayoutParams) view.getLayoutParams()) 3464 .isItemRemoved()); 3465 } 3466 } 3467 } 3468 detachAndScrapAttachedViews(recycler); 3469 layoutRange(recycler, state.getItemCount() - 1, -1); 3470 layoutLatch.countDown(); 3471 } 3472 3473 @Override 3474 public boolean supportsPredictiveItemAnimations() { 3475 return supportPredictive; 3476 } 3477 }; 3478 recyclerView.setLayoutManager(lm); 3479 lm.expectLayouts(1); 3480 setRecyclerView(recyclerView); 3481 lm.waitForLayout(2); 3482 getInstrumentation().waitForIdleSync(); 3483 3484 assertPositions.set(true); 3485 lm.expectLayouts(supportPredictive ? 2 : 1); 3486 adapter.deleteAndNotify(new int[]{deleteStart, deleteCount - 1}, new int[]{deleteStart, 1}); 3487 lm.waitForLayout(2); 3488 } 3489 3490 @Test 3491 public void typeForCache() throws Throwable { 3492 final AtomicInteger viewType = new AtomicInteger(1); 3493 final TestAdapter adapter = new TestAdapter(100) { 3494 @Override 3495 public int getItemViewType(int position) { 3496 return viewType.get(); 3497 } 3498 3499 @Override 3500 public long getItemId(int position) { 3501 return mItems.get(position).mId; 3502 } 3503 }; 3504 adapter.setHasStableIds(true); 3505 final AtomicInteger layoutStart = new AtomicInteger(2); 3506 final int childCount = 10; 3507 final TestLayoutManager lm = new TestLayoutManager() { 3508 @Override 3509 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3510 super.onLayoutChildren(recycler, state); 3511 detachAndScrapAttachedViews(recycler); 3512 layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount); 3513 layoutLatch.countDown(); 3514 } 3515 }; 3516 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3517 recyclerView.setItemAnimator(null); 3518 recyclerView.setAdapter(adapter); 3519 recyclerView.setLayoutManager(lm); 3520 recyclerView.setItemViewCacheSize(10); 3521 lm.expectLayouts(1); 3522 setRecyclerView(recyclerView); 3523 lm.waitForLayout(2); 3524 getInstrumentation().waitForIdleSync(); 3525 layoutStart.set(4); // trigger a cache for 3,4 3526 lm.expectLayouts(1); 3527 requestLayoutOnUIThread(recyclerView); 3528 lm.waitForLayout(2); 3529 // 3530 viewType.incrementAndGet(); 3531 layoutStart.set(2); // go back to bring views from cache 3532 lm.expectLayouts(1); 3533 adapter.mItems.remove(1); 3534 adapter.dispatchDataSetChanged(); 3535 lm.waitForLayout(2); 3536 runTestOnUiThread(new Runnable() { 3537 @Override 3538 public void run() { 3539 for (int i = 2; i < 4; i++) { 3540 RecyclerView.ViewHolder vh = recyclerView.findViewHolderForLayoutPosition(i); 3541 assertEquals("View holder's type should match latest type", viewType.get(), 3542 vh.getItemViewType()); 3543 } 3544 } 3545 }); 3546 } 3547 3548 @Test 3549 public void typeForExistingViews() throws Throwable { 3550 final AtomicInteger viewType = new AtomicInteger(1); 3551 final int invalidatedCount = 2; 3552 final int layoutStart = 2; 3553 final TestAdapter adapter = new TestAdapter(100) { 3554 @Override 3555 public int getItemViewType(int position) { 3556 return viewType.get(); 3557 } 3558 3559 @Override 3560 public void onBindViewHolder(TestViewHolder holder, 3561 int position) { 3562 super.onBindViewHolder(holder, position); 3563 if (position >= layoutStart && position < invalidatedCount + layoutStart) { 3564 try { 3565 assertEquals("holder type should match current view type at position " + 3566 position, viewType.get(), holder.getItemViewType()); 3567 } catch (Throwable t) { 3568 postExceptionToInstrumentation(t); 3569 } 3570 } 3571 } 3572 3573 @Override 3574 public long getItemId(int position) { 3575 return mItems.get(position).mId; 3576 } 3577 }; 3578 adapter.setHasStableIds(true); 3579 3580 final int childCount = 10; 3581 final TestLayoutManager lm = new TestLayoutManager() { 3582 @Override 3583 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3584 super.onLayoutChildren(recycler, state); 3585 detachAndScrapAttachedViews(recycler); 3586 layoutRange(recycler, layoutStart, layoutStart + childCount); 3587 layoutLatch.countDown(); 3588 } 3589 }; 3590 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3591 recyclerView.setAdapter(adapter); 3592 recyclerView.setLayoutManager(lm); 3593 lm.expectLayouts(1); 3594 setRecyclerView(recyclerView); 3595 lm.waitForLayout(2); 3596 getInstrumentation().waitForIdleSync(); 3597 viewType.incrementAndGet(); 3598 lm.expectLayouts(1); 3599 adapter.changeAndNotify(layoutStart, invalidatedCount); 3600 lm.waitForLayout(2); 3601 checkForMainThreadException(); 3602 } 3603 3604 3605 @Test 3606 public void state() throws Throwable { 3607 final TestAdapter adapter = new TestAdapter(10); 3608 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3609 recyclerView.setAdapter(adapter); 3610 recyclerView.setItemAnimator(null); 3611 final AtomicInteger itemCount = new AtomicInteger(); 3612 final AtomicBoolean structureChanged = new AtomicBoolean(); 3613 TestLayoutManager testLayoutManager = new TestLayoutManager() { 3614 @Override 3615 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3616 detachAndScrapAttachedViews(recycler); 3617 layoutRange(recycler, 0, state.getItemCount()); 3618 itemCount.set(state.getItemCount()); 3619 structureChanged.set(state.didStructureChange()); 3620 layoutLatch.countDown(); 3621 } 3622 }; 3623 recyclerView.setLayoutManager(testLayoutManager); 3624 testLayoutManager.expectLayouts(1); 3625 runTestOnUiThread(new Runnable() { 3626 @Override 3627 public void run() { 3628 getActivity().getContainer().addView(recyclerView); 3629 } 3630 }); 3631 testLayoutManager.waitForLayout(2); 3632 3633 assertEquals("item count in state should be correct", adapter.getItemCount() 3634 , itemCount.get()); 3635 assertEquals("structure changed should be true for first layout", true, 3636 structureChanged.get()); 3637 Thread.sleep(1000); //wait for other layouts. 3638 testLayoutManager.expectLayouts(1); 3639 runTestOnUiThread(new Runnable() { 3640 @Override 3641 public void run() { 3642 recyclerView.requestLayout(); 3643 } 3644 }); 3645 testLayoutManager.waitForLayout(2); 3646 assertEquals("in second layout,structure changed should be false", false, 3647 structureChanged.get()); 3648 testLayoutManager.expectLayouts(1); // 3649 adapter.deleteAndNotify(3, 2); 3650 testLayoutManager.waitForLayout(2); 3651 assertEquals("when items are removed, item count in state should be updated", 3652 adapter.getItemCount(), 3653 itemCount.get()); 3654 assertEquals("structure changed should be true when items are removed", true, 3655 structureChanged.get()); 3656 testLayoutManager.expectLayouts(1); 3657 adapter.addAndNotify(2, 5); 3658 testLayoutManager.waitForLayout(2); 3659 3660 assertEquals("when items are added, item count in state should be updated", 3661 adapter.getItemCount(), 3662 itemCount.get()); 3663 assertEquals("structure changed should be true when items are removed", true, 3664 structureChanged.get()); 3665 } 3666 3667 @Test 3668 public void detachWithoutLayoutManager() throws Throwable { 3669 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3670 runTestOnUiThread(new Runnable() { 3671 @Override 3672 public void run() { 3673 try { 3674 setRecyclerView(recyclerView); 3675 removeRecyclerView(); 3676 } catch (Throwable t) { 3677 postExceptionToInstrumentation(t); 3678 } 3679 } 3680 }); 3681 checkForMainThreadException(); 3682 } 3683 3684 @Test 3685 public void updateHiddenView() throws Throwable { 3686 final RecyclerView recyclerView = new RecyclerView(getActivity()); 3687 final int[] preLayoutRange = new int[]{0, 10}; 3688 final int[] postLayoutRange = new int[]{0, 10}; 3689 final AtomicBoolean enableGetViewTest = new AtomicBoolean(false); 3690 final List<Integer> disappearingPositions = new ArrayList<>(); 3691 final TestLayoutManager tlm = new TestLayoutManager() { 3692 @Override 3693 public boolean supportsPredictiveItemAnimations() { 3694 return true; 3695 } 3696 3697 @Override 3698 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3699 try { 3700 final int[] layoutRange = state.isPreLayout() ? preLayoutRange 3701 : postLayoutRange; 3702 detachAndScrapAttachedViews(recycler); 3703 layoutRange(recycler, layoutRange[0], layoutRange[1]); 3704 if (!state.isPreLayout()) { 3705 for (Integer position : disappearingPositions) { 3706 // test sanity. 3707 assertNull(findViewByPosition(position)); 3708 final View view = recycler.getViewForPosition(position); 3709 assertNotNull(view); 3710 addDisappearingView(view); 3711 measureChildWithMargins(view, 0, 0); 3712 // position item out of bounds. 3713 view.layout(0, -500, view.getMeasuredWidth(), 3714 -500 + view.getMeasuredHeight()); 3715 } 3716 } 3717 } catch (Throwable t) { 3718 postExceptionToInstrumentation(t); 3719 } 3720 layoutLatch.countDown(); 3721 } 3722 }; 3723 recyclerView.getItemAnimator().setMoveDuration(4000); 3724 recyclerView.getItemAnimator().setRemoveDuration(4000); 3725 final TestAdapter adapter = new TestAdapter(100); 3726 recyclerView.setAdapter(adapter); 3727 recyclerView.setLayoutManager(tlm); 3728 tlm.expectLayouts(1); 3729 setRecyclerView(recyclerView); 3730 tlm.waitForLayout(1); 3731 checkForMainThreadException(); 3732 // now, a child disappears 3733 disappearingPositions.add(0); 3734 // layout one shifted 3735 postLayoutRange[0] = 1; 3736 postLayoutRange[1] = 11; 3737 tlm.expectLayouts(2); 3738 adapter.addAndNotify(8, 1); 3739 tlm.waitForLayout(2); 3740 checkForMainThreadException(); 3741 3742 tlm.expectLayouts(2); 3743 disappearingPositions.clear(); 3744 // now that item should be moving, invalidate it and delete it. 3745 enableGetViewTest.set(true); 3746 runTestOnUiThread(new Runnable() { 3747 @Override 3748 public void run() { 3749 try { 3750 assertThat("test sanity, should still be animating", 3751 mRecyclerView.isAnimating(), CoreMatchers.is(true)); 3752 adapter.changeAndNotify(0, 1); 3753 adapter.deleteAndNotify(0, 1); 3754 } catch (Throwable throwable) { 3755 fail(throwable.getMessage()); 3756 } 3757 } 3758 }); 3759 tlm.waitForLayout(2); 3760 checkForMainThreadException(); 3761 } 3762 3763 @Test 3764 public void focusBigViewOnTop() throws Throwable { 3765 focusTooBigViewTest(Gravity.TOP); 3766 } 3767 3768 @Test 3769 public void focusBigViewOnLeft() throws Throwable { 3770 focusTooBigViewTest(Gravity.LEFT); 3771 } 3772 3773 @Test 3774 public void focusBigViewOnRight() throws Throwable { 3775 focusTooBigViewTest(Gravity.RIGHT); 3776 } 3777 3778 @Test 3779 public void focusBigViewOnBottom() throws Throwable { 3780 focusTooBigViewTest(Gravity.BOTTOM); 3781 } 3782 3783 @Test 3784 public void focusBigViewOnLeftRTL() throws Throwable { 3785 focusTooBigViewTest(Gravity.LEFT, true); 3786 assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL, 3787 mRecyclerView.getLayoutManager().getLayoutDirection()); 3788 } 3789 3790 @Test 3791 public void focusBigViewOnRightRTL() throws Throwable { 3792 focusTooBigViewTest(Gravity.RIGHT, true); 3793 assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL, 3794 mRecyclerView.getLayoutManager().getLayoutDirection()); 3795 } 3796 3797 public void focusTooBigViewTest(final int gravity) throws Throwable { 3798 focusTooBigViewTest(gravity, false); 3799 } 3800 3801 public void focusTooBigViewTest(final int gravity, final boolean rtl) throws Throwable { 3802 RecyclerView rv = new RecyclerView(getActivity()); 3803 if (rtl) { 3804 ViewCompat.setLayoutDirection(rv, ViewCompat.LAYOUT_DIRECTION_RTL); 3805 } 3806 final AtomicInteger vScrollDist = new AtomicInteger(0); 3807 final AtomicInteger hScrollDist = new AtomicInteger(0); 3808 final AtomicInteger vDesiredDist = new AtomicInteger(0); 3809 final AtomicInteger hDesiredDist = new AtomicInteger(0); 3810 TestLayoutManager tlm = new TestLayoutManager() { 3811 3812 @Override 3813 public int getLayoutDirection() { 3814 return rtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR; 3815 } 3816 3817 @Override 3818 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3819 detachAndScrapAttachedViews(recycler); 3820 final View view = recycler.getViewForPosition(0); 3821 addView(view); 3822 int left = 0, top = 0; 3823 view.setBackgroundColor(Color.rgb(0, 0, 255)); 3824 switch (gravity) { 3825 case Gravity.LEFT: 3826 case Gravity.RIGHT: 3827 view.measure( 3828 View.MeasureSpec.makeMeasureSpec((int) (getWidth() * 1.5), 3829 View.MeasureSpec.EXACTLY), 3830 View.MeasureSpec.makeMeasureSpec((int) (getHeight() * .9), 3831 View.MeasureSpec.AT_MOST)); 3832 left = gravity == Gravity.LEFT ? getWidth() - view.getMeasuredWidth() - 80 3833 : 90; 3834 top = 0; 3835 if (ViewCompat.LAYOUT_DIRECTION_RTL == getLayoutDirection()) { 3836 hDesiredDist.set((left + view.getMeasuredWidth()) - getWidth()); 3837 } else { 3838 hDesiredDist.set(left); 3839 } 3840 break; 3841 case Gravity.TOP: 3842 case Gravity.BOTTOM: 3843 view.measure( 3844 View.MeasureSpec.makeMeasureSpec((int) (getWidth() * .9), 3845 View.MeasureSpec.AT_MOST), 3846 View.MeasureSpec.makeMeasureSpec((int) (getHeight() * 1.5), 3847 View.MeasureSpec.EXACTLY)); 3848 top = gravity == Gravity.TOP ? getHeight() - view.getMeasuredHeight() - 3849 80 : 90; 3850 left = 0; 3851 vDesiredDist.set(top); 3852 break; 3853 } 3854 3855 view.layout(left, top, left + view.getMeasuredWidth(), 3856 top + view.getMeasuredHeight()); 3857 layoutLatch.countDown(); 3858 } 3859 3860 @Override 3861 public boolean canScrollVertically() { 3862 return true; 3863 } 3864 3865 @Override 3866 public boolean canScrollHorizontally() { 3867 return super.canScrollHorizontally(); 3868 } 3869 3870 @Override 3871 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 3872 RecyclerView.State state) { 3873 vScrollDist.addAndGet(dy); 3874 getChildAt(0).offsetTopAndBottom(-dy); 3875 return dy; 3876 } 3877 3878 @Override 3879 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 3880 RecyclerView.State state) { 3881 hScrollDist.addAndGet(dx); 3882 getChildAt(0).offsetLeftAndRight(-dx); 3883 return dx; 3884 } 3885 }; 3886 TestAdapter adapter = new TestAdapter(10); 3887 rv.setAdapter(adapter); 3888 rv.setLayoutManager(tlm); 3889 tlm.expectLayouts(1); 3890 setRecyclerView(rv); 3891 tlm.waitForLayout(2); 3892 View view = rv.getChildAt(0); 3893 assertTrue("test sanity", requestFocus(view, true)); 3894 assertTrue("test sanity", view.hasFocus()); 3895 assertEquals(vDesiredDist.get(), vScrollDist.get()); 3896 assertEquals(hDesiredDist.get(), hScrollDist.get()); 3897 assertEquals(mRecyclerView.getPaddingTop(), view.getTop()); 3898 if (rtl) { 3899 assertEquals(mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(), 3900 view.getRight()); 3901 } else { 3902 assertEquals(mRecyclerView.getPaddingLeft(), view.getLeft()); 3903 } 3904 } 3905 3906 @Test 3907 public void firstLayoutWithAdapterChanges() throws Throwable { 3908 final TestAdapter adapter = new TestAdapter(0); 3909 final RecyclerView rv = new RecyclerView(getActivity()); 3910 setVisibility(rv, View.GONE); 3911 TestLayoutManager tlm = new TestLayoutManager() { 3912 @Override 3913 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3914 try { 3915 super.onLayoutChildren(recycler, state); 3916 layoutRange(recycler, 0, state.getItemCount()); 3917 } catch (Throwable t) { 3918 postExceptionToInstrumentation(t); 3919 } finally { 3920 layoutLatch.countDown(); 3921 } 3922 } 3923 3924 @Override 3925 public boolean supportsPredictiveItemAnimations() { 3926 return true; 3927 } 3928 }; 3929 rv.setLayoutManager(tlm); 3930 rv.setAdapter(adapter); 3931 rv.setHasFixedSize(true); 3932 setRecyclerView(rv); 3933 tlm.expectLayouts(1); 3934 tlm.assertNoLayout("test sanity, layout should not run", 1); 3935 getInstrumentation().waitForIdleSync(); 3936 runTestOnUiThread(new Runnable() { 3937 @Override 3938 public void run() { 3939 try { 3940 adapter.addAndNotify(2); 3941 } catch (Throwable throwable) { 3942 throwable.printStackTrace(); 3943 } 3944 rv.setVisibility(View.VISIBLE); 3945 } 3946 }); 3947 checkForMainThreadException(); 3948 tlm.waitForLayout(2); 3949 assertEquals(2, rv.getChildCount()); 3950 checkForMainThreadException(); 3951 } 3952 3953 @Test 3954 public void computeScrollOffsetWithoutLayoutManager() throws Throwable { 3955 RecyclerView rv = new RecyclerView(getActivity()); 3956 rv.setAdapter(new TestAdapter(10)); 3957 setRecyclerView(rv); 3958 assertEquals(0, rv.computeHorizontalScrollExtent()); 3959 assertEquals(0, rv.computeHorizontalScrollOffset()); 3960 assertEquals(0, rv.computeHorizontalScrollRange()); 3961 3962 assertEquals(0, rv.computeVerticalScrollExtent()); 3963 assertEquals(0, rv.computeVerticalScrollOffset()); 3964 assertEquals(0, rv.computeVerticalScrollRange()); 3965 } 3966 3967 @Test 3968 public void computeScrollOffsetWithoutAdapter() throws Throwable { 3969 RecyclerView rv = new RecyclerView(getActivity()); 3970 rv.setLayoutManager(new TestLayoutManager()); 3971 setRecyclerView(rv); 3972 assertEquals(0, rv.computeHorizontalScrollExtent()); 3973 assertEquals(0, rv.computeHorizontalScrollOffset()); 3974 assertEquals(0, rv.computeHorizontalScrollRange()); 3975 3976 assertEquals(0, rv.computeVerticalScrollExtent()); 3977 assertEquals(0, rv.computeVerticalScrollOffset()); 3978 assertEquals(0, rv.computeVerticalScrollRange()); 3979 } 3980 3981 @Test 3982 public void focusRectOnScreenWithDecorOffsets() throws Throwable { 3983 focusRectOnScreenTest(true); 3984 } 3985 3986 @Test 3987 public void focusRectOnScreenWithout() throws Throwable { 3988 focusRectOnScreenTest(false); 3989 } 3990 3991 public void focusRectOnScreenTest(boolean addItemDecors) throws Throwable { 3992 RecyclerView rv = new RecyclerView(getActivity()); 3993 final AtomicInteger scrollDist = new AtomicInteger(0); 3994 TestLayoutManager tlm = new TestLayoutManager() { 3995 @Override 3996 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3997 detachAndScrapAttachedViews(recycler); 3998 final View view = recycler.getViewForPosition(0); 3999 addView(view); 4000 measureChildWithMargins(view, 0, 0); 4001 view.layout(0, -20, view.getWidth(), 4002 -20 + view.getHeight());// ignore decors on purpose 4003 layoutLatch.countDown(); 4004 } 4005 4006 @Override 4007 public boolean canScrollVertically() { 4008 return true; 4009 } 4010 4011 @Override 4012 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 4013 RecyclerView.State state) { 4014 scrollDist.addAndGet(dy); 4015 return dy; 4016 } 4017 }; 4018 TestAdapter adapter = new TestAdapter(10); 4019 if (addItemDecors) { 4020 rv.addItemDecoration(new RecyclerView.ItemDecoration() { 4021 @Override 4022 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 4023 RecyclerView.State state) { 4024 outRect.set(0, 10, 0, 10); 4025 } 4026 }); 4027 } 4028 rv.setAdapter(adapter); 4029 rv.setLayoutManager(tlm); 4030 tlm.expectLayouts(1); 4031 setRecyclerView(rv); 4032 tlm.waitForLayout(2); 4033 4034 View view = rv.getChildAt(0); 4035 requestFocus(view, true); 4036 assertEquals(addItemDecors ? -30 : -20, scrollDist.get()); 4037 } 4038 4039 @Test 4040 public void unimplementedSmoothScroll() throws Throwable { 4041 final AtomicInteger receivedScrollToPosition = new AtomicInteger(-1); 4042 final AtomicInteger receivedSmoothScrollToPosition = new AtomicInteger(-1); 4043 final CountDownLatch cbLatch = new CountDownLatch(2); 4044 TestLayoutManager tlm = new TestLayoutManager() { 4045 @Override 4046 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 4047 detachAndScrapAttachedViews(recycler); 4048 layoutRange(recycler, 0, 10); 4049 layoutLatch.countDown(); 4050 } 4051 4052 @Override 4053 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 4054 int position) { 4055 assertEquals(-1, receivedSmoothScrollToPosition.get()); 4056 receivedSmoothScrollToPosition.set(position); 4057 RecyclerView.SmoothScroller ss = 4058 new LinearSmoothScroller(recyclerView.getContext()) { 4059 @Override 4060 public PointF computeScrollVectorForPosition(int targetPosition) { 4061 return null; 4062 } 4063 }; 4064 ss.setTargetPosition(position); 4065 startSmoothScroll(ss); 4066 cbLatch.countDown(); 4067 } 4068 4069 @Override 4070 public void scrollToPosition(int position) { 4071 assertEquals(-1, receivedScrollToPosition.get()); 4072 receivedScrollToPosition.set(position); 4073 cbLatch.countDown(); 4074 } 4075 }; 4076 RecyclerView rv = new RecyclerView(getActivity()); 4077 rv.setAdapter(new TestAdapter(100)); 4078 rv.setLayoutManager(tlm); 4079 tlm.expectLayouts(1); 4080 setRecyclerView(rv); 4081 tlm.waitForLayout(2); 4082 freezeLayout(true); 4083 smoothScrollToPosition(35, false); 4084 assertEquals("smoothScrollToPosition should be ignored when frozen", 4085 -1, receivedSmoothScrollToPosition.get()); 4086 freezeLayout(false); 4087 smoothScrollToPosition(35, false); 4088 assertTrue("both scrolls should be called", cbLatch.await(3, TimeUnit.SECONDS)); 4089 checkForMainThreadException(); 4090 assertEquals(35, receivedSmoothScrollToPosition.get()); 4091 assertEquals(35, receivedScrollToPosition.get()); 4092 } 4093 4094 @Test 4095 public void jumpingJackSmoothScroller() throws Throwable { 4096 jumpingJackSmoothScrollerTest(true); 4097 } 4098 4099 @Test 4100 public void jumpingJackSmoothScrollerGoesIdle() throws Throwable { 4101 jumpingJackSmoothScrollerTest(false); 4102 } 4103 4104 @Test 4105 public void testScrollByBeforeFirstLayout() throws Throwable { 4106 final RecyclerView recyclerView = new RecyclerView(getActivity()); 4107 TestAdapter adapter = new TestAdapter(10); 4108 recyclerView.setLayoutManager(new TestLayoutManager() { 4109 AtomicBoolean didLayout = new AtomicBoolean(false); 4110 @Override 4111 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 4112 super.onLayoutChildren(recycler, state); 4113 didLayout.set(true); 4114 } 4115 4116 @Override 4117 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 4118 RecyclerView.State state) { 4119 assertThat("should run layout before scroll", 4120 didLayout.get(), CoreMatchers.is(true)); 4121 return super.scrollVerticallyBy(dy, recycler, state); 4122 } 4123 4124 @Override 4125 public boolean canScrollVertically() { 4126 return true; 4127 } 4128 }); 4129 recyclerView.setAdapter(adapter); 4130 4131 runTestOnUiThread(new Runnable() { 4132 @Override 4133 public void run() { 4134 try { 4135 setRecyclerView(recyclerView); 4136 recyclerView.scrollBy(10, 19); 4137 } catch (Throwable throwable) { 4138 postExceptionToInstrumentation(throwable); 4139 } 4140 } 4141 }); 4142 4143 checkForMainThreadException(); 4144 } 4145 4146 private void jumpingJackSmoothScrollerTest(final boolean succeed) throws Throwable { 4147 final List<Integer> receivedScrollToPositions = new ArrayList<>(); 4148 final TestAdapter testAdapter = new TestAdapter(200); 4149 final AtomicBoolean mTargetFound = new AtomicBoolean(false); 4150 TestLayoutManager tlm = new TestLayoutManager() { 4151 int pendingScrollPosition = -1; 4152 @Override 4153 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 4154 detachAndScrapAttachedViews(recycler); 4155 final int pos = pendingScrollPosition < 0 ? 0: pendingScrollPosition; 4156 layoutRange(recycler, pos, pos + 10); 4157 if (layoutLatch != null) { 4158 layoutLatch.countDown(); 4159 } 4160 } 4161 4162 @Override 4163 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 4164 final int position) { 4165 RecyclerView.SmoothScroller ss = 4166 new LinearSmoothScroller(recyclerView.getContext()) { 4167 @Override 4168 public PointF computeScrollVectorForPosition(int targetPosition) { 4169 return new PointF(0, 1); 4170 } 4171 4172 @Override 4173 protected void onTargetFound(View targetView, RecyclerView.State state, 4174 Action action) { 4175 super.onTargetFound(targetView, state, action); 4176 mTargetFound.set(true); 4177 } 4178 4179 @Override 4180 protected void updateActionForInterimTarget(Action action) { 4181 int limit = succeed ? getTargetPosition() : 100; 4182 if (pendingScrollPosition + 2 < limit) { 4183 if (pendingScrollPosition != NO_POSITION) { 4184 assertEquals(pendingScrollPosition, 4185 getChildViewHolderInt(getChildAt(0)) 4186 .getAdapterPosition()); 4187 } 4188 action.jumpTo(pendingScrollPosition + 2); 4189 } 4190 } 4191 }; 4192 ss.setTargetPosition(position); 4193 startSmoothScroll(ss); 4194 } 4195 4196 @Override 4197 public void scrollToPosition(int position) { 4198 receivedScrollToPositions.add(position); 4199 pendingScrollPosition = position; 4200 requestLayout(); 4201 } 4202 }; 4203 final RecyclerView rv = new RecyclerView(getActivity()); 4204 rv.setAdapter(testAdapter); 4205 rv.setLayoutManager(tlm); 4206 4207 tlm.expectLayouts(1); 4208 setRecyclerView(rv); 4209 tlm.waitForLayout(2); 4210 4211 runTestOnUiThread(new Runnable() { 4212 @Override 4213 public void run() { 4214 rv.smoothScrollToPosition(150); 4215 } 4216 }); 4217 int limit = 100; 4218 while (rv.getLayoutManager().isSmoothScrolling() && --limit > 0) { 4219 Thread.sleep(200); 4220 checkForMainThreadException(); 4221 } 4222 checkForMainThreadException(); 4223 assertTrue(limit > 0); 4224 for (int i = 1; i < 100; i+=2) { 4225 assertTrue("scroll positions must include " + i, receivedScrollToPositions.contains(i)); 4226 } 4227 4228 assertEquals(succeed, mTargetFound.get()); 4229 4230 } 4231 4232 private static class TestViewHolder2 extends RecyclerView.ViewHolder { 4233 4234 Object mData; 4235 4236 public TestViewHolder2(View itemView) { 4237 super(itemView); 4238 } 4239 } 4240 4241 private static class TestAdapter2 extends RecyclerView.Adapter<TestViewHolder2> { 4242 4243 List<Item> mItems; 4244 4245 private TestAdapter2(int count) { 4246 mItems = new ArrayList<>(count); 4247 for (int i = 0; i < count; i++) { 4248 mItems.add(new Item(i, "Item " + i)); 4249 } 4250 } 4251 4252 @Override 4253 public TestViewHolder2 onCreateViewHolder(ViewGroup parent, 4254 int viewType) { 4255 return new TestViewHolder2(new TextView(parent.getContext())); 4256 } 4257 4258 @Override 4259 public void onBindViewHolder(TestViewHolder2 holder, int position) { 4260 final Item item = mItems.get(position); 4261 ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")"); 4262 } 4263 4264 @Override 4265 public int getItemCount() { 4266 return mItems.size(); 4267 } 4268 } 4269 4270 public interface AdapterRunnable { 4271 4272 void run(TestAdapter adapter) throws Throwable; 4273 } 4274 4275 public class LayoutAllLayoutManager extends TestLayoutManager { 4276 private final boolean mAllowNullLayoutLatch; 4277 4278 public LayoutAllLayoutManager() { 4279 // by default, we don't allow unexpected layouts. 4280 this(false); 4281 } 4282 public LayoutAllLayoutManager(boolean allowNullLayoutLatch) { 4283 mAllowNullLayoutLatch = allowNullLayoutLatch; 4284 } 4285 4286 4287 @Override 4288 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 4289 detachAndScrapAttachedViews(recycler); 4290 layoutRange(recycler, 0, state.getItemCount()); 4291 if (!mAllowNullLayoutLatch || layoutLatch != null) { 4292 layoutLatch.countDown(); 4293 } 4294 } 4295 } 4296 4297 /** 4298 * Proxy class to make protected methods public 4299 */ 4300 public static class TestRecyclerView extends RecyclerView { 4301 4302 public TestRecyclerView(Context context) { 4303 super(context); 4304 } 4305 4306 public TestRecyclerView(Context context, @Nullable AttributeSet attrs) { 4307 super(context, attrs); 4308 } 4309 4310 public TestRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { 4311 super(context, attrs, defStyle); 4312 } 4313 4314 @Override 4315 public void detachViewFromParent(int index) { 4316 super.detachViewFromParent(index); 4317 } 4318 4319 @Override 4320 public void attachViewToParent(View child, int index, ViewGroup.LayoutParams params) { 4321 super.attachViewToParent(child, index, params); 4322 } 4323 } 4324 4325 private interface ViewRunnable { 4326 void run(View view) throws RuntimeException; 4327 } 4328} 4329