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 package androidx.recyclerview.widget;
17 
18 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
19 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
20 
21 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
22 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
23 
24 import static org.junit.Assert.assertEquals;
25 import static org.junit.Assert.assertFalse;
26 import static org.junit.Assert.assertNotNull;
27 
28 import static java.util.concurrent.TimeUnit.SECONDS;
29 
30 import android.content.Context;
31 import android.graphics.Rect;
32 import android.util.Log;
33 import android.view.View;
34 import android.view.ViewGroup;
35 
36 import org.hamcrest.CoreMatchers;
37 import org.hamcrest.MatcherAssert;
38 import org.jspecify.annotations.NonNull;
39 import org.jspecify.annotations.Nullable;
40 
41 import java.lang.reflect.Field;
42 import java.util.ArrayList;
43 import java.util.LinkedHashMap;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.concurrent.CountDownLatch;
47 import java.util.concurrent.TimeUnit;
48 
49 public class BaseLinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
50 
51     protected static final boolean DEBUG = false;
52     protected static final String TAG = "LinearLayoutManagerTest";
53 
createBaseVariations()54     protected static List<Config> createBaseVariations() {
55         List<Config> variations = new ArrayList<>();
56         for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
57             for (boolean reverseLayout : new boolean[]{false, true}) {
58                 for (boolean stackFromBottom : new boolean[]{false, true}) {
59                     for (boolean wrap : new boolean[]{false, true}) {
60                         variations.add(
61                                 new Config(orientation, reverseLayout, stackFromBottom).wrap(wrap));
62                     }
63 
64                 }
65             }
66         }
67         return variations;
68     }
69 
70     WrappedLinearLayoutManager mLayoutManager;
71     TestAdapter mTestAdapter;
72 
addConfigVariation(List<Config> base, String fieldName, Object... variations)73     protected static List<Config> addConfigVariation(List<Config> base, String fieldName,
74             Object... variations)
75             throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
76         List<Config> newConfigs = new ArrayList<Config>();
77         Field field = Config.class.getDeclaredField(fieldName);
78         for (Config config : base) {
79             for (Object variation : variations) {
80                 Config newConfig = (Config) config.clone();
81                 field.set(newConfig, variation);
82                 newConfigs.add(newConfig);
83             }
84         }
85         return newConfigs;
86     }
87 
setupByConfig(Config config, boolean waitForFirstLayout)88     void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable {
89         setupByConfig(config, waitForFirstLayout, null, null);
90     }
91 
setupByConfig(Config config, boolean waitForFirstLayout, RecyclerView.@Nullable LayoutParams childLayoutParams, RecyclerView.@Nullable LayoutParams parentLayoutParams)92     void setupByConfig(Config config, boolean waitForFirstLayout,
93             RecyclerView.@Nullable LayoutParams childLayoutParams,
94             RecyclerView.@Nullable LayoutParams parentLayoutParams) throws Throwable {
95         mRecyclerView = inflateWrappedRV();
96 
97         mRecyclerView.setHasFixedSize(true);
98         mTestAdapter = config.mTestAdapter == null
99                 ? new TestAdapter(config.mItemCount, childLayoutParams)
100                 : config.mTestAdapter;
101         mRecyclerView.setAdapter(mTestAdapter);
102         mLayoutManager = config.mTestLayoutManager == null
103                 ? new WrappedLinearLayoutManager(
104                         getActivity(), config.mOrientation, config.mReverseLayout)
105                 : config.mTestLayoutManager;
106         mLayoutManager.setStackFromEnd(config.mStackFromEnd);
107         mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
108         mRecyclerView.setLayoutManager(mLayoutManager);
109         if (config.mWrap) {
110             mRecyclerView.setLayoutParams(
111                     new ViewGroup.LayoutParams(
112                             config.mOrientation == HORIZONTAL ? WRAP_CONTENT : MATCH_PARENT,
113                             config.mOrientation == VERTICAL ? WRAP_CONTENT : MATCH_PARENT
114                     )
115             );
116         }
117         if (parentLayoutParams != null) {
118             mRecyclerView.setLayoutParams(parentLayoutParams);
119         }
120 
121         if (waitForFirstLayout) {
122             waitForFirstLayout();
123         }
124     }
125 
scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)126     public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
127             throws Throwable {
128         setupByConfig(new Config(VERTICAL, false, false), true);
129 
130         mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
131             @Override
132             void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
133                 if (state.isPreLayout()) {
134                     assertEquals("pending scroll position should still be pending",
135                             scrollPosition, mLayoutManager.mPendingScrollPosition);
136                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
137                         assertEquals("pending scroll position offset should still be pending",
138                                 scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
139                     }
140                 } else {
141                     RecyclerView.ViewHolder vh =
142                             mRecyclerView.findViewHolderForLayoutPosition(scrollPosition);
143                     assertNotNull("scroll to position should work", vh);
144                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
145                         assertEquals("scroll offset should be applied properly",
146                                 mLayoutManager.getPaddingTop() + scrollOffset +
147                                         ((RecyclerView.LayoutParams) vh.itemView
148                                                 .getLayoutParams()).topMargin,
149                                 mLayoutManager.getDecoratedTop(vh.itemView));
150                     }
151                 }
152             }
153         };
154         mLayoutManager.expectLayouts(2);
155         mActivityRule.runOnUiThread(new Runnable() {
156             @Override
157             public void run() {
158                 try {
159                     mTestAdapter.addAndNotify(0, 1);
160                     if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
161                         mLayoutManager.scrollToPosition(scrollPosition);
162                     } else {
163                         mLayoutManager.scrollToPositionWithOffset(scrollPosition,
164                                 scrollOffset);
165                     }
166 
167                 } catch (Throwable throwable) {
168                     throwable.printStackTrace();
169                 }
170 
171             }
172         });
173         mLayoutManager.waitForLayout(2);
174         checkForMainThreadException();
175     }
176 
waitForFirstLayout()177     protected void waitForFirstLayout() throws Throwable {
178         mLayoutManager.expectLayouts(1);
179         setRecyclerView(mRecyclerView);
180         mLayoutManager.waitForLayout(2);
181     }
182 
scrollToPositionWithOffset(final int position, final int offset)183     void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
184         mActivityRule.runOnUiThread(new Runnable() {
185             @Override
186             public void run() {
187                 mLayoutManager.scrollToPositionWithOffset(position, offset);
188             }
189         });
190     }
191 
assertRectSetsNotEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictItemEquality)192     public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
193             Map<Item, Rect> after, boolean strictItemEquality) {
194         final LayoutEquality layoutEquality = new LayoutEquality(
195                 message, before, after, strictItemEquality
196         );
197         Inequality inequality = layoutEquality.findInequality();
198         if (inequality == null) {
199             throw new AssertionError(
200                     layoutEquality.buildErrorLog("two layout should be different")
201             );
202         }
203     }
204 
assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after)205     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
206         assertRectSetsEqual(message, before, after, true);
207     }
208 
assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictItemEquality)209     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after,
210             boolean strictItemEquality) {
211         final LayoutEquality layoutEquality = new LayoutEquality(
212                 message, before, after, strictItemEquality
213         );
214         Inequality inequality = layoutEquality.findInequality();
215         if (inequality != null) {
216             inequality.throwAsError();
217         }
218     }
219 
220     private static class LayoutEquality {
221         private final String mMessagePrefix;
222         private final Map<Item, Rect> mBefore;
223         private final Map<Item, Rect> mAfter;
224         private boolean mStrictEquality;
225         private @Nullable String mPositionsLog;
226 
LayoutEquality(String messagePrefix, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictEquality)227         private LayoutEquality(String messagePrefix,
228                 Map<Item, Rect> before,
229                 Map<Item, Rect> after, boolean strictEquality) {
230             mMessagePrefix = messagePrefix;
231             mBefore = before;
232             mAfter = after;
233             mStrictEquality = strictEquality;
234         }
235 
findInequality()236         public @Nullable Inequality findInequality() {
237             if (mBefore.size() != mAfter.size()) {
238                 return new Inequality(this,
239                         "item counts should be equal " + mBefore.size() + " vs " + mAfter.size());
240             }
241             for (Map.Entry<Item, Rect> entry : mBefore.entrySet()) {
242                 final Item beforeItem = entry.getKey();
243                 Rect afterRect = null;
244                 if (mStrictEquality) {
245                     afterRect = mAfter.get(beforeItem);
246                     if (afterRect == null) {
247                         return new Inequality(this,
248                                 "Same item should be visible after"
249                                 + " simple re-layout, rectangle for " + beforeItem + " is "
250                                 + "missing");
251                     }
252                 } else {
253                     for (Map.Entry<Item, Rect> afterEntry : mAfter.entrySet()) {
254                         final Item afterItem = afterEntry.getKey();
255                         if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) {
256                             afterRect = afterEntry.getValue();
257                             break;
258                         }
259                     }
260                     if (afterRect == null) {
261                         return new Inequality(this,
262                                 "Item with same adapter index should be visible after "
263                                         + "simple re-layout," + beforeItem + " is missing");
264                     }
265                 }
266                 if (!entry.getValue().equals(afterRect)) {
267                     return new Inequality(this,
268                             "Item should be laid out at the same coordinates. Before:"
269                                     + entry.getValue() + ", after:" + afterRect);
270                 }
271             }
272             return null;
273         }
getPositionsLog()274         private @NonNull String getPositionsLog() {
275             if (mPositionsLog == null) {
276                 mPositionsLog = buildPositionLog();
277             }
278             return mPositionsLog;
279         }
280 
buildPositionLog()281         private String buildPositionLog() {
282             StringBuilder sb = new StringBuilder();
283             sb.append("checking rectangle equality.\n");
284             sb.append("before:\n");
285             for (Map.Entry<Item, Rect> entry : mBefore.entrySet()) {
286                 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
287             }
288             sb.append("after:\n");
289             for (Map.Entry<Item, Rect> entry : mAfter.entrySet()) {
290                 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
291             }
292             return sb.toString();
293         }
294 
buildErrorLog(String error)295         private String buildErrorLog(String error) {
296             return  error + "\n" + mMessagePrefix + "\n" + getPositionsLog();
297         }
298     }
299 
300     private static class Inequality {
301         private final String mMessage;
302         private final LayoutEquality mLayoutEquality;
303 
Inequality(LayoutEquality layoutEquality, String message)304         private Inequality(LayoutEquality layoutEquality,
305                 String message) {
306             mMessage = message;
307             mLayoutEquality = layoutEquality;
308         }
309 
throwAsError()310         void throwAsError() {
311             throw new AssertionError(mLayoutEquality.buildErrorLog(mMessage));
312         }
313     }
314 
315     static class VisibleChildren {
316 
317         int firstVisiblePosition = RecyclerView.NO_POSITION;
318 
319         int firstFullyVisiblePosition = RecyclerView.NO_POSITION;
320 
321         int lastVisiblePosition = RecyclerView.NO_POSITION;
322 
323         int lastFullyVisiblePosition = RecyclerView.NO_POSITION;
324 
325         @Override
toString()326         public String toString() {
327             return "VisibleChildren{" +
328                     "firstVisiblePosition=" + firstVisiblePosition +
329                     ", firstFullyVisiblePosition=" + firstFullyVisiblePosition +
330                     ", lastVisiblePosition=" + lastVisiblePosition +
331                     ", lastFullyVisiblePosition=" + lastFullyVisiblePosition +
332                     '}';
333         }
334     }
335 
336     static class OnLayoutListener {
337 
before(RecyclerView.Recycler recycler, RecyclerView.State state)338         void before(RecyclerView.Recycler recycler, RecyclerView.State state) {
339         }
340 
after(RecyclerView.Recycler recycler, RecyclerView.State state)341         void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
342         }
343     }
344 
345     public static class Config implements Cloneable {
346 
347         static final int DEFAULT_ITEM_COUNT = 250;
348 
349         boolean mStackFromEnd;
350 
351         int mOrientation = VERTICAL;
352 
353         boolean mReverseLayout = false;
354 
355         boolean mRecycleChildrenOnDetach = false;
356 
357         int mItemCount = DEFAULT_ITEM_COUNT;
358 
359         boolean mWrap = false;
360 
361         TestAdapter mTestAdapter;
362 
363         WrappedLinearLayoutManager mTestLayoutManager;
364 
Config(int orientation, boolean reverseLayout, boolean stackFromEnd)365         Config(int orientation, boolean reverseLayout, boolean stackFromEnd) {
366             mOrientation = orientation;
367             mReverseLayout = reverseLayout;
368             mStackFromEnd = stackFromEnd;
369         }
370 
Config()371         public Config() {
372 
373         }
374 
adapter(TestAdapter adapter)375         Config adapter(TestAdapter adapter) {
376             mTestAdapter = adapter;
377             return this;
378         }
379 
recycleChildrenOnDetach(boolean recycleChildrenOnDetach)380         Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
381             mRecycleChildrenOnDetach = recycleChildrenOnDetach;
382             return this;
383         }
384 
orientation(int orientation)385         Config orientation(int orientation) {
386             mOrientation = orientation;
387             return this;
388         }
389 
stackFromBottom(boolean stackFromBottom)390         Config stackFromBottom(boolean stackFromBottom) {
391             mStackFromEnd = stackFromBottom;
392             return this;
393         }
394 
reverseLayout(boolean reverseLayout)395         Config reverseLayout(boolean reverseLayout) {
396             mReverseLayout = reverseLayout;
397             return this;
398         }
399 
itemCount(int itemCount)400         public Config itemCount(int itemCount) {
401             mItemCount = itemCount;
402             return this;
403         }
404 
405         // required by convention
406         @Override
clone()407         public Object clone() throws CloneNotSupportedException {
408             return super.clone();
409         }
410 
411         @Override
toString()412         public String toString() {
413             return "Config{"
414                     + "mStackFromEnd=" + mStackFromEnd
415                     + ",mOrientation=" + mOrientation
416                     + ",mReverseLayout=" + mReverseLayout
417                     + ",mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach
418                     + ",mItemCount=" + mItemCount
419                     + ",wrap=" + mWrap
420                     + '}';
421         }
422 
wrap(boolean wrap)423         public Config wrap(boolean wrap) {
424             mWrap = wrap;
425             return this;
426         }
427     }
428 
429     class WrappedLinearLayoutManager extends LinearLayoutManager {
430 
431         CountDownLatch layoutLatch;
432         CountDownLatch snapLatch;
433         CountDownLatch prefetchLatch;
434         CountDownLatch callbackLatch;
435 
436         OrientationHelper mSecondaryOrientation;
437 
438         OnLayoutListener mOnLayoutListener;
439 
440         RecyclerView.OnScrollListener mSnapListener = new RecyclerView.OnScrollListener() {
441             @Override
442             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
443                 super.onScrollStateChanged(recyclerView, newState);
444                 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
445                     snapLatch.countDown();
446                     if (snapLatch.getCount() == 0L) {
447                         removeOnScrollListener(this);
448                     }
449                 }
450             }
451         };
452 
453         RecyclerView.OnScrollListener mCallbackListener = new RecyclerView.OnScrollListener() {
454             @Override
455             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
456                 super.onScrollStateChanged(recyclerView, newState);
457                 callbackLatch.countDown();
458                 if (callbackLatch.getCount() == 0L) {
459                     removeOnScrollListener(this);
460                 }
461             }
462         };
463 
WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout)464         public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
465             super(context, orientation, reverseLayout);
466         }
467 
expectLayouts(int count)468         public void expectLayouts(int count) {
469             layoutLatch = new CountDownLatch(count);
470         }
471 
expectCallbacks(int count)472         public void expectCallbacks(int count) throws Throwable {
473             callbackLatch = new CountDownLatch(count);
474             mRecyclerView.addOnScrollListener(mCallbackListener);
475         }
476 
removeOnScrollListener(RecyclerView.OnScrollListener listener)477         private void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
478             mRecyclerView.removeOnScrollListener(listener);
479         }
480 
waitForLayout(int seconds)481         public void waitForLayout(int seconds) throws Throwable {
482             layoutLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
483             checkForMainThreadException();
484             MatcherAssert.assertThat("all layouts should complete on time",
485                     layoutLatch.getCount(), CoreMatchers.is(0L));
486             // use a runnable to ensure RV layout is finished
487             getInstrumentation().runOnMainSync(new Runnable() {
488                 @Override
489                 public void run() {
490                 }
491             });
492         }
493 
assertNoCallbacks(String msg, long timeout)494         public void assertNoCallbacks(String msg, long timeout) throws Throwable {
495             callbackLatch.await(timeout, TimeUnit.SECONDS);
496             long latchCount = callbackLatch.getCount();
497             assertFalse(msg + " :" + latchCount, latchCount == 0);
498             removeOnScrollListener(mCallbackListener);
499         }
500 
expectPrefetch(int count)501         public void expectPrefetch(int count) {
502             prefetchLatch = new CountDownLatch(count);
503         }
504 
waitForPrefetch(int seconds)505         public void waitForPrefetch(int seconds) throws Throwable {
506             prefetchLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
507             checkForMainThreadException();
508             MatcherAssert.assertThat("all prefetches should complete on time",
509                     prefetchLatch.getCount(), CoreMatchers.is(0L));
510             // use a runnable to ensure RV layout is finished
511             getInstrumentation().runOnMainSync(new Runnable() {
512                 @Override
513                 public void run() {
514                 }
515             });
516         }
517 
expectIdleState(int count)518         public void expectIdleState(int count) {
519             // Remove listener if it was still there
520             mRecyclerView.removeOnScrollListener(mSnapListener);
521             snapLatch = new CountDownLatch(count);
522             mRecyclerView.addOnScrollListener(mSnapListener);
523         }
524 
waitForSnap(int seconds)525         public void waitForSnap(int seconds) throws Throwable {
526             snapLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
527             checkForMainThreadException();
528             MatcherAssert.assertThat("all scrolling should complete on time",
529                     snapLatch.getCount(), CoreMatchers.is(0L));
530             // use a runnable to ensure RV layout is finished
531             getInstrumentation().runOnMainSync(new Runnable() {
532                 @Override
533                 public void run() {}
534             });
535         }
536 
537         @Override
setOrientation(int orientation)538         public void setOrientation(int orientation) {
539             super.setOrientation(orientation);
540             mSecondaryOrientation = null;
541         }
542 
543         @Override
removeAndRecycleView(View child, RecyclerView.Recycler recycler)544         public void removeAndRecycleView(View child, RecyclerView.Recycler recycler) {
545             if (DEBUG) {
546                 Log.d(TAG, "recycling view " + mRecyclerView.getChildViewHolder(child));
547             }
548             super.removeAndRecycleView(child, recycler);
549         }
550 
551         @Override
removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler)552         public void removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler) {
553             if (DEBUG) {
554                 Log.d(TAG,
555                         "recycling view at" + mRecyclerView.getChildViewHolder(getChildAt(index)));
556             }
557             super.removeAndRecycleViewAt(index, recycler);
558         }
559 
560         @Override
ensureLayoutState()561         void ensureLayoutState() {
562             super.ensureLayoutState();
563             if (mSecondaryOrientation == null) {
564                 mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
565                         1 - getOrientation());
566             }
567         }
568 
569         @Override
createLayoutState()570         LayoutState createLayoutState() {
571             return new LayoutState() {
572                 @Override
573                 View next(RecyclerView.Recycler recycler) {
574                     final boolean hadMore = hasMore(mRecyclerView.mState);
575                     final int position = mCurrentPosition;
576                     View next = super.next(recycler);
577                     assertEquals("if has more, should return a view", hadMore, next != null);
578                     assertEquals("position of the returned view must match current position",
579                             position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition());
580                     return next;
581                 }
582             };
583         }
584 
getBoundsLog()585         public String getBoundsLog() {
586             StringBuilder sb = new StringBuilder();
587             sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding())
588                     .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding());
589             sb.append("\nchildren bounds\n");
590             final int childCount = getChildCount();
591             for (int i = 0; i < childCount; i++) {
592                 View child = getChildAt(i);
593                 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
594                         .append("[").append("start:").append(
595                         mOrientationHelper.getDecoratedStart(child)).append(", end:")
596                         .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n");
597             }
598             return sb.toString();
599         }
600 
waitForAnimationsToEnd(int timeoutInSeconds)601         public void waitForAnimationsToEnd(int timeoutInSeconds) throws InterruptedException {
602             RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
603             if (itemAnimator == null) {
604                 return;
605             }
606             final CountDownLatch latch = new CountDownLatch(1);
607             final boolean running = itemAnimator.isRunning(
608                     new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
609                         @Override
610                         public void onAnimationsFinished() {
611                             latch.countDown();
612                         }
613                     }
614             );
615             if (running) {
616                 latch.await(timeoutInSeconds, TimeUnit.SECONDS);
617             }
618         }
619 
traverseAndFindVisibleChildren()620         public VisibleChildren traverseAndFindVisibleChildren() {
621             int childCount = getChildCount();
622             final VisibleChildren visibleChildren = new VisibleChildren();
623             final int start = mOrientationHelper.getStartAfterPadding();
624             final int end = mOrientationHelper.getEndAfterPadding();
625             for (int i = 0; i < childCount; i++) {
626                 View child = getChildAt(i);
627                 final int childStart = mOrientationHelper.getDecoratedStart(child);
628                 final int childEnd = mOrientationHelper.getDecoratedEnd(child);
629                 final boolean fullyVisible = childStart >= start && childEnd <= end;
630                 final boolean hidden = childEnd <= start || childStart >= end;
631                 if (hidden) {
632                     continue;
633                 }
634                 final int position = getPosition(child);
635                 if (fullyVisible) {
636                     if (position < visibleChildren.firstFullyVisiblePosition ||
637                             visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) {
638                         visibleChildren.firstFullyVisiblePosition = position;
639                     }
640 
641                     if (position > visibleChildren.lastFullyVisiblePosition) {
642                         visibleChildren.lastFullyVisiblePosition = position;
643                     }
644                 }
645 
646                 if (position < visibleChildren.firstVisiblePosition ||
647                         visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) {
648                     visibleChildren.firstVisiblePosition = position;
649                 }
650 
651                 if (position > visibleChildren.lastVisiblePosition) {
652                     visibleChildren.lastVisiblePosition = position;
653                 }
654 
655             }
656             return visibleChildren;
657         }
658 
getViewBounds(View view)659         Rect getViewBounds(View view) {
660             if (getOrientation() == HORIZONTAL) {
661                 return new Rect(
662                         mOrientationHelper.getDecoratedStart(view),
663                         mSecondaryOrientation.getDecoratedStart(view),
664                         mOrientationHelper.getDecoratedEnd(view),
665                         mSecondaryOrientation.getDecoratedEnd(view));
666             } else {
667                 return new Rect(
668                         mSecondaryOrientation.getDecoratedStart(view),
669                         mOrientationHelper.getDecoratedStart(view),
670                         mSecondaryOrientation.getDecoratedEnd(view),
671                         mOrientationHelper.getDecoratedEnd(view));
672             }
673 
674         }
675 
collectChildCoordinates()676         Map<Item, Rect> collectChildCoordinates() throws Throwable {
677             final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
678             mActivityRule.runOnUiThread(new Runnable() {
679                 @Override
680                 public void run() {
681                     final int childCount = getChildCount();
682                     Rect layoutBounds = new Rect(0, 0,
683                             mLayoutManager.getWidth(), mLayoutManager.getHeight());
684                     for (int i = 0; i < childCount; i++) {
685                         View child = getChildAt(i);
686                         RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child
687                                 .getLayoutParams();
688                         TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
689                         Rect childBounds = getViewBounds(child);
690                         if (new Rect(childBounds).intersect(layoutBounds)) {
691                             items.put(vh.mBoundItem, childBounds);
692                         }
693                     }
694                 }
695             });
696             return items;
697         }
698 
699         @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)700         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
701             try {
702                 if (mOnLayoutListener != null) {
703                     mOnLayoutListener.before(recycler, state);
704                 }
705                 super.onLayoutChildren(recycler, state);
706                 if (mOnLayoutListener != null) {
707                     mOnLayoutListener.after(recycler, state);
708                 }
709             } catch (Throwable t) {
710                 postExceptionToInstrumentation(t);
711             }
712             layoutLatch.countDown();
713         }
714 
715         @Override
collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, LayoutPrefetchRegistry layoutPrefetchRegistry)716         public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
717                 LayoutPrefetchRegistry layoutPrefetchRegistry) {
718             if (prefetchLatch != null) prefetchLatch.countDown();
719             super.collectAdjacentPrefetchPositions(dx, dy, state, layoutPrefetchRegistry);
720         }
721     }
722 }
723