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.junit.After; 21 import org.junit.Before; 22 import org.junit.Test; 23 import org.junit.runner.RunWith; 24 25 import android.support.test.InstrumentationRegistry; 26 import android.graphics.Color; 27 import android.graphics.PointF; 28 import android.graphics.Rect; 29 import android.os.SystemClock; 30 import android.support.v4.view.ViewCompat; 31 import android.support.v7.widget.RecyclerView; 32 import android.test.TouchUtils; 33 import android.util.Log; 34 import android.view.Gravity; 35 import android.view.KeyEvent; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.ViewConfiguration; 39 import android.view.ViewGroup; 40 import android.view.ViewTreeObserver; 41 import android.widget.TextView; 42 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.concurrent.CountDownLatch; 48 import java.util.concurrent.TimeUnit; 49 import java.util.concurrent.atomic.AtomicBoolean; 50 import java.util.concurrent.atomic.AtomicInteger; 51 52 import static android.support.v7.widget.RecyclerView.NO_POSITION; 53 import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE; 54 import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING; 55 import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING; 56 import static android.support.v7.widget.RecyclerView.getChildViewHolderInt; 57 import android.support.test.runner.AndroidJUnit4; 58 59 @RunWith(AndroidJUnit4.class) 60 public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest { 61 private static final int FLAG_HORIZONTAL = 1; 62 private static final int FLAG_VERTICAL = 1 << 1; 63 private static final int FLAG_FLING = 1 << 2; 64 65 private static final boolean DEBUG = true; 66 67 private static final String TAG = "RecyclerViewLayoutTest"; 68 RecyclerViewLayoutTest()69 public RecyclerViewLayoutTest() { 70 super(DEBUG); 71 } 72 73 @Before 74 @Override setUp()75 public void setUp() throws Exception { 76 super.setUp(); 77 injectInstrumentation(InstrumentationRegistry.getInstrumentation()); 78 } 79 80 @After 81 @Override tearDown()82 public void tearDown() throws Exception { 83 super.tearDown(); 84 } 85 86 @Test testFlingFrozen()87 public void testFlingFrozen() throws Throwable { 88 testScrollFrozen(true); 89 } 90 91 @Test testDragFrozen()92 public void testDragFrozen() throws Throwable { 93 testScrollFrozen(false); 94 } 95 testScrollFrozen(boolean fling)96 private void testScrollFrozen(boolean fling) throws Throwable { 97 RecyclerView recyclerView = new RecyclerView(getActivity()); 98 99 final int horizontalScrollCount = 3; 100 final int verticalScrollCount = 3; 101 final int horizontalVelocity = 1000; 102 final int verticalVelocity = 1000; 103 final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount); 104 final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount); 105 TestLayoutManager tlm = new TestLayoutManager() { 106 @Override 107 public boolean canScrollHorizontally() { 108 return true; 109 } 110 111 @Override 112 public boolean canScrollVertically() { 113 return true; 114 } 115 116 @Override 117 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 118 layoutRange(recycler, 0, 10); 119 layoutLatch.countDown(); 120 } 121 122 @Override 123 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 124 RecyclerView.State state) { 125 if (verticalCounter.get() > 0) { 126 verticalCounter.decrementAndGet(); 127 return dy; 128 } 129 return 0; 130 } 131 132 @Override 133 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 134 RecyclerView.State state) { 135 if (horizontalCounter.get() > 0) { 136 horizontalCounter.decrementAndGet(); 137 return dx; 138 } 139 return 0; 140 } 141 }; 142 TestAdapter adapter = new TestAdapter(100); 143 recyclerView.setAdapter(adapter); 144 recyclerView.setLayoutManager(tlm); 145 tlm.expectLayouts(1); 146 setRecyclerView(recyclerView); 147 tlm.waitForLayout(2); 148 149 freezeLayout(true); 150 151 if (fling) { 152 assertFalse("fling should be blocked", fling(horizontalVelocity, verticalVelocity)); 153 } else { // drag 154 TouchUtils.dragViewTo(this, recyclerView, 155 Gravity.LEFT | Gravity.TOP, 156 mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); 157 } 158 assertEquals("rv's horizontal scroll cb must not run", horizontalScrollCount, 159 horizontalCounter.get()); 160 assertEquals("rv's vertical scroll cb must not run", verticalScrollCount, 161 verticalCounter.get()); 162 163 freezeLayout(false); 164 165 if (fling) { 166 assertTrue("fling should be started", fling(horizontalVelocity, verticalVelocity)); 167 } else { // drag 168 TouchUtils.dragViewTo(this, recyclerView, 169 Gravity.LEFT | Gravity.TOP, 170 mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); 171 } 172 assertEquals("rv's horizontal scroll cb must finishes", 0, horizontalCounter.get()); 173 assertEquals("rv's vertical scroll cb must finishes", 0, verticalCounter.get()); 174 } 175 176 @Test testFocusSearchFailFrozen()177 public void testFocusSearchFailFrozen() throws Throwable { 178 RecyclerView recyclerView = new RecyclerView(getActivity()); 179 180 final AtomicInteger focusSearchCalled = new AtomicInteger(0); 181 TestLayoutManager tlm = new TestLayoutManager() { 182 @Override 183 public boolean canScrollHorizontally() { 184 return true; 185 } 186 187 @Override 188 public boolean canScrollVertically() { 189 return true; 190 } 191 192 @Override 193 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 194 layoutRange(recycler, 0, 10); 195 layoutLatch.countDown(); 196 } 197 198 @Override 199 public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, 200 RecyclerView.State state) { 201 focusSearchCalled.addAndGet(1); 202 return null; 203 } 204 }; 205 TestAdapter adapter = new TestAdapter(100); 206 recyclerView.setAdapter(adapter); 207 recyclerView.setLayoutManager(tlm); 208 tlm.expectLayouts(1); 209 setRecyclerView(recyclerView); 210 tlm.waitForLayout(2); 211 212 final View c = recyclerView.getChildAt(recyclerView.getChildCount() - 1); 213 runTestOnUiThread(new Runnable() { 214 @Override 215 public void run() { 216 c.requestFocus(); 217 } 218 }); 219 assertTrue(c.hasFocus()); 220 221 freezeLayout(true); 222 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 223 assertEquals("onFocusSearchFailed should not be called when layout is frozen", 224 0, focusSearchCalled.get()); 225 226 freezeLayout(false); 227 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 228 assertEquals(1, focusSearchCalled.get()); 229 } 230 231 @Test testFrozenAndChangeAdapter()232 public void testFrozenAndChangeAdapter() throws Throwable { 233 RecyclerView recyclerView = new RecyclerView(getActivity()); 234 235 final AtomicInteger focusSearchCalled = new AtomicInteger(0); 236 TestLayoutManager tlm = new TestLayoutManager() { 237 @Override 238 public boolean canScrollHorizontally() { 239 return true; 240 } 241 242 @Override 243 public boolean canScrollVertically() { 244 return true; 245 } 246 247 @Override 248 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 249 layoutRange(recycler, 0, 10); 250 layoutLatch.countDown(); 251 } 252 253 @Override 254 public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, 255 RecyclerView.State state) { 256 focusSearchCalled.addAndGet(1); 257 return null; 258 } 259 }; 260 TestAdapter adapter = new TestAdapter(100); 261 recyclerView.setAdapter(adapter); 262 recyclerView.setLayoutManager(tlm); 263 tlm.expectLayouts(1); 264 setRecyclerView(recyclerView); 265 tlm.waitForLayout(2); 266 267 freezeLayout(true); 268 TestAdapter adapter2 = new TestAdapter(1000); 269 setAdapter(adapter2); 270 assertFalse(recyclerView.isLayoutFrozen()); 271 assertSame(adapter2, recyclerView.getAdapter()); 272 273 freezeLayout(true); 274 TestAdapter adapter3 = new TestAdapter(1000); 275 swapAdapter(adapter3, true); 276 assertFalse(recyclerView.isLayoutFrozen()); 277 assertSame(adapter3, recyclerView.getAdapter()); 278 } 279 280 @Test testScrollToPositionCallback()281 public void testScrollToPositionCallback() throws Throwable { 282 RecyclerView recyclerView = new RecyclerView(getActivity()); 283 TestLayoutManager tlm = new TestLayoutManager() { 284 int scrollPos = RecyclerView.NO_POSITION; 285 286 @Override 287 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 288 layoutLatch.countDown(); 289 if (scrollPos == RecyclerView.NO_POSITION) { 290 layoutRange(recycler, 0, 10); 291 } else { 292 layoutRange(recycler, scrollPos, scrollPos + 10); 293 } 294 } 295 296 @Override 297 public void scrollToPosition(int position) { 298 scrollPos = position; 299 requestLayout(); 300 } 301 }; 302 recyclerView.setLayoutManager(tlm); 303 TestAdapter adapter = new TestAdapter(100); 304 recyclerView.setAdapter(adapter); 305 final AtomicInteger rvCounter = new AtomicInteger(0); 306 final AtomicInteger viewGroupCounter = new AtomicInteger(0); 307 recyclerView.getViewTreeObserver().addOnScrollChangedListener( 308 new ViewTreeObserver.OnScrollChangedListener() { 309 @Override 310 public void onScrollChanged() { 311 viewGroupCounter.incrementAndGet(); 312 } 313 }); 314 recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { 315 @Override 316 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 317 rvCounter.incrementAndGet(); 318 super.onScrolled(recyclerView, dx, dy); 319 } 320 }); 321 tlm.expectLayouts(1); 322 323 setRecyclerView(recyclerView); 324 tlm.waitForLayout(2); 325 // wait for draw :/ 326 Thread.sleep(1000); 327 328 assertEquals("RV on scroll should be called for initialization", 1, rvCounter.get()); 329 assertEquals("VTO on scroll should be called for initialization", 1, 330 viewGroupCounter.get()); 331 tlm.expectLayouts(1); 332 freezeLayout(true); 333 scrollToPosition(3); 334 tlm.assertNoLayout("scrollToPosition should be ignored", 2); 335 freezeLayout(false); 336 scrollToPosition(3); 337 tlm.waitForLayout(2); 338 assertEquals("RV on scroll should be called", 2, rvCounter.get()); 339 assertEquals("VTO on scroll should be called", 2, viewGroupCounter.get()); 340 tlm.expectLayouts(1); 341 requestLayoutOnUIThread(recyclerView); 342 tlm.waitForLayout(2); 343 // wait for draw :/ 344 Thread.sleep(1000); 345 assertEquals("on scroll should NOT be called", 2, rvCounter.get()); 346 assertEquals("on scroll should NOT be called", 2, viewGroupCounter.get()); 347 348 } 349 350 @Test testScrollInBothDirectionEqual()351 public void testScrollInBothDirectionEqual() throws Throwable { 352 scrollInBothDirection(3, 3, 1000, 1000); 353 } 354 355 @Test testScrollInBothDirectionMoreVertical()356 public void testScrollInBothDirectionMoreVertical() throws Throwable { 357 scrollInBothDirection(2, 3, 1000, 1000); 358 } 359 360 @Test testScrollInBothDirectionMoreHorizontal()361 public void testScrollInBothDirectionMoreHorizontal() throws Throwable { 362 scrollInBothDirection(3, 2, 1000, 1000); 363 } 364 365 @Test testScrollHorizontalOnly()366 public void testScrollHorizontalOnly() throws Throwable { 367 scrollInBothDirection(3, 0, 1000, 0); 368 } 369 370 @Test testScrollVerticalOnly()371 public void testScrollVerticalOnly() throws Throwable { 372 scrollInBothDirection(0, 3, 0, 1000); 373 } 374 375 @Test testScrollInBothDirectionEqualReverse()376 public void testScrollInBothDirectionEqualReverse() throws Throwable { 377 scrollInBothDirection(3, 3, -1000, -1000); 378 } 379 380 @Test testScrollInBothDirectionMoreVerticalReverse()381 public void testScrollInBothDirectionMoreVerticalReverse() throws Throwable { 382 scrollInBothDirection(2, 3, -1000, -1000); 383 } 384 385 @Test testScrollInBothDirectionMoreHorizontalReverse()386 public void testScrollInBothDirectionMoreHorizontalReverse() throws Throwable { 387 scrollInBothDirection(3, 2, -1000, -1000); 388 } 389 390 @Test testScrollHorizontalOnlyReverse()391 public void testScrollHorizontalOnlyReverse() throws Throwable { 392 scrollInBothDirection(3, 0, -1000, 0); 393 } 394 395 @Test testScrollVerticalOnlyReverse()396 public void testScrollVerticalOnlyReverse() throws Throwable { 397 scrollInBothDirection(0, 3, 0, -1000); 398 } 399 scrollInBothDirection(int horizontalScrollCount, int verticalScrollCount, int horizontalVelocity, int verticalVelocity)400 public void scrollInBothDirection(int horizontalScrollCount, int verticalScrollCount, 401 int horizontalVelocity, int verticalVelocity) 402 throws Throwable { 403 RecyclerView recyclerView = new RecyclerView(getActivity()); 404 final AtomicInteger horizontalCounter = new AtomicInteger(horizontalScrollCount); 405 final AtomicInteger verticalCounter = new AtomicInteger(verticalScrollCount); 406 TestLayoutManager tlm = new TestLayoutManager() { 407 @Override 408 public boolean canScrollHorizontally() { 409 return true; 410 } 411 412 @Override 413 public boolean canScrollVertically() { 414 return true; 415 } 416 417 @Override 418 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 419 layoutRange(recycler, 0, 10); 420 layoutLatch.countDown(); 421 } 422 423 @Override 424 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 425 RecyclerView.State state) { 426 if (verticalCounter.get() > 0) { 427 verticalCounter.decrementAndGet(); 428 return dy; 429 } 430 return 0; 431 } 432 433 @Override 434 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 435 RecyclerView.State state) { 436 if (horizontalCounter.get() > 0) { 437 horizontalCounter.decrementAndGet(); 438 return dx; 439 } 440 return 0; 441 } 442 }; 443 TestAdapter adapter = new TestAdapter(100); 444 recyclerView.setAdapter(adapter); 445 recyclerView.setLayoutManager(tlm); 446 tlm.expectLayouts(1); 447 setRecyclerView(recyclerView); 448 tlm.waitForLayout(2); 449 assertTrue("test sanity, fling must run", fling(horizontalVelocity, verticalVelocity)); 450 assertEquals("rv's horizontal scroll cb must run " + horizontalScrollCount + " times'", 0, 451 horizontalCounter.get()); 452 assertEquals("rv's vertical scroll cb must run " + verticalScrollCount + " times'", 0, 453 verticalCounter.get()); 454 } 455 456 @Test testDragHorizontal()457 public void testDragHorizontal() throws Throwable { 458 scrollInOtherOrientationTest(FLAG_HORIZONTAL); 459 } 460 461 @Test testDragVertical()462 public void testDragVertical() throws Throwable { 463 scrollInOtherOrientationTest(FLAG_VERTICAL); 464 } 465 466 @Test testFlingHorizontal()467 public void testFlingHorizontal() throws Throwable { 468 scrollInOtherOrientationTest(FLAG_HORIZONTAL | FLAG_FLING); 469 } 470 471 @Test testFlingVertical()472 public void testFlingVertical() throws Throwable { 473 scrollInOtherOrientationTest(FLAG_VERTICAL | FLAG_FLING); 474 } 475 476 @Test testNestedDragVertical()477 public void testNestedDragVertical() throws Throwable { 478 TestedFrameLayout tfl = getActivity().mContainer; 479 tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 480 tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 481 scrollInOtherOrientationTest(FLAG_VERTICAL, 0); 482 } 483 484 @Test testNestedDragHorizontal()485 public void testNestedDragHorizontal() throws Throwable { 486 TestedFrameLayout tfl = getActivity().mContainer; 487 tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 488 tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 489 scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0); 490 } 491 492 @Test testNestedDragHorizontalCallsStopNestedScroll()493 public void testNestedDragHorizontalCallsStopNestedScroll() throws Throwable { 494 TestedFrameLayout tfl = getActivity().mContainer; 495 tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 496 tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 497 scrollInOtherOrientationTest(FLAG_HORIZONTAL, 0); 498 assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled()); 499 } 500 501 @Test testNestedDragVerticalCallsStopNestedScroll()502 public void testNestedDragVerticalCallsStopNestedScroll() throws Throwable { 503 TestedFrameLayout tfl = getActivity().mContainer; 504 tfl.setNestedScrollMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 505 tfl.setNestedFlingMode(TestedFrameLayout.TEST_NESTED_SCROLL_MODE_CONSUME); 506 scrollInOtherOrientationTest(FLAG_VERTICAL, 0); 507 assertTrue("onStopNestedScroll called", tfl.stopNestedScrollCalled()); 508 } 509 scrollInOtherOrientationTest(int flags)510 private void scrollInOtherOrientationTest(int flags) 511 throws Throwable { 512 scrollInOtherOrientationTest(flags, flags); 513 } 514 scrollInOtherOrientationTest(final int flags, int expectedFlags)515 private void scrollInOtherOrientationTest(final int flags, int expectedFlags) throws Throwable { 516 RecyclerView recyclerView = new RecyclerView(getActivity()); 517 final AtomicBoolean scrolledHorizontal = new AtomicBoolean(false); 518 final AtomicBoolean scrolledVertical = new AtomicBoolean(false); 519 520 final TestLayoutManager tlm = new TestLayoutManager() { 521 @Override 522 public boolean canScrollHorizontally() { 523 return (flags & FLAG_HORIZONTAL) != 0; 524 } 525 526 @Override 527 public boolean canScrollVertically() { 528 return (flags & FLAG_VERTICAL) != 0; 529 } 530 531 @Override 532 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 533 layoutRange(recycler, 0, 10); 534 layoutLatch.countDown(); 535 } 536 537 @Override 538 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 539 RecyclerView.State state) { 540 scrolledVertical.set(true); 541 return super.scrollVerticallyBy(dy, recycler, state); 542 } 543 544 @Override 545 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 546 RecyclerView.State state) { 547 scrolledHorizontal.set(true); 548 return super.scrollHorizontallyBy(dx, recycler, state); 549 } 550 }; 551 TestAdapter adapter = new TestAdapter(100); 552 recyclerView.setAdapter(adapter); 553 recyclerView.setLayoutManager(tlm); 554 tlm.expectLayouts(1); 555 setRecyclerView(recyclerView); 556 tlm.waitForLayout(2); 557 if ( (flags & FLAG_FLING) != 0 ) { 558 int flingVelocity = (mRecyclerView.getMaxFlingVelocity() + 559 mRecyclerView.getMinFlingVelocity()) / 2; 560 assertEquals("fling started", (expectedFlags & FLAG_FLING) != 0, 561 fling(flingVelocity, flingVelocity)); 562 } else { // drag 563 TouchUtils.dragViewTo(this, recyclerView, Gravity.LEFT | Gravity.TOP, 564 mRecyclerView.getWidth() / 2, mRecyclerView.getHeight() / 2); 565 } 566 assertEquals("horizontally scrolled: " + tlm.mScrollHorizontallyAmount, 567 (expectedFlags & FLAG_HORIZONTAL) != 0, scrolledHorizontal.get()); 568 assertEquals("vertically scrolled: " + tlm.mScrollVerticallyAmount, 569 (expectedFlags & FLAG_VERTICAL) != 0, scrolledVertical.get()); 570 } 571 fling(final int velocityX, final int velocityY)572 private boolean fling(final int velocityX, final int velocityY) throws Throwable { 573 final AtomicBoolean didStart = new AtomicBoolean(false); 574 runTestOnUiThread(new Runnable() { 575 @Override 576 public void run() { 577 boolean result = mRecyclerView.fling(velocityX, velocityY); 578 didStart.set(result); 579 } 580 }); 581 if (!didStart.get()) { 582 return false; 583 } 584 // cannot set scroll listener in case it is subject to some test so instead doing a busy 585 // loop until state goes idle 586 while (mRecyclerView.getScrollState() != SCROLL_STATE_IDLE) { 587 getInstrumentation().waitForIdleSync(); 588 } 589 return true; 590 } 591 assertPendingUpdatesAndLayout(TestLayoutManager testLayoutManager, final Runnable runnable)592 private void assertPendingUpdatesAndLayout(TestLayoutManager testLayoutManager, 593 final Runnable runnable) throws Throwable { 594 testLayoutManager.expectLayouts(1); 595 runTestOnUiThread(new Runnable() { 596 @Override 597 public void run() { 598 runnable.run(); 599 assertTrue(mRecyclerView.hasPendingAdapterUpdates()); 600 } 601 }); 602 testLayoutManager.waitForLayout(1); 603 assertFalse(mRecyclerView.hasPendingAdapterUpdates()); 604 } 605 setupBasic(RecyclerView recyclerView, TestLayoutManager tlm, TestAdapter adapter, boolean waitForFirstLayout)606 private void setupBasic(RecyclerView recyclerView, TestLayoutManager tlm, 607 TestAdapter adapter, boolean waitForFirstLayout) throws Throwable { 608 recyclerView.setLayoutManager(tlm); 609 recyclerView.setAdapter(adapter); 610 if (waitForFirstLayout) { 611 tlm.expectLayouts(1); 612 setRecyclerView(recyclerView); 613 tlm.waitForLayout(1); 614 } else { 615 setRecyclerView(recyclerView); 616 } 617 } 618 619 @Test testHasPendingUpdatesBeforeFirstLayout()620 public void testHasPendingUpdatesBeforeFirstLayout() throws Throwable { 621 RecyclerView recyclerView = new RecyclerView(getActivity()); 622 TestLayoutManager layoutManager = new DumbLayoutManager(); 623 TestAdapter testAdapter = new TestAdapter(10); 624 setupBasic(recyclerView, layoutManager, testAdapter, false); 625 assertTrue(mRecyclerView.hasPendingAdapterUpdates()); 626 } 627 628 @Test testNoPendingUpdatesAfterLayout()629 public void testNoPendingUpdatesAfterLayout() throws Throwable { 630 RecyclerView recyclerView = new RecyclerView(getActivity()); 631 TestLayoutManager layoutManager = new DumbLayoutManager(); 632 TestAdapter testAdapter = new TestAdapter(10); 633 setupBasic(recyclerView, layoutManager, testAdapter, true); 634 assertFalse(mRecyclerView.hasPendingAdapterUpdates()); 635 } 636 637 @Test testHasPendingUpdatesWhenAdapterIsChanged()638 public void testHasPendingUpdatesWhenAdapterIsChanged() throws Throwable { 639 RecyclerView recyclerView = new RecyclerView(getActivity()); 640 TestLayoutManager layoutManager = new DumbLayoutManager(); 641 final TestAdapter testAdapter = new TestAdapter(10); 642 setupBasic(recyclerView, layoutManager, testAdapter, false); 643 assertPendingUpdatesAndLayout(layoutManager, new Runnable() { 644 @Override 645 public void run() { 646 testAdapter.notifyItemRemoved(1); 647 } 648 }); 649 assertPendingUpdatesAndLayout(layoutManager, new Runnable() { 650 @Override 651 public void run() { 652 testAdapter.notifyItemInserted(2); 653 } 654 }); 655 656 assertPendingUpdatesAndLayout(layoutManager, new Runnable() { 657 @Override 658 public void run() { 659 testAdapter.notifyItemMoved(2, 3); 660 } 661 }); 662 663 assertPendingUpdatesAndLayout(layoutManager, new Runnable() { 664 @Override 665 public void run() { 666 testAdapter.notifyItemChanged(2); 667 } 668 }); 669 670 assertPendingUpdatesAndLayout(layoutManager, new Runnable() { 671 @Override 672 public void run() { 673 testAdapter.notifyDataSetChanged(); 674 } 675 }); 676 } 677 678 @Test testTransientStateRecycleViaAdapter()679 public void testTransientStateRecycleViaAdapter() throws Throwable { 680 transientStateRecycleTest(true, false); 681 } 682 683 @Test testTransientStateRecycleViaTransientStateCleanup()684 public void testTransientStateRecycleViaTransientStateCleanup() throws Throwable { 685 transientStateRecycleTest(false, true); 686 } 687 688 @Test testTransientStateDontRecycle()689 public void testTransientStateDontRecycle() throws Throwable { 690 transientStateRecycleTest(false, false); 691 } 692 transientStateRecycleTest(final boolean succeed, final boolean unsetTransientState)693 public void transientStateRecycleTest(final boolean succeed, final boolean unsetTransientState) 694 throws Throwable { 695 final List<View> failedToRecycle = new ArrayList<View>(); 696 final List<View> recycled = new ArrayList<View>(); 697 TestAdapter testAdapter = new TestAdapter(10) { 698 @Override 699 public boolean onFailedToRecycleView( 700 TestViewHolder holder) { 701 failedToRecycle.add(holder.itemView); 702 if (unsetTransientState) { 703 setHasTransientState(holder.itemView, false); 704 } 705 return succeed; 706 } 707 708 @Override 709 public void onViewRecycled(TestViewHolder holder) { 710 recycled.add(holder.itemView); 711 super.onViewRecycled(holder); 712 } 713 }; 714 TestLayoutManager tlm = new TestLayoutManager() { 715 @Override 716 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 717 if (getChildCount() == 0) { 718 detachAndScrapAttachedViews(recycler); 719 layoutRange(recycler, 0, 5); 720 } else { 721 removeAndRecycleAllViews(recycler); 722 } 723 if (layoutLatch != null) { 724 layoutLatch.countDown(); 725 } 726 } 727 }; 728 RecyclerView recyclerView = new RecyclerView(getActivity()); 729 recyclerView.setAdapter(testAdapter); 730 recyclerView.setLayoutManager(tlm); 731 recyclerView.setItemAnimator(null); 732 setRecyclerView(recyclerView); 733 getInstrumentation().waitForIdleSync(); 734 // make sure we have enough views after this position so that we'll receive the on recycled 735 // callback 736 View view = recyclerView.getChildAt(3);//this has to be greater than def cache size. 737 setHasTransientState(view, true); 738 tlm.expectLayouts(1); 739 requestLayoutOnUIThread(recyclerView); 740 tlm.waitForLayout(2); 741 742 assertTrue(failedToRecycle.contains(view)); 743 assertEquals(succeed || unsetTransientState, recycled.contains(view)); 744 } 745 746 @Test testAdapterPositionInvalidation()747 public void testAdapterPositionInvalidation() throws Throwable { 748 final RecyclerView recyclerView = new RecyclerView(getActivity()); 749 final TestAdapter adapter = new TestAdapter(10); 750 final TestLayoutManager tlm = new TestLayoutManager() { 751 @Override 752 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 753 layoutRange(recycler, 0, state.getItemCount()); 754 layoutLatch.countDown(); 755 } 756 }; 757 recyclerView.setAdapter(adapter); 758 recyclerView.setLayoutManager(tlm); 759 tlm.expectLayouts(1); 760 setRecyclerView(recyclerView); 761 tlm.waitForLayout(1); 762 runTestOnUiThread(new Runnable() { 763 @Override 764 public void run() { 765 for (int i = 0; i < tlm.getChildCount(); i++) { 766 assertNotSame("adapter positions should not be undefined", 767 recyclerView.getChildAdapterPosition(tlm.getChildAt(i)), 768 RecyclerView.NO_POSITION); 769 } 770 adapter.notifyDataSetChanged(); 771 for (int i = 0; i < tlm.getChildCount(); i++) { 772 assertSame("adapter positions should be undefined", 773 recyclerView.getChildAdapterPosition(tlm.getChildAt(i)), 774 RecyclerView.NO_POSITION); 775 } 776 } 777 }); 778 } 779 780 @Test testAdapterPositionsBasic()781 public void testAdapterPositionsBasic() throws Throwable { 782 adapterPositionsTest(null); 783 } 784 785 @Test testAdapterPositionsRemoveItems()786 public void testAdapterPositionsRemoveItems() throws Throwable { 787 adapterPositionsTest(new AdapterRunnable() { 788 @Override 789 public void run(TestAdapter adapter) throws Throwable { 790 adapter.deleteAndNotify(3, 4); 791 } 792 }); 793 } 794 795 @Test testAdapterPositionsRemoveItemsBefore()796 public void testAdapterPositionsRemoveItemsBefore() throws Throwable { 797 adapterPositionsTest(new AdapterRunnable() { 798 @Override 799 public void run(TestAdapter adapter) throws Throwable { 800 adapter.deleteAndNotify(0, 1); 801 } 802 }); 803 } 804 805 @Test testAdapterPositionsAddItemsBefore()806 public void testAdapterPositionsAddItemsBefore() throws Throwable { 807 adapterPositionsTest(new AdapterRunnable() { 808 @Override 809 public void run(TestAdapter adapter) throws Throwable { 810 adapter.addAndNotify(0, 5); 811 } 812 }); 813 } 814 815 @Test testAdapterPositionsAddItemsInside()816 public void testAdapterPositionsAddItemsInside() throws Throwable { 817 adapterPositionsTest(new AdapterRunnable() { 818 @Override 819 public void run(TestAdapter adapter) throws Throwable { 820 adapter.addAndNotify(3, 2); 821 } 822 }); 823 } 824 825 @Test testAdapterPositionsMoveItems()826 public void testAdapterPositionsMoveItems() throws Throwable { 827 adapterPositionsTest(new AdapterRunnable() { 828 @Override 829 public void run(TestAdapter adapter) throws Throwable { 830 adapter.moveAndNotify(3, 5); 831 } 832 }); 833 } 834 835 @Test testAdapterPositionsNotifyDataSetChanged()836 public void testAdapterPositionsNotifyDataSetChanged() throws Throwable { 837 adapterPositionsTest(new AdapterRunnable() { 838 @Override 839 public void run(TestAdapter adapter) throws Throwable { 840 adapter.mItems.clear(); 841 for (int i = 0; i < 20; i++) { 842 adapter.mItems.add(new Item(i, "added item")); 843 } 844 adapter.notifyDataSetChanged(); 845 } 846 }); 847 } 848 849 @Test testAvoidLeakingRecyclerViewIfViewIsNotRecycled()850 public void testAvoidLeakingRecyclerViewIfViewIsNotRecycled() throws Throwable { 851 final AtomicBoolean failedToRecycle = new AtomicBoolean(false); 852 RecyclerView rv = new RecyclerView(getActivity()); 853 TestLayoutManager tlm = new TestLayoutManager() { 854 @Override 855 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 856 detachAndScrapAttachedViews(recycler); 857 layoutRange(recycler, 0, state.getItemCount()); 858 layoutLatch.countDown(); 859 } 860 }; 861 TestAdapter adapter = new TestAdapter(10) { 862 @Override 863 public boolean onFailedToRecycleView( 864 TestViewHolder holder) { 865 failedToRecycle.set(true); 866 return false; 867 } 868 }; 869 rv.setAdapter(adapter); 870 rv.setLayoutManager(tlm); 871 tlm.expectLayouts(1); 872 setRecyclerView(rv); 873 tlm.waitForLayout(1); 874 final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0)); 875 runTestOnUiThread(new Runnable() { 876 @Override 877 public void run() { 878 ViewCompat.setHasTransientState(vh.itemView, true); 879 } 880 }); 881 tlm.expectLayouts(1); 882 adapter.deleteAndNotify(0, 10); 883 tlm.waitForLayout(2); 884 final CountDownLatch animationsLatch = new CountDownLatch(1); 885 rv.getItemAnimator().isRunning( 886 new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 887 @Override 888 public void onAnimationsFinished() { 889 animationsLatch.countDown(); 890 } 891 }); 892 assertTrue(animationsLatch.await(2, TimeUnit.SECONDS)); 893 assertTrue(failedToRecycle.get()); 894 assertNull(vh.mOwnerRecyclerView); 895 checkForMainThreadException(); 896 } 897 898 @Test testAvoidLeakingRecyclerViewViaViewHolder()899 public void testAvoidLeakingRecyclerViewViaViewHolder() throws Throwable { 900 RecyclerView rv = new RecyclerView(getActivity()); 901 TestLayoutManager tlm = new TestLayoutManager() { 902 @Override 903 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 904 detachAndScrapAttachedViews(recycler); 905 layoutRange(recycler, 0, state.getItemCount()); 906 layoutLatch.countDown(); 907 } 908 }; 909 TestAdapter adapter = new TestAdapter(10); 910 rv.setAdapter(adapter); 911 rv.setLayoutManager(tlm); 912 tlm.expectLayouts(1); 913 setRecyclerView(rv); 914 tlm.waitForLayout(1); 915 final RecyclerView.ViewHolder vh = rv.getChildViewHolder(rv.getChildAt(0)); 916 tlm.expectLayouts(1); 917 adapter.deleteAndNotify(0, 10); 918 tlm.waitForLayout(2); 919 final CountDownLatch animationsLatch = new CountDownLatch(1); 920 rv.getItemAnimator().isRunning( 921 new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 922 @Override 923 public void onAnimationsFinished() { 924 animationsLatch.countDown(); 925 } 926 }); 927 assertTrue(animationsLatch.await(2, TimeUnit.SECONDS)); 928 assertNull(vh.mOwnerRecyclerView); 929 checkForMainThreadException(); 930 } 931 adapterPositionsTest(final AdapterRunnable adapterChanges)932 public void adapterPositionsTest(final AdapterRunnable adapterChanges) throws Throwable { 933 final TestAdapter testAdapter = new TestAdapter(10); 934 TestLayoutManager tlm = new TestLayoutManager() { 935 @Override 936 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 937 try { 938 layoutRange(recycler, Math.min(state.getItemCount(), 2) 939 , Math.min(state.getItemCount(), 7)); 940 layoutLatch.countDown(); 941 } catch (Throwable t) { 942 postExceptionToInstrumentation(t); 943 } 944 } 945 }; 946 final RecyclerView recyclerView = new RecyclerView(getActivity()); 947 recyclerView.setLayoutManager(tlm); 948 recyclerView.setAdapter(testAdapter); 949 tlm.expectLayouts(1); 950 setRecyclerView(recyclerView); 951 tlm.waitForLayout(1); 952 runTestOnUiThread(new Runnable() { 953 @Override 954 public void run() { 955 try { 956 final int count = recyclerView.getChildCount(); 957 Map<View, Integer> layoutPositions = new HashMap<View, Integer>(); 958 assertTrue("test sanity", count > 0); 959 for (int i = 0; i < count; i++) { 960 View view = recyclerView.getChildAt(i); 961 TestViewHolder vh = (TestViewHolder) recyclerView.getChildViewHolder(view); 962 int index = testAdapter.mItems.indexOf(vh.mBoundItem); 963 assertEquals("should be able to find VH with adapter position " + index, vh, 964 recyclerView.findViewHolderForAdapterPosition(index)); 965 assertEquals("get adapter position should return correct index", index, 966 vh.getAdapterPosition()); 967 layoutPositions.put(view, vh.mPosition); 968 } 969 if (adapterChanges != null) { 970 adapterChanges.run(testAdapter); 971 for (int i = 0; i < count; i++) { 972 View view = recyclerView.getChildAt(i); 973 TestViewHolder vh = (TestViewHolder) recyclerView 974 .getChildViewHolder(view); 975 int index = testAdapter.mItems.indexOf(vh.mBoundItem); 976 if (index >= 0) { 977 assertEquals("should be able to find VH with adapter position " 978 + index, vh, 979 recyclerView.findViewHolderForAdapterPosition(index)); 980 } 981 assertSame("get adapter position should return correct index", index, 982 vh.getAdapterPosition()); 983 assertSame("should be able to find view with layout position", 984 vh, mRecyclerView.findViewHolderForLayoutPosition( 985 layoutPositions.get(view))); 986 } 987 988 } 989 990 } catch (Throwable t) { 991 postExceptionToInstrumentation(t); 992 } 993 } 994 }); 995 checkForMainThreadException(); 996 } 997 998 @Test testScrollStateForSmoothScroll()999 public void testScrollStateForSmoothScroll() throws Throwable { 1000 TestAdapter testAdapter = new TestAdapter(10); 1001 TestLayoutManager tlm = new TestLayoutManager(); 1002 RecyclerView recyclerView = new RecyclerView(getActivity()); 1003 recyclerView.setAdapter(testAdapter); 1004 recyclerView.setLayoutManager(tlm); 1005 setRecyclerView(recyclerView); 1006 getInstrumentation().waitForIdleSync(); 1007 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1008 final int[] stateCnts = new int[10]; 1009 final CountDownLatch latch = new CountDownLatch(2); 1010 recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { 1011 @Override 1012 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1013 stateCnts[newState] = stateCnts[newState] + 1; 1014 latch.countDown(); 1015 } 1016 }); 1017 runTestOnUiThread(new Runnable() { 1018 @Override 1019 public void run() { 1020 mRecyclerView.smoothScrollBy(0, 500); 1021 } 1022 }); 1023 latch.await(5, TimeUnit.SECONDS); 1024 assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); 1025 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 1026 assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); 1027 } 1028 1029 @Test testScrollStateForSmoothScrollWithStop()1030 public void testScrollStateForSmoothScrollWithStop() throws Throwable { 1031 TestAdapter testAdapter = new TestAdapter(10); 1032 TestLayoutManager tlm = new TestLayoutManager(); 1033 RecyclerView recyclerView = new RecyclerView(getActivity()); 1034 recyclerView.setAdapter(testAdapter); 1035 recyclerView.setLayoutManager(tlm); 1036 setRecyclerView(recyclerView); 1037 getInstrumentation().waitForIdleSync(); 1038 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1039 final int[] stateCnts = new int[10]; 1040 final CountDownLatch latch = new CountDownLatch(1); 1041 recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { 1042 @Override 1043 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1044 stateCnts[newState] = stateCnts[newState] + 1; 1045 latch.countDown(); 1046 } 1047 }); 1048 runTestOnUiThread(new Runnable() { 1049 @Override 1050 public void run() { 1051 mRecyclerView.smoothScrollBy(0, 500); 1052 } 1053 }); 1054 latch.await(5, TimeUnit.SECONDS); 1055 runTestOnUiThread(new Runnable() { 1056 @Override 1057 public void run() { 1058 mRecyclerView.stopScroll(); 1059 } 1060 }); 1061 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1062 assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); 1063 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 1064 assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); 1065 } 1066 1067 @Test testScrollStateForFling()1068 public void testScrollStateForFling() throws Throwable { 1069 TestAdapter testAdapter = new TestAdapter(10); 1070 TestLayoutManager tlm = new TestLayoutManager(); 1071 RecyclerView recyclerView = new RecyclerView(getActivity()); 1072 recyclerView.setAdapter(testAdapter); 1073 recyclerView.setLayoutManager(tlm); 1074 setRecyclerView(recyclerView); 1075 getInstrumentation().waitForIdleSync(); 1076 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1077 final int[] stateCnts = new int[10]; 1078 final CountDownLatch latch = new CountDownLatch(2); 1079 recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { 1080 @Override 1081 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1082 stateCnts[newState] = stateCnts[newState] + 1; 1083 latch.countDown(); 1084 } 1085 }); 1086 final ViewConfiguration vc = ViewConfiguration.get(getActivity()); 1087 final float fling = vc.getScaledMinimumFlingVelocity() 1088 + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .1f; 1089 runTestOnUiThread(new Runnable() { 1090 @Override 1091 public void run() { 1092 mRecyclerView.fling(0, Math.round(fling)); 1093 } 1094 }); 1095 latch.await(5, TimeUnit.SECONDS); 1096 assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); 1097 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 1098 assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); 1099 } 1100 1101 @Test testScrollStateForFlingWithStop()1102 public void testScrollStateForFlingWithStop() throws Throwable { 1103 TestAdapter testAdapter = new TestAdapter(10); 1104 TestLayoutManager tlm = new TestLayoutManager(); 1105 RecyclerView recyclerView = new RecyclerView(getActivity()); 1106 recyclerView.setAdapter(testAdapter); 1107 recyclerView.setLayoutManager(tlm); 1108 setRecyclerView(recyclerView); 1109 getInstrumentation().waitForIdleSync(); 1110 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1111 final int[] stateCnts = new int[10]; 1112 final CountDownLatch latch = new CountDownLatch(1); 1113 recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { 1114 @Override 1115 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1116 stateCnts[newState] = stateCnts[newState] + 1; 1117 latch.countDown(); 1118 } 1119 }); 1120 final ViewConfiguration vc = ViewConfiguration.get(getActivity()); 1121 final float fling = vc.getScaledMinimumFlingVelocity() 1122 + (vc.getScaledMaximumFlingVelocity() - vc.getScaledMinimumFlingVelocity()) * .8f; 1123 runTestOnUiThread(new Runnable() { 1124 @Override 1125 public void run() { 1126 mRecyclerView.fling(0, Math.round(fling)); 1127 } 1128 }); 1129 latch.await(5, TimeUnit.SECONDS); 1130 runTestOnUiThread(new Runnable() { 1131 @Override 1132 public void run() { 1133 mRecyclerView.stopScroll(); 1134 } 1135 }); 1136 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1137 assertEquals(1, stateCnts[SCROLL_STATE_SETTLING]); 1138 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 1139 assertEquals(0, stateCnts[SCROLL_STATE_DRAGGING]); 1140 } 1141 1142 @Test testScrollStateDrag()1143 public void testScrollStateDrag() throws Throwable { 1144 TestAdapter testAdapter = new TestAdapter(10); 1145 TestLayoutManager tlm = new TestLayoutManager(); 1146 RecyclerView recyclerView = new RecyclerView(getActivity()); 1147 recyclerView.setAdapter(testAdapter); 1148 recyclerView.setLayoutManager(tlm); 1149 setRecyclerView(recyclerView); 1150 getInstrumentation().waitForIdleSync(); 1151 assertEquals(SCROLL_STATE_IDLE, recyclerView.getScrollState()); 1152 final int[] stateCnts = new int[10]; 1153 recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { 1154 @Override 1155 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1156 stateCnts[newState] = stateCnts[newState] + 1; 1157 } 1158 }); 1159 drag(mRecyclerView, 0, 0, 0, 500, 5); 1160 assertEquals(0, stateCnts[SCROLL_STATE_SETTLING]); 1161 assertEquals(1, stateCnts[SCROLL_STATE_IDLE]); 1162 assertEquals(1, stateCnts[SCROLL_STATE_DRAGGING]); 1163 } 1164 drag(ViewGroup view, float fromX, float toX, float fromY, float toY, int stepCount)1165 public void drag(ViewGroup view, float fromX, float toX, float fromY, float toY, 1166 int stepCount) throws Throwable { 1167 long downTime = SystemClock.uptimeMillis(); 1168 long eventTime = SystemClock.uptimeMillis(); 1169 1170 float y = fromY; 1171 float x = fromX; 1172 1173 float yStep = (toY - fromY) / stepCount; 1174 float xStep = (toX - fromX) / stepCount; 1175 1176 MotionEvent event = MotionEvent.obtain(downTime, eventTime, 1177 MotionEvent.ACTION_DOWN, x, y, 0); 1178 sendTouch(view, event); 1179 for (int i = 0; i < stepCount; ++i) { 1180 y += yStep; 1181 x += xStep; 1182 eventTime = SystemClock.uptimeMillis(); 1183 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); 1184 sendTouch(view, event); 1185 } 1186 1187 eventTime = SystemClock.uptimeMillis(); 1188 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); 1189 sendTouch(view, event); 1190 getInstrumentation().waitForIdleSync(); 1191 } 1192 sendTouch(final ViewGroup view, final MotionEvent event)1193 private void sendTouch(final ViewGroup view, final MotionEvent event) throws Throwable { 1194 runTestOnUiThread(new Runnable() { 1195 @Override 1196 public void run() { 1197 if (view.onInterceptTouchEvent(event)) { 1198 view.onTouchEvent(event); 1199 } 1200 } 1201 }); 1202 } 1203 1204 @Test testRecycleScrap()1205 public void testRecycleScrap() throws Throwable { 1206 recycleScrapTest(false); 1207 removeRecyclerView(); 1208 recycleScrapTest(true); 1209 } 1210 recycleScrapTest(final boolean useRecycler)1211 public void recycleScrapTest(final boolean useRecycler) throws Throwable { 1212 TestAdapter testAdapter = new TestAdapter(10); 1213 final AtomicBoolean test = new AtomicBoolean(false); 1214 TestLayoutManager lm = new TestLayoutManager() { 1215 @Override 1216 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1217 if (test.get()) { 1218 try { 1219 detachAndScrapAttachedViews(recycler); 1220 for (int i = recycler.getScrapList().size() - 1; i >= 0; i--) { 1221 if (useRecycler) { 1222 recycler.recycleView(recycler.getScrapList().get(i).itemView); 1223 } else { 1224 removeAndRecycleView(recycler.getScrapList().get(i).itemView, 1225 recycler); 1226 } 1227 } 1228 if (state.mOldChangedHolders != null) { 1229 for (int i = state.mOldChangedHolders.size() - 1; i >= 0; i--) { 1230 if (useRecycler) { 1231 recycler.recycleView( 1232 state.mOldChangedHolders.valueAt(i).itemView); 1233 } else { 1234 removeAndRecycleView( 1235 state.mOldChangedHolders.valueAt(i).itemView, recycler); 1236 } 1237 } 1238 } 1239 assertEquals("no scrap should be left over", 0, recycler.getScrapCount()); 1240 assertEquals("pre layout map should be empty", 0, 1241 state.mPreLayoutHolderMap.size()); 1242 assertEquals("post layout map should be empty", 0, 1243 state.mPostLayoutHolderMap.size()); 1244 if (state.mOldChangedHolders != null) { 1245 assertEquals("post old change map should be empty", 0, 1246 state.mOldChangedHolders.size()); 1247 } 1248 } catch (Throwable t) { 1249 postExceptionToInstrumentation(t); 1250 } 1251 1252 } 1253 layoutRange(recycler, 0, 5); 1254 layoutLatch.countDown(); 1255 super.onLayoutChildren(recycler, state); 1256 } 1257 }; 1258 RecyclerView recyclerView = new RecyclerView(getActivity()); 1259 recyclerView.setAdapter(testAdapter); 1260 recyclerView.setLayoutManager(lm); 1261 recyclerView.getItemAnimator().setSupportsChangeAnimations(true); 1262 lm.expectLayouts(1); 1263 setRecyclerView(recyclerView); 1264 lm.waitForLayout(2); 1265 test.set(true); 1266 lm.expectLayouts(1); 1267 testAdapter.changeAndNotify(3, 1); 1268 lm.waitForLayout(2); 1269 checkForMainThreadException(); 1270 } 1271 1272 @Test testAccessRecyclerOnOnMeasure()1273 public void testAccessRecyclerOnOnMeasure() throws Throwable { 1274 accessRecyclerOnOnMeasureTest(false); 1275 removeRecyclerView(); 1276 accessRecyclerOnOnMeasureTest(true); 1277 } 1278 1279 @Test testSmoothScrollWithRemovedItemsAndRemoveItem()1280 public void testSmoothScrollWithRemovedItemsAndRemoveItem() throws Throwable { 1281 smoothScrollTest(true); 1282 } 1283 1284 @Test testSmoothScrollWithRemovedItems()1285 public void testSmoothScrollWithRemovedItems() throws Throwable { 1286 smoothScrollTest(false); 1287 } 1288 smoothScrollTest(final boolean removeItem)1289 public void smoothScrollTest(final boolean removeItem) throws Throwable { 1290 final LinearSmoothScroller[] lss = new LinearSmoothScroller[1]; 1291 final CountDownLatch calledOnStart = new CountDownLatch(1); 1292 final CountDownLatch calledOnStop = new CountDownLatch(1); 1293 final int visibleChildCount = 10; 1294 TestLayoutManager lm = new TestLayoutManager() { 1295 int start = 0; 1296 1297 @Override 1298 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1299 super.onLayoutChildren(recycler, state); 1300 layoutRange(recycler, start, visibleChildCount); 1301 layoutLatch.countDown(); 1302 } 1303 1304 @Override 1305 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 1306 RecyclerView.State state) { 1307 start++; 1308 if (DEBUG) { 1309 Log.d(TAG, "on scroll, remove and recycling. start:" + start + ", cnt:" 1310 + visibleChildCount); 1311 } 1312 removeAndRecycleAllViews(recycler); 1313 layoutRange(recycler, start, 1314 Math.max(state.getItemCount(), start + visibleChildCount)); 1315 return dy; 1316 } 1317 1318 @Override 1319 public boolean canScrollVertically() { 1320 return true; 1321 } 1322 1323 @Override 1324 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 1325 int position) { 1326 LinearSmoothScroller linearSmoothScroller = 1327 new LinearSmoothScroller(recyclerView.getContext()) { 1328 @Override 1329 public PointF computeScrollVectorForPosition(int targetPosition) { 1330 return new PointF(0, 1); 1331 } 1332 1333 @Override 1334 protected void onStart() { 1335 super.onStart(); 1336 calledOnStart.countDown(); 1337 } 1338 1339 @Override 1340 protected void onStop() { 1341 super.onStop(); 1342 calledOnStop.countDown(); 1343 } 1344 }; 1345 linearSmoothScroller.setTargetPosition(position); 1346 lss[0] = linearSmoothScroller; 1347 startSmoothScroll(linearSmoothScroller); 1348 } 1349 }; 1350 final RecyclerView rv = new RecyclerView(getActivity()); 1351 TestAdapter testAdapter = new TestAdapter(500); 1352 rv.setLayoutManager(lm); 1353 rv.setAdapter(testAdapter); 1354 lm.expectLayouts(1); 1355 setRecyclerView(rv); 1356 lm.waitForLayout(1); 1357 // regular scroll 1358 final int targetPosition = visibleChildCount * (removeItem ? 30 : 4); 1359 runTestOnUiThread(new Runnable() { 1360 @Override 1361 public void run() { 1362 rv.smoothScrollToPosition(targetPosition); 1363 } 1364 }); 1365 if (DEBUG) { 1366 Log.d(TAG, "scrolling to target position " + targetPosition); 1367 } 1368 assertTrue("on start should be called very soon", calledOnStart.await(2, TimeUnit.SECONDS)); 1369 if (removeItem) { 1370 final int newTarget = targetPosition - 10; 1371 testAdapter.deleteAndNotify(newTarget + 1, testAdapter.getItemCount() - newTarget - 1); 1372 final CountDownLatch targetCheck = new CountDownLatch(1); 1373 runTestOnUiThread(new Runnable() { 1374 @Override 1375 public void run() { 1376 ViewCompat.postOnAnimationDelayed(rv, new Runnable() { 1377 @Override 1378 public void run() { 1379 try { 1380 assertEquals("scroll position should be updated to next available", 1381 newTarget, lss[0].getTargetPosition()); 1382 } catch (Throwable t) { 1383 postExceptionToInstrumentation(t); 1384 } 1385 targetCheck.countDown(); 1386 } 1387 }, 50); 1388 } 1389 }); 1390 assertTrue("target position should be checked on time ", 1391 targetCheck.await(10, TimeUnit.SECONDS)); 1392 checkForMainThreadException(); 1393 assertTrue("on stop should be called", calledOnStop.await(30, TimeUnit.SECONDS)); 1394 checkForMainThreadException(); 1395 assertNotNull("should scroll to new target " + newTarget 1396 , rv.findViewHolderForLayoutPosition(newTarget)); 1397 if (DEBUG) { 1398 Log.d(TAG, "on stop has been called on time"); 1399 } 1400 } else { 1401 assertTrue("on stop should be called eventually", 1402 calledOnStop.await(30, TimeUnit.SECONDS)); 1403 assertNotNull("scroll to position should succeed", 1404 rv.findViewHolderForLayoutPosition(targetPosition)); 1405 } 1406 checkForMainThreadException(); 1407 } 1408 1409 @Test testConsecutiveSmoothScroll()1410 public void testConsecutiveSmoothScroll() throws Throwable { 1411 final AtomicInteger visibleChildCount = new AtomicInteger(10); 1412 final AtomicInteger totalScrolled = new AtomicInteger(0); 1413 final TestLayoutManager lm = new TestLayoutManager() { 1414 int start = 0; 1415 1416 @Override 1417 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1418 super.onLayoutChildren(recycler, state); 1419 layoutRange(recycler, start, visibleChildCount.get()); 1420 layoutLatch.countDown(); 1421 } 1422 1423 @Override 1424 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 1425 RecyclerView.State state) { 1426 totalScrolled.set(totalScrolled.get() + dy); 1427 return dy; 1428 } 1429 1430 @Override 1431 public boolean canScrollVertically() { 1432 return true; 1433 } 1434 }; 1435 final RecyclerView rv = new RecyclerView(getActivity()); 1436 TestAdapter testAdapter = new TestAdapter(500); 1437 rv.setLayoutManager(lm); 1438 rv.setAdapter(testAdapter); 1439 lm.expectLayouts(1); 1440 setRecyclerView(rv); 1441 lm.waitForLayout(1); 1442 runTestOnUiThread(new Runnable() { 1443 @Override 1444 public void run() { 1445 rv.smoothScrollBy(0, 2000); 1446 } 1447 }); 1448 Thread.sleep(250); 1449 final AtomicInteger scrollAmt = new AtomicInteger(); 1450 runTestOnUiThread(new Runnable() { 1451 @Override 1452 public void run() { 1453 final int soFar = totalScrolled.get(); 1454 scrollAmt.set(soFar); 1455 rv.smoothScrollBy(0, 5000 - soFar); 1456 } 1457 }); 1458 while (rv.getScrollState() != SCROLL_STATE_IDLE) { 1459 Thread.sleep(100); 1460 } 1461 final int soFar = totalScrolled.get(); 1462 assertEquals("second scroll should be competed properly", 5000, soFar); 1463 } 1464 accessRecyclerOnOnMeasureTest(final boolean enablePredictiveAnimations)1465 public void accessRecyclerOnOnMeasureTest(final boolean enablePredictiveAnimations) 1466 throws Throwable { 1467 TestAdapter testAdapter = new TestAdapter(10); 1468 final AtomicInteger expectedOnMeasureStateCount = new AtomicInteger(10); 1469 TestLayoutManager lm = new TestLayoutManager() { 1470 @Override 1471 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1472 super.onLayoutChildren(recycler, state); 1473 try { 1474 layoutRange(recycler, 0, state.getItemCount()); 1475 layoutLatch.countDown(); 1476 } catch (Throwable t) { 1477 postExceptionToInstrumentation(t); 1478 } finally { 1479 layoutLatch.countDown(); 1480 } 1481 } 1482 1483 @Override 1484 public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, 1485 int widthSpec, int heightSpec) { 1486 try { 1487 // make sure we access all views 1488 for (int i = 0; i < state.getItemCount(); i++) { 1489 View view = recycler.getViewForPosition(i); 1490 assertNotNull(view); 1491 assertEquals(i, getPosition(view)); 1492 } 1493 assertEquals(state.toString(), 1494 expectedOnMeasureStateCount.get(), state.getItemCount()); 1495 } catch (Throwable t) { 1496 postExceptionToInstrumentation(t); 1497 } 1498 super.onMeasure(recycler, state, widthSpec, heightSpec); 1499 } 1500 1501 @Override 1502 public boolean supportsPredictiveItemAnimations() { 1503 return enablePredictiveAnimations; 1504 } 1505 }; 1506 RecyclerView recyclerView = new RecyclerView(getActivity()); 1507 recyclerView.setLayoutManager(lm); 1508 recyclerView.setAdapter(testAdapter); 1509 recyclerView.setLayoutManager(lm); 1510 lm.expectLayouts(1); 1511 setRecyclerView(recyclerView); 1512 lm.waitForLayout(2); 1513 checkForMainThreadException(); 1514 lm.expectLayouts(1); 1515 if (!enablePredictiveAnimations) { 1516 expectedOnMeasureStateCount.set(15); 1517 } 1518 testAdapter.addAndNotify(4, 5); 1519 lm.waitForLayout(2); 1520 checkForMainThreadException(); 1521 } 1522 1523 @Test testSetCompatibleAdapter()1524 public void testSetCompatibleAdapter() throws Throwable { 1525 compatibleAdapterTest(true, true); 1526 removeRecyclerView(); 1527 compatibleAdapterTest(false, true); 1528 removeRecyclerView(); 1529 compatibleAdapterTest(true, false); 1530 removeRecyclerView(); 1531 compatibleAdapterTest(false, false); 1532 removeRecyclerView(); 1533 } 1534 compatibleAdapterTest(boolean useCustomPool, boolean removeAndRecycleExistingViews)1535 private void compatibleAdapterTest(boolean useCustomPool, boolean removeAndRecycleExistingViews) 1536 throws Throwable { 1537 TestAdapter testAdapter = new TestAdapter(10); 1538 final AtomicInteger recycledViewCount = new AtomicInteger(); 1539 TestLayoutManager lm = new TestLayoutManager() { 1540 @Override 1541 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1542 try { 1543 layoutRange(recycler, 0, state.getItemCount()); 1544 layoutLatch.countDown(); 1545 } catch (Throwable t) { 1546 postExceptionToInstrumentation(t); 1547 } finally { 1548 layoutLatch.countDown(); 1549 } 1550 } 1551 }; 1552 RecyclerView recyclerView = new RecyclerView(getActivity()); 1553 recyclerView.setLayoutManager(lm); 1554 recyclerView.setAdapter(testAdapter); 1555 recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() { 1556 @Override 1557 public void onViewRecycled(RecyclerView.ViewHolder holder) { 1558 recycledViewCount.incrementAndGet(); 1559 } 1560 }); 1561 lm.expectLayouts(1); 1562 setRecyclerView(recyclerView, !useCustomPool); 1563 lm.waitForLayout(2); 1564 checkForMainThreadException(); 1565 lm.expectLayouts(1); 1566 swapAdapter(new TestAdapter(10), removeAndRecycleExistingViews); 1567 lm.waitForLayout(2); 1568 checkForMainThreadException(); 1569 if (removeAndRecycleExistingViews) { 1570 assertTrue("Previous views should be recycled", recycledViewCount.get() > 0); 1571 } else { 1572 assertEquals("No views should be recycled if adapters are compatible and developer " 1573 + "did not request a recycle", 0, recycledViewCount.get()); 1574 } 1575 } 1576 1577 @Test testSetIncompatibleAdapter()1578 public void testSetIncompatibleAdapter() throws Throwable { 1579 incompatibleAdapterTest(true); 1580 incompatibleAdapterTest(false); 1581 } 1582 incompatibleAdapterTest(boolean useCustomPool)1583 public void incompatibleAdapterTest(boolean useCustomPool) throws Throwable { 1584 TestAdapter testAdapter = new TestAdapter(10); 1585 TestLayoutManager lm = new TestLayoutManager() { 1586 @Override 1587 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1588 super.onLayoutChildren(recycler, state); 1589 try { 1590 layoutRange(recycler, 0, state.getItemCount()); 1591 layoutLatch.countDown(); 1592 } catch (Throwable t) { 1593 postExceptionToInstrumentation(t); 1594 } finally { 1595 layoutLatch.countDown(); 1596 } 1597 } 1598 }; 1599 RecyclerView recyclerView = new RecyclerView(getActivity()); 1600 recyclerView.setLayoutManager(lm); 1601 recyclerView.setAdapter(testAdapter); 1602 recyclerView.setLayoutManager(lm); 1603 lm.expectLayouts(1); 1604 setRecyclerView(recyclerView, !useCustomPool); 1605 lm.waitForLayout(2); 1606 checkForMainThreadException(); 1607 lm.expectLayouts(1); 1608 setAdapter(new TestAdapter2(10)); 1609 lm.waitForLayout(2); 1610 checkForMainThreadException(); 1611 } 1612 1613 @Test testRecycleIgnored()1614 public void testRecycleIgnored() throws Throwable { 1615 final TestAdapter adapter = new TestAdapter(10); 1616 final TestLayoutManager lm = new TestLayoutManager() { 1617 @Override 1618 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1619 layoutRange(recycler, 0, 5); 1620 layoutLatch.countDown(); 1621 } 1622 }; 1623 final RecyclerView recyclerView = new RecyclerView(getActivity()); 1624 recyclerView.setAdapter(adapter); 1625 recyclerView.setLayoutManager(lm); 1626 lm.expectLayouts(1); 1627 setRecyclerView(recyclerView); 1628 lm.waitForLayout(2); 1629 runTestOnUiThread(new Runnable() { 1630 @Override 1631 public void run() { 1632 View child1 = lm.findViewByPosition(0); 1633 View child2 = lm.findViewByPosition(1); 1634 lm.ignoreView(child1); 1635 lm.ignoreView(child2); 1636 1637 lm.removeAndRecycleAllViews(recyclerView.mRecycler); 1638 assertEquals("ignored child should not be recycled or removed", 2, 1639 lm.getChildCount()); 1640 1641 Throwable[] throwables = new Throwable[1]; 1642 try { 1643 lm.removeAndRecycleView(child1, mRecyclerView.mRecycler); 1644 } catch (Throwable t) { 1645 throwables[0] = t; 1646 } 1647 assertTrue("Trying to recycle an ignored view should throw IllegalArgException " 1648 , throwables[0] instanceof IllegalArgumentException); 1649 lm.removeAllViews(); 1650 assertEquals("ignored child should be removed as well ", 0, lm.getChildCount()); 1651 } 1652 }); 1653 } 1654 1655 @Test testFindIgnoredByPosition()1656 public void testFindIgnoredByPosition() throws Throwable { 1657 final TestAdapter adapter = new TestAdapter(10); 1658 final TestLayoutManager lm = new TestLayoutManager() { 1659 @Override 1660 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1661 detachAndScrapAttachedViews(recycler); 1662 layoutRange(recycler, 0, 5); 1663 layoutLatch.countDown(); 1664 } 1665 }; 1666 final RecyclerView recyclerView = new RecyclerView(getActivity()); 1667 recyclerView.setAdapter(adapter); 1668 recyclerView.setLayoutManager(lm); 1669 lm.expectLayouts(1); 1670 setRecyclerView(recyclerView); 1671 lm.waitForLayout(2); 1672 Thread.sleep(5000); 1673 final int pos = 1; 1674 final View[] ignored = new View[1]; 1675 runTestOnUiThread(new Runnable() { 1676 @Override 1677 public void run() { 1678 View child = lm.findViewByPosition(pos); 1679 lm.ignoreView(child); 1680 ignored[0] = child; 1681 } 1682 }); 1683 assertNotNull("ignored child should not be null", ignored[0]); 1684 assertNull("find view by position should not return ignored child", 1685 lm.findViewByPosition(pos)); 1686 lm.expectLayouts(1); 1687 requestLayoutOnUIThread(mRecyclerView); 1688 lm.waitForLayout(1); 1689 assertEquals("child count should be ", 6, lm.getChildCount()); 1690 View replacement = lm.findViewByPosition(pos); 1691 assertNotNull("re-layout should replace ignored child w/ another one", replacement); 1692 assertNotSame("replacement should be a different view", replacement, ignored[0]); 1693 } 1694 1695 @Test testInvalidateAllDecorOffsets()1696 public void testInvalidateAllDecorOffsets() throws Throwable { 1697 final TestAdapter adapter = new TestAdapter(10); 1698 final RecyclerView recyclerView = new RecyclerView(getActivity()); 1699 final AtomicBoolean invalidatedOffsets = new AtomicBoolean(true); 1700 recyclerView.setAdapter(adapter); 1701 final AtomicInteger layoutCount = new AtomicInteger(4); 1702 final RecyclerView.ItemDecoration dummyItemDecoration = new RecyclerView.ItemDecoration() { 1703 }; 1704 TestLayoutManager testLayoutManager = new TestLayoutManager() { 1705 @Override 1706 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1707 try { 1708 // test 1709 for (int i = 0; i < getChildCount(); i++) { 1710 View child = getChildAt(i); 1711 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) 1712 child.getLayoutParams(); 1713 assertEquals( 1714 "Decor insets validation for VH should have expected value.", 1715 invalidatedOffsets.get(), lp.mInsetsDirty); 1716 } 1717 for (RecyclerView.ViewHolder vh : mRecyclerView.mRecycler.mCachedViews) { 1718 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) 1719 vh.itemView.getLayoutParams(); 1720 assertEquals( 1721 "Decor insets invalidation in cache for VH should have expected " 1722 + "value.", 1723 invalidatedOffsets.get(), lp.mInsetsDirty); 1724 } 1725 detachAndScrapAttachedViews(recycler); 1726 layoutRange(recycler, 0, layoutCount.get()); 1727 } catch (Throwable t) { 1728 postExceptionToInstrumentation(t); 1729 } finally { 1730 layoutLatch.countDown(); 1731 } 1732 } 1733 1734 @Override 1735 public boolean supportsPredictiveItemAnimations() { 1736 return false; 1737 } 1738 }; 1739 // first layout 1740 recyclerView.setItemViewCacheSize(5); 1741 recyclerView.setLayoutManager(testLayoutManager); 1742 testLayoutManager.expectLayouts(1); 1743 setRecyclerView(recyclerView, true, false); 1744 testLayoutManager.waitForLayout(2); 1745 checkForMainThreadException(); 1746 1747 // re-layout w/o any change 1748 invalidatedOffsets.set(false); 1749 testLayoutManager.expectLayouts(1); 1750 requestLayoutOnUIThread(recyclerView); 1751 testLayoutManager.waitForLayout(1); 1752 checkForMainThreadException(); 1753 1754 // invalidate w/o an item decorator 1755 1756 invalidateDecorOffsets(recyclerView); 1757 testLayoutManager.expectLayouts(1); 1758 invalidateDecorOffsets(recyclerView); 1759 testLayoutManager.assertNoLayout("layout should not happen", 2); 1760 checkForMainThreadException(); 1761 1762 // set item decorator, should invalidate 1763 invalidatedOffsets.set(true); 1764 testLayoutManager.expectLayouts(1); 1765 addItemDecoration(mRecyclerView, dummyItemDecoration); 1766 testLayoutManager.waitForLayout(1); 1767 checkForMainThreadException(); 1768 1769 // re-layout w/o any change 1770 invalidatedOffsets.set(false); 1771 testLayoutManager.expectLayouts(1); 1772 requestLayoutOnUIThread(recyclerView); 1773 testLayoutManager.waitForLayout(1); 1774 checkForMainThreadException(); 1775 1776 // invalidate w/ item decorator 1777 invalidatedOffsets.set(true); 1778 invalidateDecorOffsets(recyclerView); 1779 testLayoutManager.expectLayouts(1); 1780 invalidateDecorOffsets(recyclerView); 1781 testLayoutManager.waitForLayout(2); 1782 checkForMainThreadException(); 1783 1784 // trigger cache. 1785 layoutCount.set(3); 1786 invalidatedOffsets.set(false); 1787 testLayoutManager.expectLayouts(1); 1788 requestLayoutOnUIThread(mRecyclerView); 1789 testLayoutManager.waitForLayout(1); 1790 checkForMainThreadException(); 1791 assertEquals("a view should be cached", 1, mRecyclerView.mRecycler.mCachedViews.size()); 1792 1793 layoutCount.set(5); 1794 invalidatedOffsets.set(true); 1795 testLayoutManager.expectLayouts(1); 1796 invalidateDecorOffsets(recyclerView); 1797 testLayoutManager.waitForLayout(1); 1798 checkForMainThreadException(); 1799 1800 // remove item decorator 1801 invalidatedOffsets.set(true); 1802 testLayoutManager.expectLayouts(1); 1803 removeItemDecoration(mRecyclerView, dummyItemDecoration); 1804 testLayoutManager.waitForLayout(1); 1805 checkForMainThreadException(); 1806 } 1807 addItemDecoration(final RecyclerView recyclerView, final RecyclerView.ItemDecoration itemDecoration)1808 public void addItemDecoration(final RecyclerView recyclerView, final 1809 RecyclerView.ItemDecoration itemDecoration) throws Throwable { 1810 runTestOnUiThread(new Runnable() { 1811 @Override 1812 public void run() { 1813 recyclerView.addItemDecoration(itemDecoration); 1814 } 1815 }); 1816 } 1817 removeItemDecoration(final RecyclerView recyclerView, final RecyclerView.ItemDecoration itemDecoration)1818 public void removeItemDecoration(final RecyclerView recyclerView, final 1819 RecyclerView.ItemDecoration itemDecoration) throws Throwable { 1820 runTestOnUiThread(new Runnable() { 1821 @Override 1822 public void run() { 1823 recyclerView.removeItemDecoration(itemDecoration); 1824 } 1825 }); 1826 } 1827 invalidateDecorOffsets(final RecyclerView recyclerView)1828 public void invalidateDecorOffsets(final RecyclerView recyclerView) throws Throwable { 1829 runTestOnUiThread(new Runnable() { 1830 @Override 1831 public void run() { 1832 recyclerView.invalidateItemDecorations(); 1833 } 1834 }); 1835 } 1836 1837 @Test testInvalidateDecorOffsets()1838 public void testInvalidateDecorOffsets() throws Throwable { 1839 final TestAdapter adapter = new TestAdapter(10); 1840 adapter.setHasStableIds(true); 1841 final RecyclerView recyclerView = new RecyclerView(getActivity()); 1842 recyclerView.setAdapter(adapter); 1843 1844 final Map<Long, Boolean> changes = new HashMap<Long, Boolean>(); 1845 1846 TestLayoutManager testLayoutManager = new TestLayoutManager() { 1847 @Override 1848 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1849 try { 1850 if (changes.size() > 0) { 1851 // test 1852 for (int i = 0; i < getChildCount(); i++) { 1853 View child = getChildAt(i); 1854 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) 1855 child.getLayoutParams(); 1856 RecyclerView.ViewHolder vh = lp.mViewHolder; 1857 if (!changes.containsKey(vh.getItemId())) { 1858 continue; //nothing to test 1859 } 1860 assertEquals( 1861 "Decord insets validation for VH should have expected value.", 1862 changes.get(vh.getItemId()).booleanValue(), 1863 lp.mInsetsDirty); 1864 } 1865 } 1866 detachAndScrapAttachedViews(recycler); 1867 layoutRange(recycler, 0, state.getItemCount()); 1868 } catch (Throwable t) { 1869 postExceptionToInstrumentation(t); 1870 } finally { 1871 layoutLatch.countDown(); 1872 } 1873 } 1874 1875 @Override 1876 public boolean supportsPredictiveItemAnimations() { 1877 return false; 1878 } 1879 }; 1880 recyclerView.setLayoutManager(testLayoutManager); 1881 testLayoutManager.expectLayouts(1); 1882 setRecyclerView(recyclerView); 1883 testLayoutManager.waitForLayout(2); 1884 int itemAddedTo = 5; 1885 for (int i = 0; i < itemAddedTo; i++) { 1886 changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false); 1887 } 1888 for (int i = itemAddedTo; i < mRecyclerView.getChildCount(); i++) { 1889 changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true); 1890 } 1891 testLayoutManager.expectLayouts(1); 1892 adapter.addAndNotify(5, 1); 1893 testLayoutManager.waitForLayout(2); 1894 checkForMainThreadException(); 1895 1896 changes.clear(); 1897 int[] changedItems = new int[]{3, 5, 6}; 1898 for (int i = 0; i < adapter.getItemCount(); i++) { 1899 changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), false); 1900 } 1901 for (int i = 0; i < changedItems.length; i++) { 1902 changes.put(mRecyclerView.findViewHolderForLayoutPosition(changedItems[i]).getItemId(), 1903 true); 1904 } 1905 testLayoutManager.expectLayouts(1); 1906 adapter.changePositionsAndNotify(changedItems); 1907 testLayoutManager.waitForLayout(2); 1908 checkForMainThreadException(); 1909 1910 for (int i = 0; i < adapter.getItemCount(); i++) { 1911 changes.put(mRecyclerView.findViewHolderForLayoutPosition(i).getItemId(), true); 1912 } 1913 testLayoutManager.expectLayouts(1); 1914 adapter.dispatchDataSetChanged(); 1915 testLayoutManager.waitForLayout(2); 1916 checkForMainThreadException(); 1917 } 1918 1919 @Test testMovingViaStableIds()1920 public void testMovingViaStableIds() throws Throwable { 1921 stableIdsMoveTest(true); 1922 removeRecyclerView(); 1923 stableIdsMoveTest(false); 1924 removeRecyclerView(); 1925 } 1926 stableIdsMoveTest(final boolean supportsPredictive)1927 public void stableIdsMoveTest(final boolean supportsPredictive) throws Throwable { 1928 final TestAdapter testAdapter = new TestAdapter(10); 1929 testAdapter.setHasStableIds(true); 1930 final AtomicBoolean test = new AtomicBoolean(false); 1931 final int movedViewFromIndex = 3; 1932 final int movedViewToIndex = 6; 1933 final View[] movedView = new View[1]; 1934 TestLayoutManager lm = new TestLayoutManager() { 1935 @Override 1936 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1937 detachAndScrapAttachedViews(recycler); 1938 try { 1939 if (test.get()) { 1940 if (state.isPreLayout()) { 1941 View view = recycler.getViewForPosition(movedViewFromIndex, true); 1942 assertSame("In pre layout, should be able to get moved view w/ old " 1943 + "position", movedView[0], view); 1944 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); 1945 assertTrue("it should come from scrap", holder.wasReturnedFromScrap()); 1946 // clear scrap flag 1947 holder.clearReturnedFromScrapFlag(); 1948 } else { 1949 View view = recycler.getViewForPosition(movedViewToIndex, true); 1950 assertSame("In post layout, should be able to get moved view w/ new " 1951 + "position", movedView[0], view); 1952 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); 1953 assertTrue("it should come from scrap", holder.wasReturnedFromScrap()); 1954 // clear scrap flag 1955 holder.clearReturnedFromScrapFlag(); 1956 } 1957 } 1958 layoutRange(recycler, 0, state.getItemCount()); 1959 } catch (Throwable t) { 1960 postExceptionToInstrumentation(t); 1961 } finally { 1962 layoutLatch.countDown(); 1963 } 1964 1965 1966 } 1967 1968 @Override 1969 public boolean supportsPredictiveItemAnimations() { 1970 return supportsPredictive; 1971 } 1972 }; 1973 RecyclerView recyclerView = new RecyclerView(this.getActivity()); 1974 recyclerView.setAdapter(testAdapter); 1975 recyclerView.setLayoutManager(lm); 1976 lm.expectLayouts(1); 1977 setRecyclerView(recyclerView); 1978 lm.waitForLayout(1); 1979 1980 movedView[0] = recyclerView.getChildAt(movedViewFromIndex); 1981 test.set(true); 1982 lm.expectLayouts(supportsPredictive ? 2 : 1); 1983 runTestOnUiThread(new Runnable() { 1984 @Override 1985 public void run() { 1986 Item item = testAdapter.mItems.remove(movedViewFromIndex); 1987 testAdapter.mItems.add(movedViewToIndex, item); 1988 testAdapter.notifyItemRemoved(movedViewFromIndex); 1989 testAdapter.notifyItemInserted(movedViewToIndex); 1990 } 1991 }); 1992 lm.waitForLayout(2); 1993 checkForMainThreadException(); 1994 } 1995 1996 @Test testAdapterChangeDuringLayout()1997 public void testAdapterChangeDuringLayout() throws Throwable { 1998 adapterChangeInMainThreadTest("notifyDataSetChanged", new Runnable() { 1999 @Override 2000 public void run() { 2001 mRecyclerView.getAdapter().notifyDataSetChanged(); 2002 } 2003 }); 2004 2005 adapterChangeInMainThreadTest("notifyItemChanged", new Runnable() { 2006 @Override 2007 public void run() { 2008 mRecyclerView.getAdapter().notifyItemChanged(2); 2009 } 2010 }); 2011 2012 adapterChangeInMainThreadTest("notifyItemInserted", new Runnable() { 2013 @Override 2014 public void run() { 2015 mRecyclerView.getAdapter().notifyItemInserted(2); 2016 } 2017 }); 2018 adapterChangeInMainThreadTest("notifyItemRemoved", new Runnable() { 2019 @Override 2020 public void run() { 2021 mRecyclerView.getAdapter().notifyItemRemoved(2); 2022 } 2023 }); 2024 } 2025 adapterChangeInMainThreadTest(String msg, final Runnable onLayoutRunnable)2026 public void adapterChangeInMainThreadTest(String msg, 2027 final Runnable onLayoutRunnable) throws Throwable { 2028 final AtomicBoolean doneFirstLayout = new AtomicBoolean(false); 2029 TestAdapter testAdapter = new TestAdapter(10); 2030 TestLayoutManager lm = new TestLayoutManager() { 2031 @Override 2032 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2033 super.onLayoutChildren(recycler, state); 2034 try { 2035 layoutRange(recycler, 0, state.getItemCount()); 2036 if (doneFirstLayout.get()) { 2037 onLayoutRunnable.run(); 2038 } 2039 } catch (Throwable t) { 2040 postExceptionToInstrumentation(t); 2041 } finally { 2042 layoutLatch.countDown(); 2043 } 2044 2045 } 2046 }; 2047 RecyclerView recyclerView = new RecyclerView(getActivity()); 2048 recyclerView.setLayoutManager(lm); 2049 recyclerView.setAdapter(testAdapter); 2050 lm.expectLayouts(1); 2051 setRecyclerView(recyclerView); 2052 lm.waitForLayout(2); 2053 doneFirstLayout.set(true); 2054 lm.expectLayouts(1); 2055 requestLayoutOnUIThread(recyclerView); 2056 lm.waitForLayout(2); 2057 removeRecyclerView(); 2058 assertTrue("Invalid data updates should be caught:" + msg, 2059 mainThreadException instanceof IllegalStateException); 2060 mainThreadException = null; 2061 } 2062 2063 @Test testAdapterChangeDuringScroll()2064 public void testAdapterChangeDuringScroll() throws Throwable { 2065 for (int orientation : new int[]{OrientationHelper.HORIZONTAL, 2066 OrientationHelper.VERTICAL}) { 2067 adapterChangeDuringScrollTest("notifyDataSetChanged", orientation, 2068 new Runnable() { 2069 @Override 2070 public void run() { 2071 mRecyclerView.getAdapter().notifyDataSetChanged(); 2072 } 2073 }); 2074 adapterChangeDuringScrollTest("notifyItemChanged", orientation, new Runnable() { 2075 @Override 2076 public void run() { 2077 mRecyclerView.getAdapter().notifyItemChanged(2); 2078 } 2079 }); 2080 2081 adapterChangeDuringScrollTest("notifyItemInserted", orientation, new Runnable() { 2082 @Override 2083 public void run() { 2084 mRecyclerView.getAdapter().notifyItemInserted(2); 2085 } 2086 }); 2087 adapterChangeDuringScrollTest("notifyItemRemoved", orientation, new Runnable() { 2088 @Override 2089 public void run() { 2090 mRecyclerView.getAdapter().notifyItemRemoved(2); 2091 } 2092 }); 2093 } 2094 } 2095 adapterChangeDuringScrollTest(String msg, final int orientation, final Runnable onScrollRunnable)2096 public void adapterChangeDuringScrollTest(String msg, final int orientation, 2097 final Runnable onScrollRunnable) throws Throwable { 2098 TestAdapter testAdapter = new TestAdapter(100); 2099 TestLayoutManager lm = new TestLayoutManager() { 2100 @Override 2101 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2102 super.onLayoutChildren(recycler, state); 2103 try { 2104 layoutRange(recycler, 0, 10); 2105 } catch (Throwable t) { 2106 postExceptionToInstrumentation(t); 2107 } finally { 2108 layoutLatch.countDown(); 2109 } 2110 } 2111 2112 @Override 2113 public boolean canScrollVertically() { 2114 return orientation == OrientationHelper.VERTICAL; 2115 } 2116 2117 @Override 2118 public boolean canScrollHorizontally() { 2119 return orientation == OrientationHelper.HORIZONTAL; 2120 } 2121 2122 public int mockScroll() { 2123 try { 2124 onScrollRunnable.run(); 2125 } catch (Throwable t) { 2126 postExceptionToInstrumentation(t); 2127 } finally { 2128 layoutLatch.countDown(); 2129 } 2130 return 0; 2131 } 2132 2133 @Override 2134 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 2135 RecyclerView.State state) { 2136 return mockScroll(); 2137 } 2138 2139 @Override 2140 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 2141 RecyclerView.State state) { 2142 return mockScroll(); 2143 } 2144 }; 2145 RecyclerView recyclerView = new RecyclerView(getActivity()); 2146 recyclerView.setLayoutManager(lm); 2147 recyclerView.setAdapter(testAdapter); 2148 lm.expectLayouts(1); 2149 setRecyclerView(recyclerView); 2150 lm.waitForLayout(2); 2151 lm.expectLayouts(1); 2152 scrollBy(200); 2153 lm.waitForLayout(2); 2154 removeRecyclerView(); 2155 assertTrue("Invalid data updates should be caught:" + msg, 2156 mainThreadException instanceof IllegalStateException); 2157 mainThreadException = null; 2158 } 2159 2160 @Test testRecycleOnDetach()2161 public void testRecycleOnDetach() throws Throwable { 2162 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2163 final TestAdapter testAdapter = new TestAdapter(10); 2164 final AtomicBoolean didRunOnDetach = new AtomicBoolean(false); 2165 final TestLayoutManager lm = new TestLayoutManager() { 2166 @Override 2167 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2168 super.onLayoutChildren(recycler, state); 2169 layoutRange(recycler, 0, state.getItemCount() - 1); 2170 layoutLatch.countDown(); 2171 } 2172 2173 @Override 2174 public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 2175 super.onDetachedFromWindow(view, recycler); 2176 didRunOnDetach.set(true); 2177 removeAndRecycleAllViews(recycler); 2178 } 2179 }; 2180 recyclerView.setAdapter(testAdapter); 2181 recyclerView.setLayoutManager(lm); 2182 lm.expectLayouts(1); 2183 setRecyclerView(recyclerView); 2184 lm.waitForLayout(2); 2185 removeRecyclerView(); 2186 assertTrue("When recycler view is removed, detach should run", didRunOnDetach.get()); 2187 assertEquals("All children should be recycled", recyclerView.getChildCount(), 0); 2188 } 2189 2190 @Test testUpdatesWhileDetached()2191 public void testUpdatesWhileDetached() throws Throwable { 2192 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2193 final int initialAdapterSize = 20; 2194 final TestAdapter adapter = new TestAdapter(initialAdapterSize); 2195 final AtomicInteger layoutCount = new AtomicInteger(0); 2196 TestLayoutManager lm = new TestLayoutManager() { 2197 @Override 2198 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2199 super.onLayoutChildren(recycler, state); 2200 layoutRange(recycler, 0, 5); 2201 layoutCount.incrementAndGet(); 2202 layoutLatch.countDown(); 2203 } 2204 }; 2205 recyclerView.setAdapter(adapter); 2206 recyclerView.setLayoutManager(lm); 2207 recyclerView.setHasFixedSize(true); 2208 lm.expectLayouts(1); 2209 adapter.addAndNotify(4, 5); 2210 lm.assertNoLayout("When RV is not attached, layout should not happen", 1); 2211 } 2212 2213 @Test testUpdatesAfterDetach()2214 public void testUpdatesAfterDetach() throws Throwable { 2215 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2216 final int initialAdapterSize = 20; 2217 final TestAdapter adapter = new TestAdapter(initialAdapterSize); 2218 final AtomicInteger layoutCount = new AtomicInteger(0); 2219 TestLayoutManager lm = new TestLayoutManager() { 2220 @Override 2221 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2222 super.onLayoutChildren(recycler, state); 2223 layoutRange(recycler, 0, 5); 2224 layoutCount.incrementAndGet(); 2225 layoutLatch.countDown(); 2226 } 2227 }; 2228 recyclerView.setAdapter(adapter); 2229 recyclerView.setLayoutManager(lm); 2230 lm.expectLayouts(1); 2231 recyclerView.setHasFixedSize(true); 2232 setRecyclerView(recyclerView); 2233 lm.waitForLayout(2); 2234 lm.expectLayouts(1); 2235 final int prevLayoutCount = layoutCount.get(); 2236 runTestOnUiThread(new Runnable() { 2237 @Override 2238 public void run() { 2239 try { 2240 adapter.addAndNotify(4, 5); 2241 removeRecyclerView(); 2242 } catch (Throwable throwable) { 2243 postExceptionToInstrumentation(throwable); 2244 } 2245 } 2246 }); 2247 checkForMainThreadException(); 2248 2249 lm.assertNoLayout("When RV is not attached, layout should not happen", 1); 2250 assertEquals("No extra layout should happen when detached", prevLayoutCount, 2251 layoutCount.get()); 2252 } 2253 2254 @Test testNotifyDataSetChangedWithStableIds()2255 public void testNotifyDataSetChangedWithStableIds() throws Throwable { 2256 final int defaultViewType = 1; 2257 final Map<Item, Integer> viewTypeMap = new HashMap<Item, Integer>(); 2258 final Map<Integer, Integer> oldPositionToNewPositionMapping = 2259 new HashMap<Integer, Integer>(); 2260 final TestAdapter adapter = new TestAdapter(100) { 2261 @Override 2262 public int getItemViewType(int position) { 2263 Integer type = viewTypeMap.get(mItems.get(position)); 2264 return type == null ? defaultViewType : type; 2265 } 2266 2267 @Override 2268 public long getItemId(int position) { 2269 return mItems.get(position).mId; 2270 } 2271 }; 2272 adapter.setHasStableIds(true); 2273 final ArrayList<Item> previousItems = new ArrayList<Item>(); 2274 previousItems.addAll(adapter.mItems); 2275 2276 final AtomicInteger layoutStart = new AtomicInteger(50); 2277 final AtomicBoolean validate = new AtomicBoolean(false); 2278 final int childCount = 10; 2279 final TestLayoutManager lm = new TestLayoutManager() { 2280 @Override 2281 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2282 try { 2283 super.onLayoutChildren(recycler, state); 2284 if (validate.get()) { 2285 assertEquals("Cached views should be kept", 5, recycler 2286 .mCachedViews.size()); 2287 for (RecyclerView.ViewHolder vh : recycler.mCachedViews) { 2288 TestViewHolder tvh = (TestViewHolder) vh; 2289 assertTrue("view holder should be marked for update", 2290 tvh.needsUpdate()); 2291 assertTrue("view holder should be marked as invalid", tvh.isInvalid()); 2292 } 2293 } 2294 detachAndScrapAttachedViews(recycler); 2295 if (validate.get()) { 2296 assertEquals("cache size should stay the same", 5, 2297 recycler.mCachedViews.size()); 2298 assertEquals("all views should be scrapped", childCount, 2299 recycler.getScrapList().size()); 2300 for (RecyclerView.ViewHolder vh : recycler.getScrapList()) { 2301 // TODO create test case for type change 2302 TestViewHolder tvh = (TestViewHolder) vh; 2303 assertTrue("view holder should be marked for update", 2304 tvh.needsUpdate()); 2305 assertTrue("view holder should be marked as invalid", tvh.isInvalid()); 2306 } 2307 } 2308 layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount); 2309 if (validate.get()) { 2310 for (int i = 0; i < getChildCount(); i++) { 2311 View view = getChildAt(i); 2312 TestViewHolder tvh = (TestViewHolder) mRecyclerView 2313 .getChildViewHolder(view); 2314 final int oldPos = previousItems.indexOf(tvh.mBoundItem); 2315 assertEquals("view holder's position should be correct", 2316 oldPositionToNewPositionMapping.get(oldPos).intValue(), 2317 tvh.getLayoutPosition()); 2318 ; 2319 } 2320 } 2321 } catch (Throwable t) { 2322 postExceptionToInstrumentation(t); 2323 } finally { 2324 layoutLatch.countDown(); 2325 } 2326 } 2327 }; 2328 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2329 recyclerView.setItemAnimator(null); 2330 recyclerView.setAdapter(adapter); 2331 recyclerView.setLayoutManager(lm); 2332 recyclerView.setItemViewCacheSize(10); 2333 lm.expectLayouts(1); 2334 setRecyclerView(recyclerView); 2335 lm.waitForLayout(2); 2336 checkForMainThreadException(); 2337 getInstrumentation().waitForIdleSync(); 2338 layoutStart.set(layoutStart.get() + 5);//55 2339 lm.expectLayouts(1); 2340 requestLayoutOnUIThread(recyclerView); 2341 lm.waitForLayout(2); 2342 validate.set(true); 2343 lm.expectLayouts(1); 2344 runTestOnUiThread(new Runnable() { 2345 @Override 2346 public void run() { 2347 try { 2348 adapter.moveItems(false, 2349 new int[]{50, 56}, new int[]{51, 1}, new int[]{52, 2}, 2350 new int[]{53, 54}, new int[]{60, 61}, new int[]{62, 64}, 2351 new int[]{75, 58}); 2352 for (int i = 0; i < previousItems.size(); i++) { 2353 Item item = previousItems.get(i); 2354 oldPositionToNewPositionMapping.put(i, adapter.mItems.indexOf(item)); 2355 } 2356 adapter.dispatchDataSetChanged(); 2357 } catch (Throwable throwable) { 2358 postExceptionToInstrumentation(throwable); 2359 } 2360 } 2361 }); 2362 lm.waitForLayout(2); 2363 checkForMainThreadException(); 2364 } 2365 2366 @Test testCallbacksDuringAdapterSwap()2367 public void testCallbacksDuringAdapterSwap() throws Throwable { 2368 callbacksDuringAdapterChange(true); 2369 } 2370 2371 @Test testCallbacksDuringAdapterSet()2372 public void testCallbacksDuringAdapterSet() throws Throwable { 2373 callbacksDuringAdapterChange(false); 2374 } 2375 callbacksDuringAdapterChange(boolean swap)2376 public void callbacksDuringAdapterChange(boolean swap) throws Throwable { 2377 final TestAdapter2 adapter1 = swap ? createBinderCheckingAdapter() 2378 : createOwnerCheckingAdapter(); 2379 final TestAdapter2 adapter2 = swap ? createBinderCheckingAdapter() 2380 : createOwnerCheckingAdapter(); 2381 2382 TestLayoutManager tlm = new TestLayoutManager() { 2383 @Override 2384 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2385 try { 2386 layoutRange(recycler, 0, state.getItemCount()); 2387 } catch (Throwable t) { 2388 postExceptionToInstrumentation(t); 2389 } 2390 layoutLatch.countDown(); 2391 } 2392 }; 2393 RecyclerView rv = new RecyclerView(getActivity()); 2394 rv.setAdapter(adapter1); 2395 rv.setLayoutManager(tlm); 2396 tlm.expectLayouts(1); 2397 setRecyclerView(rv); 2398 tlm.waitForLayout(1); 2399 checkForMainThreadException(); 2400 tlm.expectLayouts(1); 2401 if (swap) { 2402 swapAdapter(adapter2, true); 2403 } else { 2404 setAdapter(adapter2); 2405 } 2406 checkForMainThreadException(); 2407 tlm.waitForLayout(1); 2408 checkForMainThreadException(); 2409 } 2410 createOwnerCheckingAdapter()2411 private TestAdapter2 createOwnerCheckingAdapter() { 2412 return new TestAdapter2(10) { 2413 @Override 2414 public void onViewRecycled(TestViewHolder2 holder) { 2415 assertSame("on recycled should be called w/ the creator adapter", this, 2416 holder.mData); 2417 super.onViewRecycled(holder); 2418 } 2419 2420 @Override 2421 public void onBindViewHolder(TestViewHolder2 holder, int position) { 2422 super.onBindViewHolder(holder, position); 2423 assertSame("on bind should be called w/ the creator adapter", this, holder.mData); 2424 } 2425 2426 @Override 2427 public TestViewHolder2 onCreateViewHolder(ViewGroup parent, 2428 int viewType) { 2429 final TestViewHolder2 vh = super.onCreateViewHolder(parent, viewType); 2430 vh.mData = this; 2431 return vh; 2432 } 2433 }; 2434 } 2435 2436 private TestAdapter2 createBinderCheckingAdapter() { 2437 return new TestAdapter2(10) { 2438 @Override 2439 public void onViewRecycled(TestViewHolder2 holder) { 2440 assertSame("on recycled should be called w/ the creator adapter", this, 2441 holder.mData); 2442 holder.mData = null; 2443 super.onViewRecycled(holder); 2444 } 2445 2446 @Override 2447 public void onBindViewHolder(TestViewHolder2 holder, int position) { 2448 super.onBindViewHolder(holder, position); 2449 holder.mData = this; 2450 } 2451 }; 2452 } 2453 2454 @Test 2455 public void testFindViewById() throws Throwable { 2456 findViewByIdTest(false); 2457 removeRecyclerView(); 2458 findViewByIdTest(true); 2459 } 2460 2461 public void findViewByIdTest(final boolean supportPredictive) throws Throwable { 2462 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2463 final int initialAdapterSize = 20; 2464 final TestAdapter adapter = new TestAdapter(initialAdapterSize); 2465 final int deleteStart = 6; 2466 final int deleteCount = 5; 2467 recyclerView.setAdapter(adapter); 2468 final AtomicBoolean assertPositions = new AtomicBoolean(false); 2469 TestLayoutManager lm = new TestLayoutManager() { 2470 @Override 2471 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2472 super.onLayoutChildren(recycler, state); 2473 if (assertPositions.get()) { 2474 if (state.isPreLayout()) { 2475 for (int i = 0; i < deleteStart; i++) { 2476 View view = findViewByPosition(i); 2477 assertNotNull("find view by position for existing items should work " 2478 + "fine", view); 2479 assertFalse("view should not be marked as removed", 2480 ((RecyclerView.LayoutParams) view.getLayoutParams()) 2481 .isItemRemoved()); 2482 } 2483 for (int i = 0; i < deleteCount; i++) { 2484 View view = findViewByPosition(i + deleteStart); 2485 assertNotNull("find view by position should work fine for removed " 2486 + "views in pre-layout", view); 2487 assertTrue("view should be marked as removed", 2488 ((RecyclerView.LayoutParams) view.getLayoutParams()) 2489 .isItemRemoved()); 2490 } 2491 for (int i = deleteStart + deleteCount; i < 20; i++) { 2492 View view = findViewByPosition(i); 2493 assertNotNull(view); 2494 assertFalse("view should not be marked as removed", 2495 ((RecyclerView.LayoutParams) view.getLayoutParams()) 2496 .isItemRemoved()); 2497 } 2498 } else { 2499 for (int i = 0; i < initialAdapterSize - deleteCount; i++) { 2500 View view = findViewByPosition(i); 2501 assertNotNull("find view by position for existing item " + i + 2502 " should work fine. child count:" + getChildCount(), view); 2503 TestViewHolder viewHolder = 2504 (TestViewHolder) mRecyclerView.getChildViewHolder(view); 2505 assertSame("should be the correct item " + viewHolder 2506 , viewHolder.mBoundItem, 2507 adapter.mItems.get(viewHolder.mPosition)); 2508 assertFalse("view should not be marked as removed", 2509 ((RecyclerView.LayoutParams) view.getLayoutParams()) 2510 .isItemRemoved()); 2511 } 2512 } 2513 } 2514 detachAndScrapAttachedViews(recycler); 2515 layoutRange(recycler, state.getItemCount() - 1, -1); 2516 layoutLatch.countDown(); 2517 } 2518 2519 @Override 2520 public boolean supportsPredictiveItemAnimations() { 2521 return supportPredictive; 2522 } 2523 }; 2524 recyclerView.setLayoutManager(lm); 2525 lm.expectLayouts(1); 2526 setRecyclerView(recyclerView); 2527 lm.waitForLayout(2); 2528 getInstrumentation().waitForIdleSync(); 2529 2530 assertPositions.set(true); 2531 lm.expectLayouts(supportPredictive ? 2 : 1); 2532 adapter.deleteAndNotify(new int[]{deleteStart, deleteCount - 1}, new int[]{deleteStart, 1}); 2533 lm.waitForLayout(2); 2534 } 2535 2536 @Test 2537 public void testTypeForCache() throws Throwable { 2538 final AtomicInteger viewType = new AtomicInteger(1); 2539 final TestAdapter adapter = new TestAdapter(100) { 2540 @Override 2541 public int getItemViewType(int position) { 2542 return viewType.get(); 2543 } 2544 2545 @Override 2546 public long getItemId(int position) { 2547 return mItems.get(position).mId; 2548 } 2549 }; 2550 adapter.setHasStableIds(true); 2551 final AtomicInteger layoutStart = new AtomicInteger(2); 2552 final int childCount = 10; 2553 final TestLayoutManager lm = new TestLayoutManager() { 2554 @Override 2555 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2556 super.onLayoutChildren(recycler, state); 2557 detachAndScrapAttachedViews(recycler); 2558 layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount); 2559 layoutLatch.countDown(); 2560 } 2561 }; 2562 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2563 recyclerView.setItemAnimator(null); 2564 recyclerView.setAdapter(adapter); 2565 recyclerView.setLayoutManager(lm); 2566 recyclerView.setItemViewCacheSize(10); 2567 lm.expectLayouts(1); 2568 setRecyclerView(recyclerView); 2569 lm.waitForLayout(2); 2570 getInstrumentation().waitForIdleSync(); 2571 layoutStart.set(4); // trigger a cache for 3,4 2572 lm.expectLayouts(1); 2573 requestLayoutOnUIThread(recyclerView); 2574 lm.waitForLayout(2); 2575 // 2576 viewType.incrementAndGet(); 2577 layoutStart.set(2); // go back to bring views from cache 2578 lm.expectLayouts(1); 2579 adapter.mItems.remove(1); 2580 adapter.dispatchDataSetChanged(); 2581 lm.waitForLayout(2); 2582 runTestOnUiThread(new Runnable() { 2583 @Override 2584 public void run() { 2585 for (int i = 2; i < 4; i++) { 2586 RecyclerView.ViewHolder vh = recyclerView.findViewHolderForLayoutPosition(i); 2587 assertEquals("View holder's type should match latest type", viewType.get(), 2588 vh.getItemViewType()); 2589 } 2590 } 2591 }); 2592 } 2593 2594 @Test 2595 public void testTypeForExistingViews() throws Throwable { 2596 final AtomicInteger viewType = new AtomicInteger(1); 2597 final int invalidatedCount = 2; 2598 final int layoutStart = 2; 2599 final TestAdapter adapter = new TestAdapter(100) { 2600 @Override 2601 public int getItemViewType(int position) { 2602 return viewType.get(); 2603 } 2604 2605 @Override 2606 public void onBindViewHolder(TestViewHolder holder, 2607 int position) { 2608 super.onBindViewHolder(holder, position); 2609 if (position >= layoutStart && position < invalidatedCount + layoutStart) { 2610 try { 2611 assertEquals("holder type should match current view type at position " + 2612 position, viewType.get(), holder.getItemViewType()); 2613 } catch (Throwable t) { 2614 postExceptionToInstrumentation(t); 2615 } 2616 } 2617 } 2618 2619 @Override 2620 public long getItemId(int position) { 2621 return mItems.get(position).mId; 2622 } 2623 }; 2624 adapter.setHasStableIds(true); 2625 2626 final int childCount = 10; 2627 final TestLayoutManager lm = new TestLayoutManager() { 2628 @Override 2629 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2630 super.onLayoutChildren(recycler, state); 2631 detachAndScrapAttachedViews(recycler); 2632 layoutRange(recycler, layoutStart, layoutStart + childCount); 2633 layoutLatch.countDown(); 2634 } 2635 }; 2636 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2637 recyclerView.setAdapter(adapter); 2638 recyclerView.setLayoutManager(lm); 2639 lm.expectLayouts(1); 2640 setRecyclerView(recyclerView); 2641 lm.waitForLayout(2); 2642 getInstrumentation().waitForIdleSync(); 2643 viewType.incrementAndGet(); 2644 lm.expectLayouts(1); 2645 adapter.changeAndNotify(layoutStart, invalidatedCount); 2646 lm.waitForLayout(2); 2647 checkForMainThreadException(); 2648 } 2649 2650 2651 @Test 2652 public void testState() throws Throwable { 2653 final TestAdapter adapter = new TestAdapter(10); 2654 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2655 recyclerView.setAdapter(adapter); 2656 recyclerView.setItemAnimator(null); 2657 final AtomicInteger itemCount = new AtomicInteger(); 2658 final AtomicBoolean structureChanged = new AtomicBoolean(); 2659 TestLayoutManager testLayoutManager = new TestLayoutManager() { 2660 @Override 2661 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2662 detachAndScrapAttachedViews(recycler); 2663 layoutRange(recycler, 0, state.getItemCount()); 2664 itemCount.set(state.getItemCount()); 2665 structureChanged.set(state.didStructureChange()); 2666 layoutLatch.countDown(); 2667 } 2668 }; 2669 recyclerView.setLayoutManager(testLayoutManager); 2670 testLayoutManager.expectLayouts(1); 2671 runTestOnUiThread(new Runnable() { 2672 @Override 2673 public void run() { 2674 getActivity().mContainer.addView(recyclerView); 2675 } 2676 }); 2677 testLayoutManager.waitForLayout(2, TimeUnit.SECONDS); 2678 2679 assertEquals("item count in state should be correct", adapter.getItemCount() 2680 , itemCount.get()); 2681 assertEquals("structure changed should be true for first layout", true, 2682 structureChanged.get()); 2683 Thread.sleep(1000); //wait for other layouts. 2684 testLayoutManager.expectLayouts(1); 2685 runTestOnUiThread(new Runnable() { 2686 @Override 2687 public void run() { 2688 recyclerView.requestLayout(); 2689 } 2690 }); 2691 testLayoutManager.waitForLayout(2); 2692 assertEquals("in second layout,structure changed should be false", false, 2693 structureChanged.get()); 2694 testLayoutManager.expectLayouts(1); // 2695 adapter.deleteAndNotify(3, 2); 2696 testLayoutManager.waitForLayout(2); 2697 assertEquals("when items are removed, item count in state should be updated", 2698 adapter.getItemCount(), 2699 itemCount.get()); 2700 assertEquals("structure changed should be true when items are removed", true, 2701 structureChanged.get()); 2702 testLayoutManager.expectLayouts(1); 2703 adapter.addAndNotify(2, 5); 2704 testLayoutManager.waitForLayout(2); 2705 2706 assertEquals("when items are added, item count in state should be updated", 2707 adapter.getItemCount(), 2708 itemCount.get()); 2709 assertEquals("structure changed should be true when items are removed", true, 2710 structureChanged.get()); 2711 } 2712 2713 @Test 2714 public void testDetachWithoutLayoutManager() throws Throwable { 2715 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2716 runTestOnUiThread(new Runnable() { 2717 @Override 2718 public void run() { 2719 try { 2720 setRecyclerView(recyclerView); 2721 removeRecyclerView(); 2722 } catch (Throwable t) { 2723 postExceptionToInstrumentation(t); 2724 } 2725 } 2726 }); 2727 checkForMainThreadException(); 2728 } 2729 2730 @Test 2731 public void testUpdateHiddenView() throws Throwable { 2732 final RecyclerView.ViewHolder[] mTargetVH = new RecyclerView.ViewHolder[1]; 2733 final RecyclerView recyclerView = new RecyclerView(getActivity()); 2734 final int[] preLayoutRange = new int[]{0, 10}; 2735 final int[] postLayoutRange = new int[]{0, 10}; 2736 final AtomicBoolean enableGetViewTest = new AtomicBoolean(false); 2737 final List<Integer> disappearingPositions = new ArrayList<Integer>(); 2738 final TestLayoutManager tlm = new TestLayoutManager() { 2739 @Override 2740 public boolean supportsPredictiveItemAnimations() { 2741 return true; 2742 } 2743 2744 @Override 2745 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2746 try { 2747 final int[] layoutRange = state.isPreLayout() ? preLayoutRange 2748 : postLayoutRange; 2749 detachAndScrapAttachedViews(recycler); 2750 layoutRange(recycler, layoutRange[0], layoutRange[1]); 2751 if (!state.isPreLayout()) { 2752 for (Integer position : disappearingPositions) { 2753 // test sanity. 2754 assertNull(findViewByPosition(position)); 2755 final View view = recycler.getViewForPosition(position); 2756 addDisappearingView(view); 2757 measureChildWithMargins(view, 0, 0); 2758 // position item out of bounds. 2759 view.layout(0, -500, view.getMeasuredWidth(), 2760 -500 + view.getMeasuredHeight()); 2761 } 2762 } 2763 } catch (Throwable t) { 2764 postExceptionToInstrumentation(t); 2765 } 2766 layoutLatch.countDown(); 2767 } 2768 }; 2769 2770 recyclerView.getItemAnimator().setMoveDuration(2000); 2771 recyclerView.getItemAnimator().setRemoveDuration(2000); 2772 final TestAdapter adapter = new TestAdapter(100); 2773 recyclerView.setAdapter(adapter); 2774 recyclerView.setLayoutManager(tlm); 2775 tlm.expectLayouts(1); 2776 setRecyclerView(recyclerView); 2777 2778 tlm.waitForLayout(1); 2779 checkForMainThreadException(); 2780 mTargetVH[0] = recyclerView.findViewHolderForAdapterPosition(0); 2781 // now, a child disappears 2782 disappearingPositions.add(0); 2783 // layout one shifted 2784 postLayoutRange[0] = 1; 2785 postLayoutRange[1] = 11; 2786 tlm.expectLayouts(2); 2787 adapter.addAndNotify(8, 1); 2788 tlm.waitForLayout(2); 2789 checkForMainThreadException(); 2790 2791 tlm.expectLayouts(2); 2792 disappearingPositions.clear(); 2793 // now that item should be moving, invalidate it and delete it. 2794 enableGetViewTest.set(true); 2795 runTestOnUiThread(new Runnable() { 2796 @Override 2797 public void run() { 2798 try { 2799 adapter.changeAndNotify(0, 1); 2800 adapter.deleteAndNotify(0, 1); 2801 } catch (Throwable throwable) { 2802 throwable.printStackTrace(); 2803 } 2804 } 2805 }); 2806 tlm.waitForLayout(2); 2807 checkForMainThreadException(); 2808 } 2809 2810 @Test 2811 public void testFocusBigViewOnTop() throws Throwable { 2812 focusTooBigViewTest(Gravity.TOP); 2813 } 2814 2815 @Test 2816 public void testFocusBigViewOnLeft() throws Throwable { 2817 focusTooBigViewTest(Gravity.LEFT); 2818 } 2819 2820 @Test 2821 public void testFocusBigViewOnRight() throws Throwable { 2822 focusTooBigViewTest(Gravity.RIGHT); 2823 } 2824 2825 @Test 2826 public void testFocusBigViewOnBottom() throws Throwable { 2827 focusTooBigViewTest(Gravity.BOTTOM); 2828 } 2829 2830 @Test 2831 public void testFocusBigViewOnLeftRTL() throws Throwable { 2832 focusTooBigViewTest(Gravity.LEFT, true); 2833 assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL, 2834 mRecyclerView.getLayoutManager().getLayoutDirection()); 2835 } 2836 2837 @Test 2838 public void testFocusBigViewOnRightRTL() throws Throwable { 2839 focusTooBigViewTest(Gravity.RIGHT, true); 2840 assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL, 2841 mRecyclerView.getLayoutManager().getLayoutDirection()); 2842 } 2843 2844 public void focusTooBigViewTest(final int gravity) throws Throwable { 2845 focusTooBigViewTest(gravity, false); 2846 } 2847 2848 public void focusTooBigViewTest(final int gravity, final boolean rtl) throws Throwable { 2849 RecyclerView rv = new RecyclerView(getActivity()); 2850 if (rtl) { 2851 ViewCompat.setLayoutDirection(rv, ViewCompat.LAYOUT_DIRECTION_RTL); 2852 } 2853 final AtomicInteger vScrollDist = new AtomicInteger(0); 2854 final AtomicInteger hScrollDist = new AtomicInteger(0); 2855 final AtomicInteger vDesiredDist = new AtomicInteger(0); 2856 final AtomicInteger hDesiredDist = new AtomicInteger(0); 2857 TestLayoutManager tlm = new TestLayoutManager() { 2858 2859 @Override 2860 public int getLayoutDirection() { 2861 return rtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR; 2862 } 2863 2864 @Override 2865 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2866 detachAndScrapAttachedViews(recycler); 2867 final View view = recycler.getViewForPosition(0); 2868 addView(view); 2869 int left = 0, top = 0; 2870 view.setBackgroundColor(Color.rgb(0, 0, 255)); 2871 switch (gravity) { 2872 case Gravity.LEFT: 2873 case Gravity.RIGHT: 2874 view.measure( 2875 View.MeasureSpec.makeMeasureSpec((int) (getWidth() * 1.5), 2876 View.MeasureSpec.EXACTLY), 2877 View.MeasureSpec.makeMeasureSpec((int) (getHeight() * .9), 2878 View.MeasureSpec.AT_MOST)); 2879 left = gravity == Gravity.LEFT ? getWidth() - view.getMeasuredWidth() - 80 2880 : 90; 2881 top = 0; 2882 if (ViewCompat.LAYOUT_DIRECTION_RTL == getLayoutDirection()) { 2883 hDesiredDist.set((left + view.getMeasuredWidth()) - getWidth()); 2884 } else { 2885 hDesiredDist.set(left); 2886 } 2887 break; 2888 case Gravity.TOP: 2889 case Gravity.BOTTOM: 2890 view.measure( 2891 View.MeasureSpec.makeMeasureSpec((int) (getWidth() * .9), 2892 View.MeasureSpec.AT_MOST), 2893 View.MeasureSpec.makeMeasureSpec((int) (getHeight() * 1.5), 2894 View.MeasureSpec.EXACTLY)); 2895 top = gravity == Gravity.TOP ? getHeight() - view.getMeasuredHeight() - 2896 80 : 90; 2897 left = 0; 2898 vDesiredDist.set(top); 2899 break; 2900 } 2901 2902 view.layout(left, top, left + view.getMeasuredWidth(), 2903 top + view.getMeasuredHeight()); 2904 layoutLatch.countDown(); 2905 } 2906 2907 @Override 2908 public boolean canScrollVertically() { 2909 return true; 2910 } 2911 2912 @Override 2913 public boolean canScrollHorizontally() { 2914 return super.canScrollHorizontally(); 2915 } 2916 2917 @Override 2918 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 2919 RecyclerView.State state) { 2920 vScrollDist.addAndGet(dy); 2921 getChildAt(0).offsetTopAndBottom(-dy); 2922 return dy; 2923 } 2924 2925 @Override 2926 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 2927 RecyclerView.State state) { 2928 hScrollDist.addAndGet(dx); 2929 getChildAt(0).offsetLeftAndRight(-dx); 2930 return dx; 2931 } 2932 }; 2933 TestAdapter adapter = new TestAdapter(10); 2934 rv.setAdapter(adapter); 2935 rv.setLayoutManager(tlm); 2936 tlm.expectLayouts(1); 2937 setRecyclerView(rv); 2938 tlm.waitForLayout(2); 2939 View view = rv.getChildAt(0); 2940 requestFocus(view); 2941 Thread.sleep(1000); 2942 assertEquals(vDesiredDist.get(), vScrollDist.get()); 2943 assertEquals(hDesiredDist.get(), hScrollDist.get()); 2944 assertEquals(mRecyclerView.getPaddingTop(), view.getTop()); 2945 if (rtl) { 2946 assertEquals(mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(), 2947 view.getRight()); 2948 } else { 2949 assertEquals(mRecyclerView.getPaddingLeft(), view.getLeft()); 2950 } 2951 } 2952 2953 @Test 2954 public void testFocusRectOnScreenWithDecorOffsets() throws Throwable { 2955 focusRectOnScreenTest(true); 2956 } 2957 2958 @Test 2959 public void testFocusRectOnScreenWithout() throws Throwable { 2960 focusRectOnScreenTest(false); 2961 } 2962 2963 2964 public void focusRectOnScreenTest(boolean addItemDecors) throws Throwable { 2965 RecyclerView rv = new RecyclerView(getActivity()); 2966 final AtomicInteger scrollDist = new AtomicInteger(0); 2967 TestLayoutManager tlm = new TestLayoutManager() { 2968 @Override 2969 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2970 detachAndScrapAttachedViews(recycler); 2971 final View view = recycler.getViewForPosition(0); 2972 addView(view); 2973 measureChildWithMargins(view, 0, 0); 2974 view.layout(0, -20, view.getWidth(), 2975 -20 + view.getHeight());// ignore decors on purpose 2976 layoutLatch.countDown(); 2977 } 2978 2979 @Override 2980 public boolean canScrollVertically() { 2981 return true; 2982 } 2983 2984 @Override 2985 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 2986 RecyclerView.State state) { 2987 scrollDist.addAndGet(dy); 2988 return dy; 2989 } 2990 }; 2991 TestAdapter adapter = new TestAdapter(10); 2992 if (addItemDecors) { 2993 rv.addItemDecoration(new RecyclerView.ItemDecoration() { 2994 @Override 2995 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 2996 RecyclerView.State state) { 2997 outRect.set(0, 10, 0, 10); 2998 } 2999 }); 3000 } 3001 rv.setAdapter(adapter); 3002 rv.setLayoutManager(tlm); 3003 tlm.expectLayouts(1); 3004 setRecyclerView(rv); 3005 tlm.waitForLayout(2); 3006 3007 View view = rv.getChildAt(0); 3008 requestFocus(view); 3009 Thread.sleep(1000); 3010 assertEquals(addItemDecors ? -30 : -20, scrollDist.get()); 3011 } 3012 3013 @Test 3014 public void testUnimplementedSmoothScroll() throws Throwable { 3015 final AtomicInteger receivedScrollToPosition = new AtomicInteger(-1); 3016 final AtomicInteger receivedSmoothScrollToPosition = new AtomicInteger(-1); 3017 final CountDownLatch cbLatch = new CountDownLatch(2); 3018 TestLayoutManager tlm = new TestLayoutManager() { 3019 @Override 3020 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3021 detachAndScrapAttachedViews(recycler); 3022 layoutRange(recycler, 0, 10); 3023 layoutLatch.countDown(); 3024 } 3025 3026 @Override 3027 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 3028 int position) { 3029 assertEquals(-1, receivedSmoothScrollToPosition.get()); 3030 receivedSmoothScrollToPosition.set(position); 3031 RecyclerView.SmoothScroller ss = 3032 new LinearSmoothScroller(recyclerView.getContext()) { 3033 @Override 3034 public PointF computeScrollVectorForPosition(int targetPosition) { 3035 return null; 3036 } 3037 }; 3038 ss.setTargetPosition(position); 3039 startSmoothScroll(ss); 3040 cbLatch.countDown(); 3041 } 3042 3043 @Override 3044 public void scrollToPosition(int position) { 3045 assertEquals(-1, receivedScrollToPosition.get()); 3046 receivedScrollToPosition.set(position); 3047 cbLatch.countDown(); 3048 } 3049 }; 3050 RecyclerView rv = new RecyclerView(getActivity()); 3051 rv.setAdapter(new TestAdapter(100)); 3052 rv.setLayoutManager(tlm); 3053 tlm.expectLayouts(1); 3054 setRecyclerView(rv); 3055 tlm.waitForLayout(2); 3056 freezeLayout(true); 3057 smoothScrollToPosition(35); 3058 assertEquals("smoothScrollToPosition should be ignored when frozen", 3059 -1, receivedSmoothScrollToPosition.get()); 3060 freezeLayout(false); 3061 smoothScrollToPosition(35); 3062 assertTrue("both scrolls should be called", cbLatch.await(3, TimeUnit.SECONDS)); 3063 checkForMainThreadException(); 3064 assertEquals(35, receivedSmoothScrollToPosition.get()); 3065 assertEquals(35, receivedScrollToPosition.get()); 3066 } 3067 3068 @Test 3069 public void testJumpingJackSmoothScroller() throws Throwable { 3070 jumpingJackSmoothScrollerTest(true); 3071 } 3072 3073 @Test 3074 public void testJumpingJackSmoothScrollerGoesIdle() throws Throwable { 3075 jumpingJackSmoothScrollerTest(false); 3076 } 3077 3078 private void jumpingJackSmoothScrollerTest(final boolean succeed) throws Throwable { 3079 final List<Integer> receivedScrollToPositions = new ArrayList<>(); 3080 final TestAdapter testAdapter = new TestAdapter(200); 3081 final AtomicBoolean mTargetFound = new AtomicBoolean(false); 3082 TestLayoutManager tlm = new TestLayoutManager() { 3083 int pendingScrollPosition = -1; 3084 @Override 3085 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 3086 detachAndScrapAttachedViews(recycler); 3087 final int pos = pendingScrollPosition < 0 ? 0: pendingScrollPosition; 3088 layoutRange(recycler, pos, pos + 10); 3089 if (layoutLatch != null) { 3090 layoutLatch.countDown(); 3091 } 3092 } 3093 3094 @Override 3095 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 3096 final int position) { 3097 RecyclerView.SmoothScroller ss = 3098 new LinearSmoothScroller(recyclerView.getContext()) { 3099 @Override 3100 public PointF computeScrollVectorForPosition(int targetPosition) { 3101 return new PointF(0, 1); 3102 } 3103 3104 @Override 3105 protected void onTargetFound(View targetView, RecyclerView.State state, 3106 Action action) { 3107 super.onTargetFound(targetView, state, action); 3108 mTargetFound.set(true); 3109 } 3110 3111 @Override 3112 protected void updateActionForInterimTarget(Action action) { 3113 int limit = succeed ? getTargetPosition() : 100; 3114 if (pendingScrollPosition + 2 < limit) { 3115 if (pendingScrollPosition != NO_POSITION) { 3116 assertEquals(pendingScrollPosition, 3117 getChildViewHolderInt(getChildAt(0)) 3118 .getAdapterPosition()); 3119 } 3120 action.jumpTo(pendingScrollPosition + 2); 3121 } 3122 } 3123 }; 3124 ss.setTargetPosition(position); 3125 startSmoothScroll(ss); 3126 } 3127 3128 @Override 3129 public void scrollToPosition(int position) { 3130 receivedScrollToPositions.add(position); 3131 pendingScrollPosition = position; 3132 requestLayout(); 3133 } 3134 }; 3135 final RecyclerView rv = new RecyclerView(getActivity()); 3136 rv.setAdapter(testAdapter); 3137 rv.setLayoutManager(tlm); 3138 3139 tlm.expectLayouts(1); 3140 setRecyclerView(rv); 3141 tlm.waitForLayout(2); 3142 3143 runTestOnUiThread(new Runnable() { 3144 @Override 3145 public void run() { 3146 rv.smoothScrollToPosition(150); 3147 } 3148 }); 3149 int limit = 100; 3150 while (rv.getLayoutManager().isSmoothScrolling() && --limit > 0) { 3151 Thread.sleep(200); 3152 checkForMainThreadException(); 3153 } 3154 checkForMainThreadException(); 3155 assertTrue(limit > 0); 3156 for (int i = 1; i < 100; i+=2) { 3157 assertTrue("scroll positions must include " + i, receivedScrollToPositions.contains(i)); 3158 } 3159 3160 assertEquals(succeed, mTargetFound.get()); 3161 3162 } 3163 3164 private static class TestViewHolder2 extends RecyclerView.ViewHolder { 3165 3166 Object mData; 3167 3168 public TestViewHolder2(View itemView) { 3169 super(itemView); 3170 } 3171 } 3172 3173 private static class TestAdapter2 extends RecyclerView.Adapter<TestViewHolder2> { 3174 3175 List<Item> mItems; 3176 3177 private TestAdapter2(int count) { 3178 mItems = new ArrayList<Item>(count); 3179 for (int i = 0; i < count; i++) { 3180 mItems.add(new Item(i, "Item " + i)); 3181 } 3182 } 3183 3184 @Override 3185 public TestViewHolder2 onCreateViewHolder(ViewGroup parent, 3186 int viewType) { 3187 return new TestViewHolder2(new TextView(parent.getContext())); 3188 } 3189 3190 @Override 3191 public void onBindViewHolder(TestViewHolder2 holder, int position) { 3192 final Item item = mItems.get(position); 3193 ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")"); 3194 } 3195 3196 @Override 3197 public int getItemCount() { 3198 return mItems.size(); 3199 } 3200 } 3201 3202 private static interface AdapterRunnable { 3203 3204 public void run(TestAdapter adapter) throws Throwable; 3205 } 3206 3207 } 3208