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