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