BaseRecyclerViewInstrumentationTest.java revision 3911e1c2d38e301d5ffbdf11f808fdc593dd83e9
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 17package android.support.v7.widget; 18 19import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE; 20 21import org.hamcrest.CoreMatchers; 22import org.hamcrest.MatcherAssert; 23import org.junit.After; 24import org.junit.Before; 25import org.junit.Rule; 26 27import android.app.Instrumentation; 28import android.graphics.Rect; 29import android.os.Looper; 30import android.support.annotation.Nullable; 31import android.support.test.InstrumentationRegistry; 32import android.support.test.rule.ActivityTestRule; 33import android.support.v4.view.ViewCompat; 34import android.support.v7.recyclerview.test.SameActivityTestRule; 35import android.util.Log; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.ViewGroup; 39import android.widget.FrameLayout; 40import android.widget.TextView; 41 42import java.lang.reflect.InvocationTargetException; 43import java.lang.reflect.Method; 44import java.util.ArrayList; 45import java.util.HashSet; 46import java.util.List; 47import java.util.Set; 48import java.util.concurrent.CountDownLatch; 49import java.util.concurrent.TimeUnit; 50import java.util.concurrent.atomic.AtomicBoolean; 51import java.util.concurrent.atomic.AtomicInteger; 52import java.util.concurrent.locks.ReentrantLock; 53import android.support.v7.recyclerview.test.R; 54 55import static org.junit.Assert.*; 56 57import static java.util.concurrent.TimeUnit.SECONDS; 58 59abstract public class BaseRecyclerViewInstrumentationTest { 60 61 private static final String TAG = "RecyclerViewTest"; 62 63 private boolean mDebug; 64 65 protected RecyclerView mRecyclerView; 66 67 protected AdapterHelper mAdapterHelper; 68 69 private Throwable mMainThreadException; 70 71 private boolean mIgnoreMainThreadException = false; 72 73 Thread mInstrumentationThread; 74 75 @Rule 76 public ActivityTestRule<TestActivity> mActivityRule = new SameActivityTestRule() { 77 @Override 78 public boolean canReUseActivity() { 79 return BaseRecyclerViewInstrumentationTest.this.canReUseActivity(); 80 } 81 }; 82 83 public BaseRecyclerViewInstrumentationTest() { 84 this(false); 85 } 86 87 public BaseRecyclerViewInstrumentationTest(boolean debug) { 88 mDebug = debug; 89 } 90 91 void checkForMainThreadException() throws Throwable { 92 if (!mIgnoreMainThreadException && mMainThreadException != null) { 93 throw mMainThreadException; 94 } 95 } 96 97 public void setIgnoreMainThreadException(boolean ignoreMainThreadException) { 98 mIgnoreMainThreadException = ignoreMainThreadException; 99 } 100 101 public Throwable getMainThreadException() { 102 return mMainThreadException; 103 } 104 105 protected TestActivity getActivity() { 106 return mActivityRule.getActivity(); 107 } 108 109 @Before 110 public final void setUpInsThread() throws Exception { 111 mInstrumentationThread = Thread.currentThread(); 112 Item.idCounter.set(0); 113 } 114 115 void setHasTransientState(final View view, final boolean value) { 116 try { 117 runTestOnUiThread(new Runnable() { 118 @Override 119 public void run() { 120 ViewCompat.setHasTransientState(view, value); 121 } 122 }); 123 } catch (Throwable throwable) { 124 Log.e(TAG, "", throwable); 125 } 126 } 127 128 public boolean canReUseActivity() { 129 return true; 130 } 131 132 protected void enableAccessibility() 133 throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 134 Method getUIAutomation = Instrumentation.class.getMethod("getUiAutomation"); 135 getUIAutomation.invoke(InstrumentationRegistry.getInstrumentation()); 136 } 137 138 void setAdapter(final RecyclerView.Adapter adapter) throws Throwable { 139 runTestOnUiThread(new Runnable() { 140 @Override 141 public void run() { 142 mRecyclerView.setAdapter(adapter); 143 } 144 }); 145 } 146 147 public View focusSearch(final View focused, final int direction) 148 throws Throwable { 149 final View[] result = new View[1]; 150 runTestOnUiThread(new Runnable() { 151 @Override 152 public void run() { 153 View view = focused.focusSearch(direction); 154 if (view != null && view != focused) { 155 view.requestFocus(); 156 } 157 result[0] = view; 158 } 159 }); 160 return result[0]; 161 } 162 163 protected WrappedRecyclerView inflateWrappedRV() { 164 return (WrappedRecyclerView) 165 LayoutInflater.from(getActivity()).inflate(R.layout.wrapped_test_rv, 166 getRecyclerViewContainer(), false); 167 } 168 169 void swapAdapter(final RecyclerView.Adapter adapter, 170 final boolean removeAndRecycleExistingViews) throws Throwable { 171 runTestOnUiThread(new Runnable() { 172 @Override 173 public void run() { 174 try { 175 mRecyclerView.swapAdapter(adapter, removeAndRecycleExistingViews); 176 } catch (Throwable t) { 177 postExceptionToInstrumentation(t); 178 } 179 } 180 }); 181 checkForMainThreadException(); 182 } 183 184 void postExceptionToInstrumentation(Throwable t) { 185 if (mInstrumentationThread == Thread.currentThread()) { 186 throw new RuntimeException(t); 187 } 188 if (mMainThreadException != null) { 189 Log.e(TAG, "receiving another main thread exception. dropping.", t); 190 } else { 191 Log.e(TAG, "captured exception on main thread", t); 192 mMainThreadException = t; 193 } 194 195 if (mRecyclerView != null && mRecyclerView 196 .getLayoutManager() instanceof TestLayoutManager) { 197 TestLayoutManager lm = (TestLayoutManager) mRecyclerView.getLayoutManager(); 198 // finish all layouts so that we get the correct exception 199 if (lm.layoutLatch != null) { 200 while (lm.layoutLatch.getCount() > 0) { 201 lm.layoutLatch.countDown(); 202 } 203 } 204 } 205 } 206 207 public Instrumentation getInstrumentation() { 208 return InstrumentationRegistry.getInstrumentation(); 209 } 210 211 @After 212 public final void tearDown() throws Exception { 213 if (mRecyclerView != null) { 214 try { 215 removeRecyclerView(); 216 } catch (Throwable throwable) { 217 throwable.printStackTrace(); 218 } 219 } 220 getInstrumentation().waitForIdleSync(); 221 222 try { 223 checkForMainThreadException(); 224 } catch (Exception e) { 225 throw e; 226 } catch (Throwable throwable) { 227 throw new Exception(Log.getStackTraceString(throwable)); 228 } 229 } 230 231 public Rect getDecoratedRecyclerViewBounds() { 232 return new Rect( 233 mRecyclerView.getPaddingLeft(), 234 mRecyclerView.getPaddingTop(), 235 mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(), 236 mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom() 237 ); 238 } 239 240 public void removeRecyclerView() throws Throwable { 241 if (mRecyclerView == null) { 242 return; 243 } 244 if (!isMainThread()) { 245 getInstrumentation().waitForIdleSync(); 246 } 247 runTestOnUiThread(new Runnable() { 248 @Override 249 public void run() { 250 try { 251 // do not run validation if we already have an error 252 if (mMainThreadException == null) { 253 final RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); 254 if (adapter instanceof AttachDetachCountingAdapter) { 255 ((AttachDetachCountingAdapter) adapter).getCounter() 256 .validateRemaining(mRecyclerView); 257 } 258 } 259 getActivity().getContainer().removeAllViews(); 260 } catch (Throwable t) { 261 postExceptionToInstrumentation(t); 262 } 263 } 264 }); 265 mRecyclerView = null; 266 } 267 268 void waitForAnimations(int seconds) throws Throwable { 269 final CountDownLatch latch = new CountDownLatch(1); 270 runTestOnUiThread(new Runnable() { 271 @Override 272 public void run() { 273 mRecyclerView.mItemAnimator 274 .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 275 @Override 276 public void onAnimationsFinished() { 277 latch.countDown(); 278 } 279 }); 280 } 281 }); 282 283 assertTrue("animations didn't finish on expected time of " + seconds + " seconds", 284 latch.await(seconds, TimeUnit.SECONDS)); 285 } 286 287 public void waitForIdleScroll(final RecyclerView recyclerView) throws Throwable { 288 final CountDownLatch latch = new CountDownLatch(1); 289 runTestOnUiThread(new Runnable() { 290 @Override 291 public void run() { 292 RecyclerView.OnScrollListener listener = new RecyclerView.OnScrollListener() { 293 @Override 294 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 295 if (newState == SCROLL_STATE_IDLE) { 296 latch.countDown(); 297 recyclerView.removeOnScrollListener(this); 298 } 299 } 300 }; 301 if (recyclerView.getScrollState() == SCROLL_STATE_IDLE) { 302 latch.countDown(); 303 } else { 304 recyclerView.addOnScrollListener(listener); 305 } 306 } 307 }); 308 assertTrue("should go idle in 10 seconds", latch.await(10, TimeUnit.SECONDS)); 309 } 310 311 public boolean requestFocus(final View view, boolean waitForScroll) throws Throwable { 312 final boolean[] result = new boolean[1]; 313 try { 314 runTestOnUiThread(new Runnable() { 315 @Override 316 public void run() { 317 result[0] = view.requestFocus(); 318 } 319 }); 320 } catch (Throwable throwable) { 321 fail(throwable.getMessage()); 322 } 323 if (waitForScroll && result[0]) { 324 waitForIdleScroll(mRecyclerView); 325 } 326 return result[0]; 327 } 328 329 public void setRecyclerView(final RecyclerView recyclerView) throws Throwable { 330 setRecyclerView(recyclerView, true); 331 } 332 public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool) 333 throws Throwable { 334 setRecyclerView(recyclerView, assignDummyPool, true); 335 } 336 public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool, 337 boolean addPositionCheckItemAnimator) 338 throws Throwable { 339 mRecyclerView = recyclerView; 340 if (assignDummyPool) { 341 RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() { 342 @Override 343 public RecyclerView.ViewHolder getRecycledView(int viewType) { 344 RecyclerView.ViewHolder viewHolder = super.getRecycledView(viewType); 345 if (viewHolder == null) { 346 return null; 347 } 348 viewHolder.addFlags(RecyclerView.ViewHolder.FLAG_BOUND); 349 viewHolder.mPosition = 200; 350 viewHolder.mOldPosition = 300; 351 viewHolder.mPreLayoutPosition = 500; 352 return viewHolder; 353 } 354 355 @Override 356 public void putRecycledView(RecyclerView.ViewHolder scrap) { 357 assertNull(scrap.mOwnerRecyclerView); 358 super.putRecycledView(scrap); 359 } 360 }; 361 mRecyclerView.setRecycledViewPool(pool); 362 } 363 if (addPositionCheckItemAnimator) { 364 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { 365 @Override 366 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 367 RecyclerView.State state) { 368 RecyclerView.ViewHolder vh = parent.getChildViewHolder(view); 369 if (!vh.isRemoved()) { 370 assertNotSame("If getItemOffsets is called, child should have a valid" 371 + " adapter position unless it is removed : " + vh, 372 vh.getAdapterPosition(), RecyclerView.NO_POSITION); 373 } 374 } 375 }); 376 } 377 mAdapterHelper = recyclerView.mAdapterHelper; 378 runTestOnUiThread(new Runnable() { 379 @Override 380 public void run() { 381 getActivity().getContainer().addView(recyclerView); 382 } 383 }); 384 } 385 386 protected FrameLayout getRecyclerViewContainer() { 387 return getActivity().getContainer(); 388 } 389 390 public void requestLayoutOnUIThread(final View view) { 391 try { 392 runTestOnUiThread(new Runnable() { 393 @Override 394 public void run() { 395 view.requestLayout(); 396 } 397 }); 398 } catch (Throwable throwable) { 399 Log.e(TAG, "", throwable); 400 } 401 } 402 403 public void scrollBy(final int dt) { 404 try { 405 runTestOnUiThread(new Runnable() { 406 @Override 407 public void run() { 408 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) { 409 mRecyclerView.scrollBy(dt, 0); 410 } else { 411 mRecyclerView.scrollBy(0, dt); 412 } 413 414 } 415 }); 416 } catch (Throwable throwable) { 417 Log.e(TAG, "", throwable); 418 } 419 } 420 421 public void smoothScrollBy(final int dt) { 422 try { 423 runTestOnUiThread(new Runnable() { 424 @Override 425 public void run() { 426 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) { 427 mRecyclerView.smoothScrollBy(dt, 0); 428 } else { 429 mRecyclerView.smoothScrollBy(0, dt); 430 } 431 432 } 433 }); 434 } catch (Throwable throwable) { 435 Log.e(TAG, "", throwable); 436 } 437 getInstrumentation().waitForIdleSync(); 438 } 439 440 void scrollToPosition(final int position) throws Throwable { 441 runTestOnUiThread(new Runnable() { 442 @Override 443 public void run() { 444 mRecyclerView.getLayoutManager().scrollToPosition(position); 445 } 446 }); 447 } 448 449 void smoothScrollToPosition(final int position) throws Throwable { 450 smoothScrollToPosition(position, true); 451 } 452 453 void smoothScrollToPosition(final int position, boolean assertArrival) throws Throwable { 454 if (mDebug) { 455 Log.d(TAG, "SMOOTH scrolling to " + position); 456 } 457 final CountDownLatch viewAdded = new CountDownLatch(1); 458 final RecyclerView.OnChildAttachStateChangeListener listener = 459 new RecyclerView.OnChildAttachStateChangeListener() { 460 @Override 461 public void onChildViewAttachedToWindow(View view) { 462 if (position == mRecyclerView.getChildAdapterPosition(view)) { 463 viewAdded.countDown(); 464 } 465 } 466 @Override 467 public void onChildViewDetachedFromWindow(View view) { 468 } 469 }; 470 final AtomicBoolean addedListener = new AtomicBoolean(false); 471 runTestOnUiThread(new Runnable() { 472 @Override 473 public void run() { 474 RecyclerView.ViewHolder viewHolderForAdapterPosition = 475 mRecyclerView.findViewHolderForAdapterPosition(position); 476 if (viewHolderForAdapterPosition != null) { 477 viewAdded.countDown(); 478 } else { 479 mRecyclerView.addOnChildAttachStateChangeListener(listener); 480 addedListener.set(true); 481 } 482 483 } 484 }); 485 runTestOnUiThread(new Runnable() { 486 @Override 487 public void run() { 488 mRecyclerView.smoothScrollToPosition(position); 489 } 490 }); 491 getInstrumentation().waitForIdleSync(); 492 assertThat("should be able to scroll in 10 seconds", !assertArrival || 493 viewAdded.await(10, TimeUnit.SECONDS), 494 CoreMatchers.is(true)); 495 waitForIdleScroll(mRecyclerView); 496 if (mDebug) { 497 Log.d(TAG, "SMOOTH scrolling done"); 498 } 499 if (addedListener.get()) { 500 runTestOnUiThread(new Runnable() { 501 @Override 502 public void run() { 503 mRecyclerView.removeOnChildAttachStateChangeListener(listener); 504 } 505 }); 506 } 507 getInstrumentation().waitForIdleSync(); 508 } 509 510 void freezeLayout(final boolean freeze) throws Throwable { 511 runTestOnUiThread(new Runnable() { 512 @Override 513 public void run() { 514 mRecyclerView.setLayoutFrozen(freeze); 515 } 516 }); 517 } 518 519 public void setVisibility(final View view, final int visibility) throws Throwable { 520 runTestOnUiThread(new Runnable() { 521 @Override 522 public void run() { 523 view.setVisibility(visibility); 524 } 525 }); 526 } 527 528 public class TestViewHolder extends RecyclerView.ViewHolder { 529 530 Item mBoundItem; 531 Object mData; 532 533 public TestViewHolder(View itemView) { 534 super(itemView); 535 itemView.setFocusable(true); 536 } 537 538 @Override 539 public String toString() { 540 return super.toString() + " item:" + mBoundItem + ", data:" + mData; 541 } 542 543 public Object getData() { 544 return mData; 545 } 546 547 public void setData(Object data) { 548 mData = data; 549 } 550 } 551 class DumbLayoutManager extends TestLayoutManager { 552 @Override 553 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 554 detachAndScrapAttachedViews(recycler); 555 layoutRange(recycler, 0, state.getItemCount()); 556 if (layoutLatch != null) { 557 layoutLatch.countDown(); 558 } 559 } 560 } 561 public class TestLayoutManager extends RecyclerView.LayoutManager { 562 int mScrollVerticallyAmount; 563 int mScrollHorizontallyAmount; 564 protected CountDownLatch layoutLatch; 565 private boolean mSupportsPredictive = false; 566 567 public void expectLayouts(int count) { 568 layoutLatch = new CountDownLatch(count); 569 } 570 571 public void waitForLayout(int seconds) throws Throwable { 572 layoutLatch.await(seconds * (mDebug ? 1000 : 1), SECONDS); 573 checkForMainThreadException(); 574 MatcherAssert.assertThat("all layouts should complete on time", 575 layoutLatch.getCount(), CoreMatchers.is(0L)); 576 // use a runnable to ensure RV layout is finished 577 getInstrumentation().runOnMainSync(new Runnable() { 578 @Override 579 public void run() { 580 } 581 }); 582 } 583 584 public boolean isSupportsPredictive() { 585 return mSupportsPredictive; 586 } 587 588 public void setSupportsPredictive(boolean supportsPredictive) { 589 mSupportsPredictive = supportsPredictive; 590 } 591 592 @Override 593 public boolean supportsPredictiveItemAnimations() { 594 return mSupportsPredictive; 595 } 596 597 public void assertLayoutCount(int count, String msg, long timeout) throws Throwable { 598 layoutLatch.await(timeout, TimeUnit.SECONDS); 599 assertEquals(msg, count, layoutLatch.getCount()); 600 } 601 602 public void assertNoLayout(String msg, long timeout) throws Throwable { 603 layoutLatch.await(timeout, TimeUnit.SECONDS); 604 assertFalse(msg, layoutLatch.getCount() == 0); 605 } 606 607 @Override 608 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 609 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 610 ViewGroup.LayoutParams.WRAP_CONTENT); 611 } 612 613 void assertVisibleItemPositions() { 614 int i = getChildCount(); 615 TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter(); 616 while (i-- > 0) { 617 View view = getChildAt(i); 618 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); 619 Item item = ((TestViewHolder) lp.mViewHolder).mBoundItem; 620 if (mDebug) { 621 Log.d(TAG, "testing item " + i); 622 } 623 if (!lp.isItemRemoved()) { 624 RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view); 625 assertSame("item position in LP should match adapter value :" + vh, 626 testAdapter.mItems.get(vh.mPosition), item); 627 } 628 } 629 } 630 631 RecyclerView.LayoutParams getLp(View v) { 632 return (RecyclerView.LayoutParams) v.getLayoutParams(); 633 } 634 635 protected void layoutRange(RecyclerView.Recycler recycler, int start, int end) { 636 assertScrap(recycler); 637 if (mDebug) { 638 Log.d(TAG, "will layout items from " + start + " to " + end); 639 } 640 int diff = end > start ? 1 : -1; 641 int top = 0; 642 for (int i = start; i != end; i+=diff) { 643 if (mDebug) { 644 Log.d(TAG, "laying out item " + i); 645 } 646 View view = recycler.getViewForPosition(i); 647 assertNotNull("view should not be null for valid position. " 648 + "got null view at position " + i, view); 649 if (!mRecyclerView.mState.isPreLayout()) { 650 RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view 651 .getLayoutParams(); 652 assertFalse("In post layout, getViewForPosition should never return a view " 653 + "that is removed", layoutParams != null 654 && layoutParams.isItemRemoved()); 655 656 } 657 assertEquals("getViewForPosition should return correct position", 658 i, getPosition(view)); 659 addView(view); 660 measureChildWithMargins(view, 0, 0); 661 if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { 662 layoutDecorated(view, getWidth() - getDecoratedMeasuredWidth(view), top, 663 getWidth(), top + getDecoratedMeasuredHeight(view)); 664 } else { 665 layoutDecorated(view, 0, top, getDecoratedMeasuredWidth(view) 666 , top + getDecoratedMeasuredHeight(view)); 667 } 668 669 top += view.getMeasuredHeight(); 670 } 671 } 672 673 private void assertScrap(RecyclerView.Recycler recycler) { 674 if (mRecyclerView.getAdapter() != null && 675 !mRecyclerView.getAdapter().hasStableIds()) { 676 for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) { 677 assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid()); 678 } 679 } 680 } 681 682 @Override 683 public boolean canScrollHorizontally() { 684 return true; 685 } 686 687 @Override 688 public boolean canScrollVertically() { 689 return true; 690 } 691 692 @Override 693 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 694 RecyclerView.State state) { 695 mScrollHorizontallyAmount += dx; 696 return dx; 697 } 698 699 @Override 700 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 701 RecyclerView.State state) { 702 mScrollVerticallyAmount += dy; 703 return dy; 704 } 705 706 // START MOCKITO OVERRIDES 707 // We override package protected methods to make them public. This is necessary to run 708 // mockito on Kitkat 709 @Override 710 public void setRecyclerView(RecyclerView recyclerView) { 711 super.setRecyclerView(recyclerView); 712 } 713 714 @Override 715 public void dispatchAttachedToWindow(RecyclerView view) { 716 super.dispatchAttachedToWindow(view); 717 } 718 719 @Override 720 public void dispatchDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 721 super.dispatchDetachedFromWindow(view, recycler); 722 } 723 724 @Override 725 public void setExactMeasureSpecsFrom(RecyclerView recyclerView) { 726 super.setExactMeasureSpecsFrom(recyclerView); 727 } 728 729 @Override 730 public void setMeasureSpecs(int wSpec, int hSpec) { 731 super.setMeasureSpecs(wSpec, hSpec); 732 } 733 734 @Override 735 public void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) { 736 super.setMeasuredDimensionFromChildren(widthSpec, heightSpec); 737 } 738 739 @Override 740 public boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, 741 RecyclerView.LayoutParams lp) { 742 return super.shouldReMeasureChild(child, widthSpec, heightSpec, lp); 743 } 744 745 @Override 746 public boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, 747 RecyclerView.LayoutParams lp) { 748 return super.shouldMeasureChild(child, widthSpec, heightSpec, lp); 749 } 750 751 @Override 752 public void removeAndRecycleScrapInt(RecyclerView.Recycler recycler) { 753 super.removeAndRecycleScrapInt(recycler); 754 } 755 756 @Override 757 public void stopSmoothScroller() { 758 super.stopSmoothScroller(); 759 } 760 761 // END MOCKITO OVERRIDES 762 } 763 764 static class Item { 765 final static AtomicInteger idCounter = new AtomicInteger(0); 766 final public int mId = idCounter.incrementAndGet(); 767 768 int mAdapterIndex; 769 770 final String mText; 771 int mType = 0; 772 773 Item(int adapterIndex, String text) { 774 mAdapterIndex = adapterIndex; 775 mText = text; 776 } 777 778 @Override 779 public String toString() { 780 return "Item{" + 781 "mId=" + mId + 782 ", originalIndex=" + mAdapterIndex + 783 ", text='" + mText + '\'' + 784 '}'; 785 } 786 } 787 788 public class TestAdapter extends RecyclerView.Adapter<TestViewHolder> 789 implements AttachDetachCountingAdapter { 790 791 public static final String DEFAULT_ITEM_PREFIX = "Item "; 792 793 ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter(); 794 List<Item> mItems; 795 final @Nullable RecyclerView.LayoutParams mLayoutParams; 796 797 public TestAdapter(int count) { 798 this(count, null); 799 } 800 801 public TestAdapter(int count, @Nullable RecyclerView.LayoutParams layoutParams) { 802 mItems = new ArrayList<Item>(count); 803 addItems(0, count, DEFAULT_ITEM_PREFIX); 804 mLayoutParams = layoutParams; 805 } 806 807 private void addItems(int pos, int count, String prefix) { 808 for (int i = 0; i < count; i++, pos++) { 809 mItems.add(pos, new Item(pos, prefix)); 810 } 811 } 812 813 @Override 814 public int getItemViewType(int position) { 815 return getItemAt(position).mType; 816 } 817 818 @Override 819 public void onViewAttachedToWindow(TestViewHolder holder) { 820 super.onViewAttachedToWindow(holder); 821 mAttachmentCounter.onViewAttached(holder); 822 } 823 824 @Override 825 public void onViewDetachedFromWindow(TestViewHolder holder) { 826 super.onViewDetachedFromWindow(holder); 827 mAttachmentCounter.onViewDetached(holder); 828 } 829 830 @Override 831 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 832 super.onAttachedToRecyclerView(recyclerView); 833 mAttachmentCounter.onAttached(recyclerView); 834 } 835 836 @Override 837 public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 838 super.onDetachedFromRecyclerView(recyclerView); 839 mAttachmentCounter.onDetached(recyclerView); 840 } 841 842 @Override 843 public TestViewHolder onCreateViewHolder(ViewGroup parent, 844 int viewType) { 845 TextView itemView = new TextView(parent.getContext()); 846 itemView.setFocusableInTouchMode(true); 847 itemView.setFocusable(true); 848 return new TestViewHolder(itemView); 849 } 850 851 @Override 852 public void onBindViewHolder(TestViewHolder holder, int position) { 853 assertNotNull(holder.mOwnerRecyclerView); 854 assertEquals(position, holder.getAdapterPosition()); 855 final Item item = mItems.get(position); 856 ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mId + ")"); 857 holder.mBoundItem = item; 858 if (mLayoutParams != null) { 859 holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(mLayoutParams)); 860 } 861 } 862 863 public Item getItemAt(int position) { 864 return mItems.get(position); 865 } 866 867 @Override 868 public void onViewRecycled(TestViewHolder holder) { 869 super.onViewRecycled(holder); 870 final int adapterPosition = holder.getAdapterPosition(); 871 final boolean shouldHavePosition = !holder.isRemoved() && holder.isBound() && 872 !holder.isAdapterPositionUnknown() && !holder.isInvalid(); 873 String log = "Position check for " + holder.toString(); 874 assertEquals(log, shouldHavePosition, adapterPosition != RecyclerView.NO_POSITION); 875 if (shouldHavePosition) { 876 assertTrue(log, mItems.size() > adapterPosition); 877 assertSame(log, holder.mBoundItem, mItems.get(adapterPosition)); 878 } 879 } 880 881 public void deleteAndNotify(final int start, final int count) throws Throwable { 882 deleteAndNotify(new int[]{start, count}); 883 } 884 885 /** 886 * Deletes items in the given ranges. 887 * <p> 888 * Note that each operation affects the one after so you should offset them properly. 889 * <p> 890 * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with 891 * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be 892 * A D E. Then it will delete 2,1 which means it will delete E. 893 */ 894 public void deleteAndNotify(final int[]... startCountTuples) throws Throwable { 895 for (int[] tuple : startCountTuples) { 896 tuple[1] = -tuple[1]; 897 } 898 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 899 } 900 901 @Override 902 public long getItemId(int position) { 903 return hasStableIds() ? mItems.get(position).mId : super.getItemId(position); 904 } 905 906 public void offsetOriginalIndices(int start, int offset) { 907 for (int i = start; i < mItems.size(); i++) { 908 mItems.get(i).mAdapterIndex += offset; 909 } 910 } 911 912 /** 913 * @param start inclusive 914 * @param end exclusive 915 * @param offset 916 */ 917 public void offsetOriginalIndicesBetween(int start, int end, int offset) { 918 for (int i = start; i < end && i < mItems.size(); i++) { 919 mItems.get(i).mAdapterIndex += offset; 920 } 921 } 922 923 public void addAndNotify(final int count) throws Throwable { 924 assertEquals(0, mItems.size()); 925 new AddRemoveRunnable(DEFAULT_ITEM_PREFIX, new int[]{0, count}).runOnMainThread(); 926 } 927 928 public void resetItemsTo(final List<Item> testItems) throws Throwable { 929 if (!mItems.isEmpty()) { 930 deleteAndNotify(0, mItems.size()); 931 } 932 mItems = testItems; 933 runTestOnUiThread(new Runnable() { 934 @Override 935 public void run() { 936 notifyItemRangeInserted(0, testItems.size()); 937 } 938 }); 939 } 940 941 public void addAndNotify(final int start, final int count) throws Throwable { 942 addAndNotify(new int[]{start, count}); 943 } 944 945 public void addAndNotify(final int[]... startCountTuples) throws Throwable { 946 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 947 } 948 949 public void dispatchDataSetChanged() throws Throwable { 950 runTestOnUiThread(new Runnable() { 951 @Override 952 public void run() { 953 notifyDataSetChanged(); 954 } 955 }); 956 } 957 958 public void changeAndNotify(final int start, final int count) throws Throwable { 959 runTestOnUiThread(new Runnable() { 960 @Override 961 public void run() { 962 notifyItemRangeChanged(start, count); 963 } 964 }); 965 } 966 967 public void changeAndNotifyWithPayload(final int start, final int count, 968 final Object payload) throws Throwable { 969 runTestOnUiThread(new Runnable() { 970 @Override 971 public void run() { 972 notifyItemRangeChanged(start, count, payload); 973 } 974 }); 975 } 976 977 public void changePositionsAndNotify(final int... positions) throws Throwable { 978 runTestOnUiThread(new Runnable() { 979 @Override 980 public void run() { 981 for (int i = 0; i < positions.length; i += 1) { 982 TestAdapter.super.notifyItemRangeChanged(positions[i], 1); 983 } 984 } 985 }); 986 } 987 988 /** 989 * Similar to other methods but negative count means delete and position count means add. 990 * <p> 991 * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an 992 * item to index 1, then remove an item from index 2 (updated index 2) 993 */ 994 public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable { 995 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 996 } 997 998 @Override 999 public int getItemCount() { 1000 return mItems.size(); 1001 } 1002 1003 /** 1004 * Uses notifyDataSetChanged 1005 */ 1006 public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable { 1007 for (int i = 0; i < fromToTuples.length; i += 1) { 1008 int[] tuple = fromToTuples[i]; 1009 moveItem(tuple[0], tuple[1], false); 1010 } 1011 if (notifyChange) { 1012 dispatchDataSetChanged(); 1013 } 1014 } 1015 1016 /** 1017 * Uses notifyDataSetChanged 1018 */ 1019 public void moveItem(final int from, final int to, final boolean notifyChange) 1020 throws Throwable { 1021 runTestOnUiThread(new Runnable() { 1022 @Override 1023 public void run() { 1024 moveInUIThread(from, to); 1025 if (notifyChange) { 1026 notifyDataSetChanged(); 1027 } 1028 } 1029 }); 1030 } 1031 1032 /** 1033 * Uses notifyItemMoved 1034 */ 1035 public void moveAndNotify(final int from, final int to) throws Throwable { 1036 runTestOnUiThread(new Runnable() { 1037 @Override 1038 public void run() { 1039 moveInUIThread(from, to); 1040 notifyItemMoved(from, to); 1041 } 1042 }); 1043 } 1044 1045 public void clearOnUIThread() { 1046 assertEquals("clearOnUIThread called from a wrong thread", 1047 Looper.getMainLooper(), Looper.myLooper()); 1048 mItems = new ArrayList<Item>(); 1049 notifyDataSetChanged(); 1050 } 1051 1052 protected void moveInUIThread(int from, int to) { 1053 Item item = mItems.remove(from); 1054 offsetOriginalIndices(from, -1); 1055 mItems.add(to, item); 1056 offsetOriginalIndices(to + 1, 1); 1057 item.mAdapterIndex = to; 1058 } 1059 1060 1061 @Override 1062 public ViewAttachDetachCounter getCounter() { 1063 return mAttachmentCounter; 1064 } 1065 1066 private class AddRemoveRunnable implements Runnable { 1067 final String mNewItemPrefix; 1068 final int[][] mStartCountTuples; 1069 1070 public AddRemoveRunnable(String newItemPrefix, int[]... startCountTuples) { 1071 mNewItemPrefix = newItemPrefix; 1072 mStartCountTuples = startCountTuples; 1073 } 1074 1075 public AddRemoveRunnable(int[][] startCountTuples) { 1076 this("new item ", startCountTuples); 1077 } 1078 1079 public void runOnMainThread() throws Throwable { 1080 if (Looper.myLooper() == Looper.getMainLooper()) { 1081 run(); 1082 } else { 1083 runTestOnUiThread(this); 1084 } 1085 } 1086 1087 @Override 1088 public void run() { 1089 for (int[] tuple : mStartCountTuples) { 1090 if (tuple[1] < 0) { 1091 delete(tuple); 1092 } else { 1093 add(tuple); 1094 } 1095 } 1096 } 1097 1098 private void add(int[] tuple) { 1099 // offset others 1100 offsetOriginalIndices(tuple[0], tuple[1]); 1101 addItems(tuple[0], tuple[1], mNewItemPrefix); 1102 notifyItemRangeInserted(tuple[0], tuple[1]); 1103 } 1104 1105 private void delete(int[] tuple) { 1106 final int count = -tuple[1]; 1107 offsetOriginalIndices(tuple[0] + count, tuple[1]); 1108 for (int i = 0; i < count; i++) { 1109 mItems.remove(tuple[0]); 1110 } 1111 notifyItemRangeRemoved(tuple[0], count); 1112 } 1113 } 1114 } 1115 1116 public boolean isMainThread() { 1117 return Looper.myLooper() == Looper.getMainLooper(); 1118 } 1119 1120 public void runTestOnUiThread(final Runnable r) throws Throwable { 1121 if (Looper.myLooper() == Looper.getMainLooper()) { 1122 r.run(); 1123 } else { 1124 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 1125 @Override 1126 public void run() { 1127 try { 1128 r.run(); 1129 } catch (Throwable t) { 1130 postExceptionToInstrumentation(t); 1131 } 1132 } 1133 }); 1134 checkForMainThreadException(); 1135 } 1136 } 1137 1138 static class TargetTuple { 1139 1140 final int mPosition; 1141 1142 final int mLayoutDirection; 1143 1144 TargetTuple(int position, int layoutDirection) { 1145 this.mPosition = position; 1146 this.mLayoutDirection = layoutDirection; 1147 } 1148 1149 @Override 1150 public String toString() { 1151 return "TargetTuple{" + 1152 "mPosition=" + mPosition + 1153 ", mLayoutDirection=" + mLayoutDirection + 1154 '}'; 1155 } 1156 } 1157 1158 public interface AttachDetachCountingAdapter { 1159 1160 ViewAttachDetachCounter getCounter(); 1161 } 1162 1163 public class ViewAttachDetachCounter { 1164 1165 Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>(); 1166 1167 public void validateRemaining(RecyclerView recyclerView) { 1168 final int childCount = recyclerView.getChildCount(); 1169 for (int i = 0; i < childCount; i++) { 1170 View view = recyclerView.getChildAt(i); 1171 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); 1172 assertTrue("remaining view should be in attached set " + vh, 1173 mAttachedSet.contains(vh)); 1174 } 1175 assertEquals("there should not be any views left in attached set", 1176 childCount, mAttachedSet.size()); 1177 } 1178 1179 public void onViewDetached(RecyclerView.ViewHolder viewHolder) { 1180 try { 1181 assertTrue("view holder should be in attached set", 1182 mAttachedSet.remove(viewHolder)); 1183 } catch (Throwable t) { 1184 postExceptionToInstrumentation(t); 1185 } 1186 } 1187 1188 public void onViewAttached(RecyclerView.ViewHolder viewHolder) { 1189 try { 1190 assertTrue("view holder should not be in attached set", 1191 mAttachedSet.add(viewHolder)); 1192 } catch (Throwable t) { 1193 postExceptionToInstrumentation(t); 1194 } 1195 } 1196 1197 public void onAttached(RecyclerView recyclerView) { 1198 // when a new RV is attached, clear the set and add all view holders 1199 mAttachedSet.clear(); 1200 final int childCount = recyclerView.getChildCount(); 1201 for (int i = 0; i < childCount; i ++) { 1202 View view = recyclerView.getChildAt(i); 1203 mAttachedSet.add(recyclerView.getChildViewHolder(view)); 1204 } 1205 } 1206 1207 public void onDetached(RecyclerView recyclerView) { 1208 validateRemaining(recyclerView); 1209 } 1210 } 1211} 1212