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