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