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