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