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