• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package android.support.v7.widget;
2 
3 import android.graphics.Rect;
4 import android.support.annotation.Nullable;
5 import android.util.Log;
6 import android.view.View;
7 import android.view.ViewGroup;
8 
9 import java.lang.reflect.Field;
10 import java.util.ArrayList;
11 import java.util.Arrays;
12 import java.util.HashSet;
13 import java.util.LinkedHashMap;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.concurrent.CountDownLatch;
17 import java.util.concurrent.TimeUnit;
18 import java.util.concurrent.atomic.AtomicInteger;
19 
20 import static android.support.v7.widget.LayoutState.LAYOUT_END;
21 import static android.support.v7.widget.LayoutState.LAYOUT_START;
22 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
23 import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
24 import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE;
25 import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL;
26 import static org.junit.Assert.assertEquals;
27 import static org.junit.Assert.assertFalse;
28 import static org.junit.Assert.assertNotNull;
29 import static org.junit.Assert.assertTrue;
30 
31 import static java.util.concurrent.TimeUnit.SECONDS;
32 
33 import org.hamcrest.CoreMatchers;
34 import org.hamcrest.MatcherAssert;
35 
36 public class BaseStaggeredGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
37 
38     protected static final boolean DEBUG = false;
39     protected static final int AVG_ITEM_PER_VIEW = 3;
40     protected static final String TAG = "StaggeredGridLayoutManagerTest";
41     volatile WrappedLayoutManager mLayoutManager;
42     GridTestAdapter mAdapter;
43 
createBaseVariations()44     protected static List<Config> createBaseVariations() {
45         List<Config> variations = new ArrayList<>();
46         for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
47             for (boolean reverseLayout : new boolean[]{false, true}) {
48                 for (int spanCount : new int[]{1, 3}) {
49                     for (int gapStrategy : new int[]{GAP_HANDLING_NONE,
50                             GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}) {
51                         for (boolean wrap : new boolean[]{true, false}) {
52                             variations.add(new Config(orientation, reverseLayout, spanCount,
53                                     gapStrategy).wrap(wrap));
54                         }
55 
56                     }
57                 }
58             }
59         }
60         return variations;
61     }
62 
addConfigVariation(List<Config> base, String fieldName, Object... variations)63     protected static List<Config> addConfigVariation(List<Config> base, String fieldName,
64             Object... variations)
65             throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
66         List<Config> newConfigs = new ArrayList<Config>();
67         Field field = Config.class.getDeclaredField(fieldName);
68         for (Config config : base) {
69             for (Object variation : variations) {
70                 Config newConfig = (Config) config.clone();
71                 field.set(newConfig, variation);
72                 newConfigs.add(newConfig);
73             }
74         }
75         return newConfigs;
76     }
77 
setupByConfig(Config config)78     void setupByConfig(Config config) throws Throwable {
79         setupByConfig(config, new GridTestAdapter(config.mItemCount, config.mOrientation));
80     }
81 
setupByConfig(Config config, GridTestAdapter adapter)82     void setupByConfig(Config config, GridTestAdapter adapter) throws Throwable {
83         mAdapter = adapter;
84         mRecyclerView = new RecyclerView(getActivity());
85         mRecyclerView.setAdapter(mAdapter);
86         mRecyclerView.setHasFixedSize(true);
87         mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
88                 config.mOrientation);
89         mLayoutManager.setGapStrategy(config.mGapStrategy);
90         mLayoutManager.setReverseLayout(config.mReverseLayout);
91         mRecyclerView.setLayoutManager(mLayoutManager);
92         mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
93             @Override
94             public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
95                     RecyclerView.State state) {
96                 try {
97                     StaggeredGridLayoutManager.LayoutParams
98                             lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
99                     assertNotNull("view should have layout params assigned", lp);
100                     assertNotNull("when item offsets are requested, view should have a valid span",
101                             lp.mSpan);
102                 } catch (Throwable t) {
103                     postExceptionToInstrumentation(t);
104                 }
105             }
106         });
107     }
108 
getLp(View view)109     StaggeredGridLayoutManager.LayoutParams getLp(View view) {
110         return (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
111     }
112 
waitFirstLayout()113     void waitFirstLayout() throws Throwable {
114         mLayoutManager.expectLayouts(1);
115         setRecyclerView(mRecyclerView);
116         mLayoutManager.waitForLayout(3);
117         getInstrumentation().waitForIdleSync();
118     }
119 
120     /**
121      * enqueues an empty runnable to main thread so that we can be assured it did run
122      *
123      * @param count Number of times to run
124      */
waitForMainThread(int count)125     protected void waitForMainThread(int count) throws Throwable {
126         final AtomicInteger i = new AtomicInteger(count);
127         while (i.get() > 0) {
128             runTestOnUiThread(new Runnable() {
129                 @Override
130                 public void run() {
131                     i.decrementAndGet();
132                 }
133             });
134         }
135     }
136 
assertRectSetsNotEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after)137     public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
138             Map<Item, Rect> after) {
139         Throwable throwable = null;
140         try {
141             assertRectSetsEqual("NOT " + message, before, after);
142         } catch (Throwable t) {
143             throwable = t;
144         }
145         assertNotNull(message + " two layout should be different", throwable);
146     }
147 
assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after)148     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
149         assertRectSetsEqual(message, before, after, true);
150     }
151 
assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictItemEquality)152     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after,
153             boolean strictItemEquality) {
154         StringBuilder log = new StringBuilder();
155         if (DEBUG) {
156             log.append("checking rectangle equality.\n");
157             log.append("total space:" + mLayoutManager.mPrimaryOrientation.getTotalSpace());
158             log.append("before:");
159             for (Map.Entry<Item, Rect> entry : before.entrySet()) {
160                 log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
161                         .append(entry.getValue());
162             }
163             log.append("\nafter:");
164             for (Map.Entry<Item, Rect> entry : after.entrySet()) {
165                 log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
166                         .append(entry.getValue());
167             }
168             message += "\n\n" + log.toString();
169         }
170         assertEquals(message + ": item counts should be equal", before.size()
171                 , after.size());
172         for (Map.Entry<Item, Rect> entry : before.entrySet()) {
173             final Item beforeItem = entry.getKey();
174             Rect afterRect = null;
175             if (strictItemEquality) {
176                 afterRect = after.get(beforeItem);
177                 assertNotNull(message + ": Same item should be visible after simple re-layout",
178                         afterRect);
179             } else {
180                 for (Map.Entry<Item, Rect> afterEntry : after.entrySet()) {
181                     final Item afterItem = afterEntry.getKey();
182                     if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) {
183                         afterRect = afterEntry.getValue();
184                         break;
185                     }
186                 }
187                 assertNotNull(message + ": Item with same adapter index should be visible " +
188                                 "after simple re-layout",
189                         afterRect);
190             }
191             assertEquals(message + ": Item should be laid out at the same coordinates",
192                     entry.getValue(),
193                     afterRect);
194         }
195     }
196 
assertViewPositions(Config config)197     protected void assertViewPositions(Config config) {
198         ArrayList<ArrayList<View>> viewsBySpan = mLayoutManager.collectChildrenBySpan();
199         OrientationHelper orientationHelper = OrientationHelper
200                 .createOrientationHelper(mLayoutManager, config.mOrientation);
201         for (ArrayList<View> span : viewsBySpan) {
202             // validate all children's order. first child should have min start mPosition
203             final int count = span.size();
204             for (int i = 0, j = 1; j < count; i++, j++) {
205                 View prev = span.get(i);
206                 View next = span.get(j);
207                 assertTrue(config + " prev item should be above next item",
208                         orientationHelper.getDecoratedEnd(prev) <= orientationHelper
209                                 .getDecoratedStart(next)
210                 );
211 
212             }
213         }
214     }
215 
findInvisibleTarget(Config config)216     protected TargetTuple findInvisibleTarget(Config config) {
217         int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE;
218         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
219             View child = mLayoutManager.getChildAt(i);
220             int position = mRecyclerView.getChildLayoutPosition(child);
221             if (position < minPosition) {
222                 minPosition = position;
223             }
224             if (position > maxPosition) {
225                 maxPosition = position;
226             }
227         }
228         final int tailTarget = maxPosition + (mAdapter.getItemCount() - maxPosition) / 2;
229         final int headTarget = minPosition / 2;
230         final int target;
231         // where will the child come from ?
232         final int itemLayoutDirection;
233         if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) {
234             target = tailTarget;
235             itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END;
236         } else {
237             target = headTarget;
238             itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START;
239         }
240         if (DEBUG) {
241             Log.d(TAG,
242                     config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition);
243         }
244         return new TargetTuple(target, itemLayoutDirection);
245     }
246 
scrollToPositionWithOffset(final int position, final int offset)247     protected void scrollToPositionWithOffset(final int position, final int offset)
248             throws Throwable {
249         runTestOnUiThread(new Runnable() {
250             @Override
251             public void run() {
252                 mLayoutManager.scrollToPositionWithOffset(position, offset);
253             }
254         });
255     }
256 
257     static class OnLayoutListener {
258 
before(RecyclerView.Recycler recycler, RecyclerView.State state)259         void before(RecyclerView.Recycler recycler, RecyclerView.State state) {
260         }
261 
after(RecyclerView.Recycler recycler, RecyclerView.State state)262         void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
263         }
264     }
265 
266     static class VisibleChildren {
267 
268         int[] firstVisiblePositions;
269 
270         int[] firstFullyVisiblePositions;
271 
272         int[] lastVisiblePositions;
273 
274         int[] lastFullyVisiblePositions;
275 
276         View findFirstPartialVisibleClosestToStart;
277         View findFirstPartialVisibleClosestToEnd;
278 
VisibleChildren(int spanCount)279         VisibleChildren(int spanCount) {
280             firstFullyVisiblePositions = new int[spanCount];
281             firstVisiblePositions = new int[spanCount];
282             lastVisiblePositions = new int[spanCount];
283             lastFullyVisiblePositions = new int[spanCount];
284             for (int i = 0; i < spanCount; i++) {
285                 firstFullyVisiblePositions[i] = RecyclerView.NO_POSITION;
286                 firstVisiblePositions[i] = RecyclerView.NO_POSITION;
287                 lastVisiblePositions[i] = RecyclerView.NO_POSITION;
288                 lastFullyVisiblePositions[i] = RecyclerView.NO_POSITION;
289             }
290         }
291 
292         @Override
equals(Object o)293         public boolean equals(Object o) {
294             if (this == o) {
295                 return true;
296             }
297             if (o == null || getClass() != o.getClass()) {
298                 return false;
299             }
300 
301             VisibleChildren that = (VisibleChildren) o;
302 
303             if (!Arrays.equals(firstFullyVisiblePositions, that.firstFullyVisiblePositions)) {
304                 return false;
305             }
306             if (findFirstPartialVisibleClosestToStart
307                     != null ? !findFirstPartialVisibleClosestToStart
308                     .equals(that.findFirstPartialVisibleClosestToStart)
309                     : that.findFirstPartialVisibleClosestToStart != null) {
310                 return false;
311             }
312             if (!Arrays.equals(firstVisiblePositions, that.firstVisiblePositions)) {
313                 return false;
314             }
315             if (!Arrays.equals(lastFullyVisiblePositions, that.lastFullyVisiblePositions)) {
316                 return false;
317             }
318             if (findFirstPartialVisibleClosestToEnd != null ? !findFirstPartialVisibleClosestToEnd
319                     .equals(that.findFirstPartialVisibleClosestToEnd)
320                     : that.findFirstPartialVisibleClosestToEnd
321                             != null) {
322                 return false;
323             }
324             if (!Arrays.equals(lastVisiblePositions, that.lastVisiblePositions)) {
325                 return false;
326             }
327 
328             return true;
329         }
330 
331         @Override
hashCode()332         public int hashCode() {
333             int result = Arrays.hashCode(firstVisiblePositions);
334             result = 31 * result + Arrays.hashCode(firstFullyVisiblePositions);
335             result = 31 * result + Arrays.hashCode(lastVisiblePositions);
336             result = 31 * result + Arrays.hashCode(lastFullyVisiblePositions);
337             result = 31 * result + (findFirstPartialVisibleClosestToStart != null
338                     ? findFirstPartialVisibleClosestToStart
339                     .hashCode() : 0);
340             result = 31 * result + (findFirstPartialVisibleClosestToEnd != null
341                     ? findFirstPartialVisibleClosestToEnd
342                     .hashCode()
343                     : 0);
344             return result;
345         }
346 
347         @Override
toString()348         public String toString() {
349             return "VisibleChildren{" +
350                     "firstVisiblePositions=" + Arrays.toString(firstVisiblePositions) +
351                     ", firstFullyVisiblePositions=" + Arrays.toString(firstFullyVisiblePositions) +
352                     ", lastVisiblePositions=" + Arrays.toString(lastVisiblePositions) +
353                     ", lastFullyVisiblePositions=" + Arrays.toString(lastFullyVisiblePositions) +
354                     ", findFirstPartialVisibleClosestToStart=" +
355                     viewToString(findFirstPartialVisibleClosestToStart) +
356                     ", findFirstPartialVisibleClosestToEnd=" +
357                     viewToString(findFirstPartialVisibleClosestToEnd) +
358                     '}';
359         }
360 
viewToString(View view)361         private String viewToString(View view) {
362             if (view == null) {
363                 return null;
364             }
365             ViewGroup.LayoutParams lp = view.getLayoutParams();
366             if (lp instanceof RecyclerView.LayoutParams == false) {
367                 return System.identityHashCode(view) + "(?)";
368             }
369             RecyclerView.LayoutParams rvlp = (RecyclerView.LayoutParams) lp;
370             return System.identityHashCode(view) + "(" + rvlp.getViewAdapterPosition() + ")";
371         }
372     }
373 
374     abstract static class OnBindCallback {
375 
onBoundItem(TestViewHolder vh, int position)376         abstract void onBoundItem(TestViewHolder vh, int position);
377 
assignRandomSize()378         boolean assignRandomSize() {
379             return true;
380         }
381 
onCreatedViewHolder(TestViewHolder vh)382         void onCreatedViewHolder(TestViewHolder vh) {
383         }
384     }
385 
386     static class Config implements Cloneable {
387 
388         static final int DEFAULT_ITEM_COUNT = 300;
389 
390         int mOrientation = OrientationHelper.VERTICAL;
391 
392         boolean mReverseLayout = false;
393 
394         int mSpanCount = 3;
395 
396         int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
397 
398         int mItemCount = DEFAULT_ITEM_COUNT;
399 
400         boolean mWrap = false;
401 
Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy)402         Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) {
403             mOrientation = orientation;
404             mReverseLayout = reverseLayout;
405             mSpanCount = spanCount;
406             mGapStrategy = gapStrategy;
407         }
408 
Config()409         public Config() {
410 
411         }
412 
orientation(int orientation)413         Config orientation(int orientation) {
414             mOrientation = orientation;
415             return this;
416         }
417 
reverseLayout(boolean reverseLayout)418         Config reverseLayout(boolean reverseLayout) {
419             mReverseLayout = reverseLayout;
420             return this;
421         }
422 
spanCount(int spanCount)423         Config spanCount(int spanCount) {
424             mSpanCount = spanCount;
425             return this;
426         }
427 
gapStrategy(int gapStrategy)428         Config gapStrategy(int gapStrategy) {
429             mGapStrategy = gapStrategy;
430             return this;
431         }
432 
itemCount(int itemCount)433         public Config itemCount(int itemCount) {
434             mItemCount = itemCount;
435             return this;
436         }
437 
wrap(boolean wrap)438         public Config wrap(boolean wrap) {
439             mWrap = wrap;
440             return this;
441         }
442 
443         @Override
toString()444         public String toString() {
445             return "[CONFIG:" +
446                     " span:" + mSpanCount + "," +
447                     " orientation:" + (mOrientation == HORIZONTAL ? "horz," : "vert,") +
448                     " reverse:" + (mReverseLayout ? "T" : "F") +
449                     " itemCount:" + mItemCount +
450                     " wrapContent:" + mWrap +
451                     " gap strategy: " + gapStrategyName(mGapStrategy);
452         }
453 
gapStrategyName(int gapStrategy)454         protected static String gapStrategyName(int gapStrategy) {
455             switch (gapStrategy) {
456                 case GAP_HANDLING_NONE:
457                     return "none";
458                 case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
459                     return "move spans";
460             }
461             return "gap strategy: unknown";
462         }
463 
464         @Override
clone()465         public Object clone() throws CloneNotSupportedException {
466             return super.clone();
467         }
468     }
469 
470     class WrappedLayoutManager extends StaggeredGridLayoutManager {
471 
472         CountDownLatch layoutLatch;
473         OnLayoutListener mOnLayoutListener;
474         // gradle does not yet let us customize manifest for tests which is necessary to test RTL.
475         // until bug is fixed, we'll fake it.
476         // public issue id: 57819
477         Boolean mFakeRTL;
478         CountDownLatch snapLatch;
479 
480         @Override
isLayoutRTL()481         boolean isLayoutRTL() {
482             return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
483         }
484 
expectLayouts(int count)485         public void expectLayouts(int count) {
486             layoutLatch = new CountDownLatch(count);
487         }
488 
waitForLayout(int seconds)489         public void waitForLayout(int seconds) throws Throwable {
490             layoutLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
491             checkForMainThreadException();
492             MatcherAssert.assertThat("all layouts should complete on time",
493                     layoutLatch.getCount(), CoreMatchers.is(0L));
494             // use a runnable to ensure RV layout is finished
495             getInstrumentation().runOnMainSync(new Runnable() {
496                 @Override
497                 public void run() {
498                 }
499             });
500         }
501 
expectIdleState(int count)502         public void expectIdleState(int count) {
503             snapLatch = new CountDownLatch(count);
504             mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
505                 @Override
506                 public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
507                     super.onScrollStateChanged(recyclerView, newState);
508                     if (newState == RecyclerView.SCROLL_STATE_IDLE) {
509                         snapLatch.countDown();
510                         if (snapLatch.getCount() == 0L) {
511                             mRecyclerView.removeOnScrollListener(this);
512                         }
513                     }
514                 }
515             });
516         }
517 
waitForSnap(int seconds)518         public void waitForSnap(int seconds) throws Throwable {
519             snapLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
520             checkForMainThreadException();
521             MatcherAssert.assertThat("all scrolling should complete on time",
522                     snapLatch.getCount(), CoreMatchers.is(0L));
523             // use a runnable to ensure RV layout is finished
524             getInstrumentation().runOnMainSync(new Runnable() {
525                 @Override
526                 public void run() {
527                 }
528             });
529         }
530 
assertNoLayout(String msg, long timeout)531         public void assertNoLayout(String msg, long timeout) throws Throwable {
532             layoutLatch.await(timeout, TimeUnit.SECONDS);
533             assertFalse(msg, layoutLatch.getCount() == 0);
534         }
535 
536         @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)537         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
538             String before;
539             if (DEBUG) {
540                 before = layoutToString("before");
541             } else {
542                 before = "enable DEBUG";
543             }
544             try {
545                 if (mOnLayoutListener != null) {
546                     mOnLayoutListener.before(recycler, state);
547                 }
548                 super.onLayoutChildren(recycler, state);
549                 if (mOnLayoutListener != null) {
550                     mOnLayoutListener.after(recycler, state);
551                 }
552                 validateChildren(before);
553             } catch (Throwable t) {
554                 postExceptionToInstrumentation(t);
555             }
556 
557             layoutLatch.countDown();
558         }
559 
560         @Override
scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state)561         int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
562             try {
563                 int result = super.scrollBy(dt, recycler, state);
564                 validateChildren();
565                 return result;
566             } catch (Throwable t) {
567                 postExceptionToInstrumentation(t);
568             }
569 
570             return 0;
571         }
572 
findFirstVisibleItemClosestToCenter()573         View findFirstVisibleItemClosestToCenter() {
574             final int boundsStart = mPrimaryOrientation.getStartAfterPadding();
575             final int boundsEnd = mPrimaryOrientation.getEndAfterPadding();
576             final int boundsCenter = (boundsStart + boundsEnd) / 2;
577             final Rect childBounds = new Rect();
578             int minDist = Integer.MAX_VALUE;
579             View closestChild = null;
580             for (int i = getChildCount() - 1; i >= 0; i--) {
581                 final View child = getChildAt(i);
582                 childBounds.setEmpty();
583                 getDecoratedBoundsWithMargins(child, childBounds);
584                 int childCenter = canScrollHorizontally()
585                         ? childBounds.centerX() : childBounds.centerY();
586                 int dist = Math.abs(boundsCenter - childCenter);
587                 if (dist < minDist) {
588                     minDist = dist;
589                     closestChild = child;
590                 }
591             }
592             return closestChild;
593         }
594 
WrappedLayoutManager(int spanCount, int orientation)595         public WrappedLayoutManager(int spanCount, int orientation) {
596             super(spanCount, orientation);
597         }
598 
collectChildrenBySpan()599         ArrayList<ArrayList<View>> collectChildrenBySpan() {
600             ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>();
601             for (int i = 0; i < getSpanCount(); i++) {
602                 viewsBySpan.add(new ArrayList<View>());
603             }
604             for (int i = 0; i < getChildCount(); i++) {
605                 View view = getChildAt(i);
606                 LayoutParams lp
607                         = (LayoutParams) view
608                         .getLayoutParams();
609                 viewsBySpan.get(lp.mSpan.mIndex).add(view);
610             }
611             return viewsBySpan;
612         }
613 
614         @Nullable
615         @Override
onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state)616         public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
617                 RecyclerView.State state) {
618             View result = null;
619             try {
620                 result = super.onFocusSearchFailed(focused, direction, recycler, state);
621                 validateChildren();
622             } catch (Throwable t) {
623                 postExceptionToInstrumentation(t);
624             }
625             return result;
626         }
627 
getViewBounds(View view)628         Rect getViewBounds(View view) {
629             if (getOrientation() == HORIZONTAL) {
630                 return new Rect(
631                         mPrimaryOrientation.getDecoratedStart(view),
632                         mSecondaryOrientation.getDecoratedStart(view),
633                         mPrimaryOrientation.getDecoratedEnd(view),
634                         mSecondaryOrientation.getDecoratedEnd(view));
635             } else {
636                 return new Rect(
637                         mSecondaryOrientation.getDecoratedStart(view),
638                         mPrimaryOrientation.getDecoratedStart(view),
639                         mSecondaryOrientation.getDecoratedEnd(view),
640                         mPrimaryOrientation.getDecoratedEnd(view));
641             }
642         }
643 
getBoundsLog()644         public String getBoundsLog() {
645             StringBuilder sb = new StringBuilder();
646             sb.append("view bounds:[start:").append(mPrimaryOrientation.getStartAfterPadding())
647                     .append(",").append(" end").append(mPrimaryOrientation.getEndAfterPadding());
648             sb.append("\nchildren bounds\n");
649             final int childCount = getChildCount();
650             for (int i = 0; i < childCount; i++) {
651                 View child = getChildAt(i);
652                 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
653                         .append("[").append("start:").append(
654                         mPrimaryOrientation.getDecoratedStart(child)).append(", end:")
655                         .append(mPrimaryOrientation.getDecoratedEnd(child)).append("]\n");
656             }
657             return sb.toString();
658         }
659 
traverseAndFindVisibleChildren()660         public VisibleChildren traverseAndFindVisibleChildren() {
661             int childCount = getChildCount();
662             final VisibleChildren visibleChildren = new VisibleChildren(getSpanCount());
663             final int start = mPrimaryOrientation.getStartAfterPadding();
664             final int end = mPrimaryOrientation.getEndAfterPadding();
665             for (int i = 0; i < childCount; i++) {
666                 View child = getChildAt(i);
667                 final int childStart = mPrimaryOrientation.getDecoratedStart(child);
668                 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
669                 final boolean fullyVisible = childStart >= start && childEnd <= end;
670                 final boolean hidden = childEnd <= start || childStart >= end;
671                 if (hidden) {
672                     continue;
673                 }
674                 final int position = getPosition(child);
675                 final int span = getLp(child).getSpanIndex();
676                 if (fullyVisible) {
677                     if (position < visibleChildren.firstFullyVisiblePositions[span] ||
678                             visibleChildren.firstFullyVisiblePositions[span]
679                                     == RecyclerView.NO_POSITION) {
680                         visibleChildren.firstFullyVisiblePositions[span] = position;
681                     }
682 
683                     if (position > visibleChildren.lastFullyVisiblePositions[span]) {
684                         visibleChildren.lastFullyVisiblePositions[span] = position;
685                     }
686                 }
687 
688                 if (position < visibleChildren.firstVisiblePositions[span] ||
689                         visibleChildren.firstVisiblePositions[span] == RecyclerView.NO_POSITION) {
690                     visibleChildren.firstVisiblePositions[span] = position;
691                 }
692 
693                 if (position > visibleChildren.lastVisiblePositions[span]) {
694                     visibleChildren.lastVisiblePositions[span] = position;
695                 }
696                 if (visibleChildren.findFirstPartialVisibleClosestToStart == null) {
697                     visibleChildren.findFirstPartialVisibleClosestToStart = child;
698                 }
699                 visibleChildren.findFirstPartialVisibleClosestToEnd = child;
700             }
701             return visibleChildren;
702         }
703 
collectChildCoordinates()704         Map<Item, Rect> collectChildCoordinates() throws Throwable {
705             final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
706             runTestOnUiThread(new Runnable() {
707                 @Override
708                 public void run() {
709                     final int childCount = getChildCount();
710                     for (int i = 0; i < childCount; i++) {
711                         View child = getChildAt(i);
712                         // do it if and only if child is visible
713                         if (child.getRight() < 0 || child.getBottom() < 0 ||
714                                 child.getLeft() >= getWidth() || child.getTop() >= getHeight()) {
715                             // invisible children may be drawn in cases like scrolling so we should
716                             // ignore them
717                             continue;
718                         }
719                         LayoutParams lp = (LayoutParams) child
720                                 .getLayoutParams();
721                         TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
722                         items.put(vh.mBoundItem, getViewBounds(child));
723                     }
724                 }
725             });
726             return items;
727         }
728 
729 
setFakeRtl(Boolean fakeRtl)730         public void setFakeRtl(Boolean fakeRtl) {
731             mFakeRTL = fakeRtl;
732             try {
733                 requestLayoutOnUIThread(mRecyclerView);
734             } catch (Throwable throwable) {
735                 postExceptionToInstrumentation(throwable);
736             }
737         }
738 
layoutToString(String hint)739         String layoutToString(String hint) {
740             StringBuilder sb = new StringBuilder();
741             sb.append("LAYOUT POSITIONS AND INDICES ").append(hint).append("\n");
742             for (int i = 0; i < getChildCount(); i++) {
743                 final View view = getChildAt(i);
744                 final LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
745                 sb.append(String.format("index: %d pos: %d top: %d bottom: %d span: %d isFull:%s",
746                         i, getPosition(view),
747                         mPrimaryOrientation.getDecoratedStart(view),
748                         mPrimaryOrientation.getDecoratedEnd(view),
749                         layoutParams.getSpanIndex(), layoutParams.isFullSpan())).append("\n");
750             }
751             return sb.toString();
752         }
753 
validateChildren()754         protected void validateChildren() {
755             validateChildren(null);
756         }
757 
validateChildren(String msg)758         private void validateChildren(String msg) {
759             if (getChildCount() == 0 || mRecyclerView.mState.isPreLayout()) {
760                 return;
761             }
762             final int dir = mShouldReverseLayout ? -1 : 1;
763             int i = 0;
764             int pos = -1;
765             while (i < getChildCount()) {
766                 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
767                 if (lp.isItemRemoved()) {
768                     i++;
769                     continue;
770                 }
771                 pos = getPosition(getChildAt(i));
772                 break;
773             }
774             if (pos == -1) {
775                 return;
776             }
777             while (++i < getChildCount()) {
778                 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
779                 if (lp.isItemRemoved()) {
780                     continue;
781                 }
782                 pos += dir;
783                 if (getPosition(getChildAt(i)) != pos) {
784                     throw new RuntimeException("INVALID POSITION FOR CHILD " + i + "\n" +
785                             layoutToString("ERROR") + "\n msg:" + msg);
786                 }
787             }
788         }
789     }
790 
791     class GridTestAdapter extends TestAdapter {
792 
793         int mOrientation;
794         int mRecyclerViewWidth;
795         int mRecyclerViewHeight;
796         Integer mSizeReference = null;
797 
798         // original ids of items that should be full span
799         HashSet<Integer> mFullSpanItems = new HashSet<Integer>();
800 
801         protected boolean mViewsHaveEqualSize = false; // size in the scrollable direction
802 
803         protected OnBindCallback mOnBindCallback;
804 
GridTestAdapter(int count, int orientation)805         GridTestAdapter(int count, int orientation) {
806             super(count);
807             mOrientation = orientation;
808         }
809 
810         @Override
onCreateViewHolder(ViewGroup parent, int viewType)811         public TestViewHolder onCreateViewHolder(ViewGroup parent,
812                 int viewType) {
813             mRecyclerViewWidth = parent.getWidth();
814             mRecyclerViewHeight = parent.getHeight();
815             TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
816             if (mOnBindCallback != null) {
817                 mOnBindCallback.onCreatedViewHolder(vh);
818             }
819             return vh;
820         }
821 
822         @Override
offsetOriginalIndices(int start, int offset)823         public void offsetOriginalIndices(int start, int offset) {
824             if (mFullSpanItems.size() > 0) {
825                 HashSet<Integer> old = mFullSpanItems;
826                 mFullSpanItems = new HashSet<Integer>();
827                 for (Integer i : old) {
828                     if (i < start) {
829                         mFullSpanItems.add(i);
830                     } else if (offset > 0 || (start + Math.abs(offset)) <= i) {
831                         mFullSpanItems.add(i + offset);
832                     } else if (DEBUG) {
833                         Log.d(TAG, "removed full span item " + i);
834                     }
835                 }
836             }
837             super.offsetOriginalIndices(start, offset);
838         }
839 
840         @Override
moveInUIThread(int from, int to)841         protected void moveInUIThread(int from, int to) {
842             boolean setAsFullSpanAgain = mFullSpanItems.contains(from);
843             super.moveInUIThread(from, to);
844             if (setAsFullSpanAgain) {
845                 mFullSpanItems.add(to);
846             }
847         }
848 
849         @Override
onBindViewHolder(TestViewHolder holder, int position)850         public void onBindViewHolder(TestViewHolder holder,
851                 int position) {
852             if (mSizeReference == null) {
853                 mSizeReference = mOrientation == OrientationHelper.HORIZONTAL ? mRecyclerViewWidth
854                         / AVG_ITEM_PER_VIEW : mRecyclerViewHeight / AVG_ITEM_PER_VIEW;
855             }
856             super.onBindViewHolder(holder, position);
857             Item item = mItems.get(position);
858             RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
859                     .getLayoutParams();
860             if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
861                 ((StaggeredGridLayoutManager.LayoutParams) lp)
862                         .setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
863             } else {
864                 StaggeredGridLayoutManager.LayoutParams slp
865                         = (StaggeredGridLayoutManager.LayoutParams) mLayoutManager
866                         .generateDefaultLayoutParams();
867                 holder.itemView.setLayoutParams(slp);
868                 slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
869                 lp = slp;
870             }
871 
872             if (mOnBindCallback == null || mOnBindCallback.assignRandomSize()) {
873                 final int minSize = mViewsHaveEqualSize ? mSizeReference :
874                         mSizeReference + 20 * (item.mId % 10);
875                 if (mOrientation == OrientationHelper.HORIZONTAL) {
876                     holder.itemView.setMinimumWidth(minSize);
877                 } else {
878                     holder.itemView.setMinimumHeight(minSize);
879                 }
880                 lp.topMargin = 3;
881                 lp.leftMargin = 5;
882                 lp.rightMargin = 7;
883                 lp.bottomMargin = 9;
884             }
885 
886             if (mOnBindCallback != null) {
887                 mOnBindCallback.onBoundItem(holder, position);
888             }
889         }
890     }
891 }
892