• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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