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
479        @Override
480        boolean isLayoutRTL() {
481            return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
482        }
483
484        public void expectLayouts(int count) {
485            layoutLatch = new CountDownLatch(count);
486        }
487
488        public void waitForLayout(int seconds) throws Throwable {
489            layoutLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
490            checkForMainThreadException();
491            MatcherAssert.assertThat("all layouts should complete on time",
492                    layoutLatch.getCount(), CoreMatchers.is(0L));
493            // use a runnable to ensure RV layout is finished
494            getInstrumentation().runOnMainSync(new Runnable() {
495                @Override
496                public void run() {
497                }
498            });
499        }
500
501        public void assertNoLayout(String msg, long timeout) throws Throwable {
502            layoutLatch.await(timeout, TimeUnit.SECONDS);
503            assertFalse(msg, layoutLatch.getCount() == 0);
504        }
505
506        @Override
507        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
508            String before;
509            if (DEBUG) {
510                before = layoutToString("before");
511            } else {
512                before = "enable DEBUG";
513            }
514            try {
515                if (mOnLayoutListener != null) {
516                    mOnLayoutListener.before(recycler, state);
517                }
518                super.onLayoutChildren(recycler, state);
519                if (mOnLayoutListener != null) {
520                    mOnLayoutListener.after(recycler, state);
521                }
522                validateChildren(before);
523            } catch (Throwable t) {
524                postExceptionToInstrumentation(t);
525            }
526
527            layoutLatch.countDown();
528        }
529
530        @Override
531        int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
532            try {
533                int result = super.scrollBy(dt, recycler, state);
534                validateChildren();
535                return result;
536            } catch (Throwable t) {
537                postExceptionToInstrumentation(t);
538            }
539
540            return 0;
541        }
542
543        public WrappedLayoutManager(int spanCount, int orientation) {
544            super(spanCount, orientation);
545        }
546
547        ArrayList<ArrayList<View>> collectChildrenBySpan() {
548            ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>();
549            for (int i = 0; i < getSpanCount(); i++) {
550                viewsBySpan.add(new ArrayList<View>());
551            }
552            for (int i = 0; i < getChildCount(); i++) {
553                View view = getChildAt(i);
554                LayoutParams lp
555                        = (LayoutParams) view
556                        .getLayoutParams();
557                viewsBySpan.get(lp.mSpan.mIndex).add(view);
558            }
559            return viewsBySpan;
560        }
561
562        @Nullable
563        @Override
564        public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
565                RecyclerView.State state) {
566            View result = null;
567            try {
568                result = super.onFocusSearchFailed(focused, direction, recycler, state);
569                validateChildren();
570            } catch (Throwable t) {
571                postExceptionToInstrumentation(t);
572            }
573            return result;
574        }
575
576        Rect getViewBounds(View view) {
577            if (getOrientation() == HORIZONTAL) {
578                return new Rect(
579                        mPrimaryOrientation.getDecoratedStart(view),
580                        mSecondaryOrientation.getDecoratedStart(view),
581                        mPrimaryOrientation.getDecoratedEnd(view),
582                        mSecondaryOrientation.getDecoratedEnd(view));
583            } else {
584                return new Rect(
585                        mSecondaryOrientation.getDecoratedStart(view),
586                        mPrimaryOrientation.getDecoratedStart(view),
587                        mSecondaryOrientation.getDecoratedEnd(view),
588                        mPrimaryOrientation.getDecoratedEnd(view));
589            }
590        }
591
592        public String getBoundsLog() {
593            StringBuilder sb = new StringBuilder();
594            sb.append("view bounds:[start:").append(mPrimaryOrientation.getStartAfterPadding())
595                    .append(",").append(" end").append(mPrimaryOrientation.getEndAfterPadding());
596            sb.append("\nchildren bounds\n");
597            final int childCount = getChildCount();
598            for (int i = 0; i < childCount; i++) {
599                View child = getChildAt(i);
600                sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
601                        .append("[").append("start:").append(
602                        mPrimaryOrientation.getDecoratedStart(child)).append(", end:")
603                        .append(mPrimaryOrientation.getDecoratedEnd(child)).append("]\n");
604            }
605            return sb.toString();
606        }
607
608        public VisibleChildren traverseAndFindVisibleChildren() {
609            int childCount = getChildCount();
610            final VisibleChildren visibleChildren = new VisibleChildren(getSpanCount());
611            final int start = mPrimaryOrientation.getStartAfterPadding();
612            final int end = mPrimaryOrientation.getEndAfterPadding();
613            for (int i = 0; i < childCount; i++) {
614                View child = getChildAt(i);
615                final int childStart = mPrimaryOrientation.getDecoratedStart(child);
616                final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
617                final boolean fullyVisible = childStart >= start && childEnd <= end;
618                final boolean hidden = childEnd <= start || childStart >= end;
619                if (hidden) {
620                    continue;
621                }
622                final int position = getPosition(child);
623                final int span = getLp(child).getSpanIndex();
624                if (fullyVisible) {
625                    if (position < visibleChildren.firstFullyVisiblePositions[span] ||
626                            visibleChildren.firstFullyVisiblePositions[span]
627                                    == RecyclerView.NO_POSITION) {
628                        visibleChildren.firstFullyVisiblePositions[span] = position;
629                    }
630
631                    if (position > visibleChildren.lastFullyVisiblePositions[span]) {
632                        visibleChildren.lastFullyVisiblePositions[span] = position;
633                    }
634                }
635
636                if (position < visibleChildren.firstVisiblePositions[span] ||
637                        visibleChildren.firstVisiblePositions[span] == RecyclerView.NO_POSITION) {
638                    visibleChildren.firstVisiblePositions[span] = position;
639                }
640
641                if (position > visibleChildren.lastVisiblePositions[span]) {
642                    visibleChildren.lastVisiblePositions[span] = position;
643                }
644                if (visibleChildren.findFirstPartialVisibleClosestToStart == null) {
645                    visibleChildren.findFirstPartialVisibleClosestToStart = child;
646                }
647                visibleChildren.findFirstPartialVisibleClosestToEnd = child;
648            }
649            return visibleChildren;
650        }
651
652        Map<Item, Rect> collectChildCoordinates() throws Throwable {
653            final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
654            runTestOnUiThread(new Runnable() {
655                @Override
656                public void run() {
657                    final int childCount = getChildCount();
658                    for (int i = 0; i < childCount; i++) {
659                        View child = getChildAt(i);
660                        // do it if and only if child is visible
661                        if (child.getRight() < 0 || child.getBottom() < 0 ||
662                                child.getLeft() >= getWidth() || child.getTop() >= getHeight()) {
663                            // invisible children may be drawn in cases like scrolling so we should
664                            // ignore them
665                            continue;
666                        }
667                        LayoutParams lp = (LayoutParams) child
668                                .getLayoutParams();
669                        TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
670                        items.put(vh.mBoundItem, getViewBounds(child));
671                    }
672                }
673            });
674            return items;
675        }
676
677
678        public void setFakeRtl(Boolean fakeRtl) {
679            mFakeRTL = fakeRtl;
680            try {
681                requestLayoutOnUIThread(mRecyclerView);
682            } catch (Throwable throwable) {
683                postExceptionToInstrumentation(throwable);
684            }
685        }
686
687        String layoutToString(String hint) {
688            StringBuilder sb = new StringBuilder();
689            sb.append("LAYOUT POSITIONS AND INDICES ").append(hint).append("\n");
690            for (int i = 0; i < getChildCount(); i++) {
691                final View view = getChildAt(i);
692                final LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
693                sb.append(String.format("index: %d pos: %d top: %d bottom: %d span: %d isFull:%s",
694                        i, getPosition(view),
695                        mPrimaryOrientation.getDecoratedStart(view),
696                        mPrimaryOrientation.getDecoratedEnd(view),
697                        layoutParams.getSpanIndex(), layoutParams.isFullSpan())).append("\n");
698            }
699            return sb.toString();
700        }
701
702        protected void validateChildren() {
703            validateChildren(null);
704        }
705
706        private void validateChildren(String msg) {
707            if (getChildCount() == 0 || mRecyclerView.mState.isPreLayout()) {
708                return;
709            }
710            final int dir = mShouldReverseLayout ? -1 : 1;
711            int i = 0;
712            int pos = -1;
713            while (i < getChildCount()) {
714                LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
715                if (lp.isItemRemoved()) {
716                    i++;
717                    continue;
718                }
719                pos = getPosition(getChildAt(i));
720                break;
721            }
722            if (pos == -1) {
723                return;
724            }
725            while (++i < getChildCount()) {
726                LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
727                if (lp.isItemRemoved()) {
728                    continue;
729                }
730                pos += dir;
731                if (getPosition(getChildAt(i)) != pos) {
732                    throw new RuntimeException("INVALID POSITION FOR CHILD " + i + "\n" +
733                            layoutToString("ERROR") + "\n msg:" + msg);
734                }
735            }
736        }
737    }
738
739    class GridTestAdapter extends TestAdapter {
740
741        int mOrientation;
742        int mRecyclerViewWidth;
743        int mRecyclerViewHeight;
744        Integer mSizeReference = null;
745
746        // original ids of items that should be full span
747        HashSet<Integer> mFullSpanItems = new HashSet<Integer>();
748
749        protected boolean mViewsHaveEqualSize = false; // size in the scrollable direction
750
751        protected OnBindCallback mOnBindCallback;
752
753        GridTestAdapter(int count, int orientation) {
754            super(count);
755            mOrientation = orientation;
756        }
757
758        @Override
759        public TestViewHolder onCreateViewHolder(ViewGroup parent,
760                int viewType) {
761            mRecyclerViewWidth = parent.getWidth();
762            mRecyclerViewHeight = parent.getHeight();
763            TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
764            if (mOnBindCallback != null) {
765                mOnBindCallback.onCreatedViewHolder(vh);
766            }
767            return vh;
768        }
769
770        @Override
771        public void offsetOriginalIndices(int start, int offset) {
772            if (mFullSpanItems.size() > 0) {
773                HashSet<Integer> old = mFullSpanItems;
774                mFullSpanItems = new HashSet<Integer>();
775                for (Integer i : old) {
776                    if (i < start) {
777                        mFullSpanItems.add(i);
778                    } else if (offset > 0 || (start + Math.abs(offset)) <= i) {
779                        mFullSpanItems.add(i + offset);
780                    } else if (DEBUG) {
781                        Log.d(TAG, "removed full span item " + i);
782                    }
783                }
784            }
785            super.offsetOriginalIndices(start, offset);
786        }
787
788        @Override
789        protected void moveInUIThread(int from, int to) {
790            boolean setAsFullSpanAgain = mFullSpanItems.contains(from);
791            super.moveInUIThread(from, to);
792            if (setAsFullSpanAgain) {
793                mFullSpanItems.add(to);
794            }
795        }
796
797        @Override
798        public void onBindViewHolder(TestViewHolder holder,
799                int position) {
800            if (mSizeReference == null) {
801                mSizeReference = mOrientation == OrientationHelper.HORIZONTAL ? mRecyclerViewWidth
802                        / AVG_ITEM_PER_VIEW : mRecyclerViewHeight / AVG_ITEM_PER_VIEW;
803            }
804            super.onBindViewHolder(holder, position);
805            Item item = mItems.get(position);
806            RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
807                    .getLayoutParams();
808            if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
809                ((StaggeredGridLayoutManager.LayoutParams) lp)
810                        .setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
811            } else {
812                StaggeredGridLayoutManager.LayoutParams slp
813                        = (StaggeredGridLayoutManager.LayoutParams) mLayoutManager
814                        .generateDefaultLayoutParams();
815                holder.itemView.setLayoutParams(slp);
816                slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
817                lp = slp;
818            }
819
820            if (mOnBindCallback == null || mOnBindCallback.assignRandomSize()) {
821                final int minSize = mViewsHaveEqualSize ? mSizeReference :
822                        mSizeReference + 20 * (item.mId % 10);
823                if (mOrientation == OrientationHelper.HORIZONTAL) {
824                    holder.itemView.setMinimumWidth(minSize);
825                } else {
826                    holder.itemView.setMinimumHeight(minSize);
827                }
828                lp.topMargin = 3;
829                lp.leftMargin = 5;
830                lp.rightMargin = 7;
831                lp.bottomMargin = 9;
832            }
833
834            if (mOnBindCallback != null) {
835                mOnBindCallback.onBoundItem(holder, position);
836            }
837        }
838    }
839}
840