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