BaseStaggeredGridLayoutManagerTest.java revision c587f7dba5a337169e854e235da59f595255d6cc
1package android.support.v7.widget;
2
3import android.graphics.Rect;
4import android.support.annotation.Nullable;
5import android.util.Log;
6import android.view.View;
7import android.view.ViewGroup;
8
9import java.lang.reflect.Field;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.HashSet;
13import java.util.LinkedHashMap;
14import java.util.List;
15import java.util.Map;
16import java.util.concurrent.CountDownLatch;
17import java.util.concurrent.TimeUnit;
18import java.util.concurrent.atomic.AtomicInteger;
19
20import static android.support.v7.widget.LayoutState.LAYOUT_END;
21import static android.support.v7.widget.LayoutState.LAYOUT_START;
22import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
23import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
24import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE;
25import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL;
26import static org.junit.Assert.assertEquals;
27import static org.junit.Assert.assertFalse;
28import static org.junit.Assert.assertNotNull;
29import static org.junit.Assert.assertTrue;
30
31import static java.util.concurrent.TimeUnit.SECONDS;
32
33import org.hamcrest.CoreMatchers;
34import org.hamcrest.MatcherAssert;
35
36public 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
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
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
78    void setupByConfig(Config config) throws Throwable {
79        setupByConfig(config, new GridTestAdapter(config.mItemCount, config.mOrientation));
80    }
81
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
109    StaggeredGridLayoutManager.LayoutParams getLp(View view) {
110        return (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
111    }
112
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     */
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
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
148    public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
149        assertRectSetsEqual(message, before, after, true);
150    }
151
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
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
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
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
259        void before(RecyclerView.Recycler recycler, RecyclerView.State state) {
260        }
261
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
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
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
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
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
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
376        abstract void onBoundItem(TestViewHolder vh, int position);
377
378        boolean assignRandomSize() {
379            return true;
380        }
381
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
402        Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) {
403            mOrientation = orientation;
404            mReverseLayout = reverseLayout;
405            mSpanCount = spanCount;
406            mGapStrategy = gapStrategy;
407        }
408
409        public Config() {
410
411        }
412
413        Config orientation(int orientation) {
414            mOrientation = orientation;
415            return this;
416        }
417
418        Config reverseLayout(boolean reverseLayout) {
419            mReverseLayout = reverseLayout;
420            return this;
421        }
422
423        Config spanCount(int spanCount) {
424            mSpanCount = spanCount;
425            return this;
426        }
427
428        Config gapStrategy(int gapStrategy) {
429            mGapStrategy = gapStrategy;
430            return this;
431        }
432
433        public Config itemCount(int itemCount) {
434            mItemCount = itemCount;
435            return this;
436        }
437
438        public Config wrap(boolean wrap) {
439            mWrap = wrap;
440            return this;
441        }
442
443        @Override
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
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
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
481        boolean isLayoutRTL() {
482            return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
483        }
484
485        public void expectLayouts(int count) {
486            layoutLatch = new CountDownLatch(count);
487        }
488
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
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
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
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
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
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
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
595        public WrappedLayoutManager(int spanCount, int orientation) {
596            super(spanCount, orientation);
597        }
598
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
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
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
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
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
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
730        public void setFakeRtl(Boolean fakeRtl) {
731            mFakeRTL = fakeRtl;
732            try {
733                requestLayoutOnUIThread(mRecyclerView);
734            } catch (Throwable throwable) {
735                postExceptionToInstrumentation(throwable);
736            }
737        }
738
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
754        protected void validateChildren() {
755            validateChildren(null);
756        }
757
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
805        GridTestAdapter(int count, int orientation) {
806            super(count);
807            mOrientation = orientation;
808        }
809
810        @Override
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
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
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
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