1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.widget;
18
19import android.content.Context;
20import android.support.v4.view.AccessibilityDelegateCompat;
21import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
22import android.util.Log;
23import android.view.View;
24import android.view.ViewGroup;
25
26import java.util.ArrayList;
27import java.util.Arrays;
28import java.util.BitSet;
29import java.util.HashMap;
30import java.util.HashSet;
31import java.util.List;
32import java.util.Map;
33import java.util.Set;
34import java.util.concurrent.CountDownLatch;
35
36import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
37import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
38import static java.util.concurrent.TimeUnit.SECONDS;
39
40public class GridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
41
42    static final String TAG = "GridLayoutManagerTest";
43
44    static final boolean DEBUG = false;
45
46    WrappedGridLayoutManager mGlm;
47
48    GridTestAdapter mAdapter;
49
50    final List<Config> mBaseVariations = new ArrayList<Config>();
51
52    @Override
53    protected void setUp() throws Exception {
54        super.setUp();
55        for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
56            for (boolean reverseLayout : new boolean[]{false, true}) {
57                for (int spanCount : new int[]{1, 3, 4}) {
58                    mBaseVariations.add(new Config(spanCount, orientation, reverseLayout));
59                }
60            }
61        }
62    }
63
64    public RecyclerView setupBasic(Config config) throws Throwable {
65        return setupBasic(config, new GridTestAdapter(config.mItemCount));
66    }
67
68    public RecyclerView setupBasic(Config config, GridTestAdapter testAdapter) throws Throwable {
69        RecyclerView recyclerView = new RecyclerView(getActivity());
70        mAdapter = testAdapter;
71        mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
72                config.mReverseLayout);
73        mAdapter.assignSpanSizeLookup(mGlm);
74        recyclerView.setAdapter(mAdapter);
75        recyclerView.setLayoutManager(mGlm);
76        return recyclerView;
77    }
78
79    public void waitForFirstLayout(RecyclerView recyclerView) throws Throwable {
80        mGlm.expectLayout(1);
81        setRecyclerView(recyclerView);
82        mGlm.waitForLayout(2);
83    }
84
85    public void testCustomWidthInHorizontal() throws Throwable {
86        customSizeInScrollDirectionTest(new Config(3, HORIZONTAL, false));
87    }
88
89    public void testCustomHeightInVertical() throws Throwable {
90        customSizeInScrollDirectionTest(new Config(3, VERTICAL, false));
91    }
92
93    public void customSizeInScrollDirectionTest(final Config config) throws Throwable {
94        final int[] sizePerPosition = new int[]{3, 5, 9, 21, 3, 5, 9, 6, 9, 1};
95        final int[] expectedSizePerPosition = new int[]{9, 9, 9, 21, 3, 5, 9, 9, 9, 1};
96        final GridTestAdapter testAdapter = new GridTestAdapter(10) {
97            @Override
98            public void onBindViewHolder(TestViewHolder holder,
99                    int position) {
100                super.onBindViewHolder(holder, position);
101                ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
102                if (layoutParams == null) {
103                    layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
104                            ViewGroup.LayoutParams.WRAP_CONTENT);
105                    holder.itemView.setLayoutParams(layoutParams);
106                }
107                final int size = sizePerPosition[position];
108                if (config.mOrientation == HORIZONTAL) {
109                    layoutParams.width = size;
110                } else {
111                    layoutParams.height = size;
112                }
113            }
114        };
115        testAdapter.setFullSpan(3, 5);
116        final RecyclerView rv = setupBasic(config, testAdapter);
117        waitForFirstLayout(rv);
118
119        assertTrue("[test sanity] some views should be laid out", mRecyclerView.getChildCount() > 0);
120        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
121            View child = mRecyclerView.getChildAt(i);
122            final int size = config.mOrientation == HORIZONTAL ? child.getWidth()
123                    : child.getHeight();
124            assertEquals("child " + i + " should have the size specified in its layout params",
125                    expectedSizePerPosition[i], size);
126        }
127        checkForMainThreadException();
128    }
129
130    public void testLayoutParams() throws Throwable {
131        layoutParamsTest(GridLayoutManager.HORIZONTAL);
132        removeRecyclerView();
133        layoutParamsTest(GridLayoutManager.VERTICAL);
134    }
135
136    public void testHorizontalAccessibilitySpanIndices() throws Throwable {
137        accessibilitySpanIndicesTest(HORIZONTAL);
138    }
139
140    public void testVerticalAccessibilitySpanIndices() throws Throwable {
141        accessibilitySpanIndicesTest(VERTICAL);
142    }
143
144    public void accessibilitySpanIndicesTest(int orientation) throws Throwable {
145        final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false));
146        waitForFirstLayout(recyclerView);
147        final AccessibilityDelegateCompat delegateCompat = mRecyclerView
148                .getCompatAccessibilityDelegate().getItemDelegate();
149        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
150        final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2);
151        final int position = recyclerView.getChildLayoutPosition(chosen);
152        runTestOnUiThread(new Runnable() {
153            @Override
154            public void run() {
155                delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info);
156            }
157        });
158        GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
159        AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info
160                .getCollectionItemInfo();
161        assertNotNull(itemInfo);
162        assertEquals("result should have span group position",
163                ssl.getSpanGroupIndex(position, mGlm.getSpanCount()),
164                orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex());
165        assertEquals("result should have span index",
166                ssl.getSpanIndex(position, mGlm.getSpanCount()),
167                orientation == HORIZONTAL ? itemInfo.getRowIndex() :  itemInfo.getColumnIndex());
168        assertEquals("result should have span size",
169                ssl.getSpanSize(position),
170                orientation == HORIZONTAL ? itemInfo.getRowSpan() :  itemInfo.getColumnSpan());
171    }
172
173    public GridLayoutManager.LayoutParams ensureGridLp(View view) {
174        ViewGroup.LayoutParams lp = view.getLayoutParams();
175        GridLayoutManager.LayoutParams glp;
176        if (lp instanceof GridLayoutManager.LayoutParams) {
177            glp = (GridLayoutManager.LayoutParams) lp;
178        } else if (lp == null) {
179            glp = (GridLayoutManager.LayoutParams) mGlm
180                    .generateDefaultLayoutParams();
181            view.setLayoutParams(glp);
182        } else {
183            glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp);
184            view.setLayoutParams(glp);
185        }
186        return glp;
187    }
188
189    public void layoutParamsTest(final int orientation) throws Throwable {
190        final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation),
191                new GridTestAdapter(100) {
192                    @Override
193                    public void onBindViewHolder(TestViewHolder holder,
194                            int position) {
195                        super.onBindViewHolder(holder, position);
196                        GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
197                        int val = 0;
198                        switch (position % 5) {
199                            case 0:
200                                val = 10;
201                                break;
202                            case 1:
203                                val = 30;
204                                break;
205                            case 2:
206                                val = GridLayoutManager.LayoutParams.WRAP_CONTENT;
207                                break;
208                            case 3:
209                                val = GridLayoutManager.LayoutParams.FILL_PARENT;
210                                break;
211                            case 4:
212                                val = 200;
213                                break;
214                        }
215                        if (orientation == GridLayoutManager.VERTICAL) {
216                            glp.height = val;
217                        } else {
218                            glp.width = val;
219                        }
220                        holder.itemView.setLayoutParams(glp);
221                    }
222                });
223        waitForFirstLayout(rv);
224        final OrientationHelper helper = mGlm.mOrientationHelper;
225        final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2)));
226        assertEquals(firstRowSize,
227                helper.getDecoratedMeasurement(mGlm.findViewByPosition(0)));
228        assertEquals(firstRowSize,
229                helper.getDecoratedMeasurement(mGlm.findViewByPosition(1)));
230        assertEquals(firstRowSize,
231                helper.getDecoratedMeasurement(mGlm.findViewByPosition(2)));
232        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0)));
233        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1)));
234        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2)));
235
236        final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3)));
237        assertEquals(secondRowSize,
238                helper.getDecoratedMeasurement(mGlm.findViewByPosition(3)));
239        assertEquals(secondRowSize,
240                helper.getDecoratedMeasurement(mGlm.findViewByPosition(4)));
241        assertEquals(secondRowSize,
242                helper.getDecoratedMeasurement(mGlm.findViewByPosition(5)));
243        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3)));
244        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4)));
245        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5)));
246    }
247
248    private int getSize(View view) {
249        if (mGlm.getOrientation() == GridLayoutManager.HORIZONTAL) {
250            return view.getWidth();
251        }
252        return view.getHeight();
253    }
254
255    public void testAnchorUpdate() throws InterruptedException {
256        GridLayoutManager glm = new GridLayoutManager(getActivity(), 11);
257        final GridLayoutManager.SpanSizeLookup spanSizeLookup
258                = new GridLayoutManager.SpanSizeLookup() {
259            @Override
260            public int getSpanSize(int position) {
261                if (position > 200) {
262                    return 100;
263                }
264                if (position > 20) {
265                    return 2;
266                }
267                return 1;
268            }
269        };
270        glm.setSpanSizeLookup(spanSizeLookup);
271        glm.mAnchorInfo.mPosition = 11;
272        RecyclerView.State state = new RecyclerView.State();
273        state.mItemCount = 1000;
274        glm.onAnchorReady(state, glm.mAnchorInfo);
275        assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition);
276
277        glm.mAnchorInfo.mPosition = 13;
278        glm.onAnchorReady(state, glm.mAnchorInfo);
279        assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition);
280
281        glm.mAnchorInfo.mPosition = 23;
282        glm.onAnchorReady(state, glm.mAnchorInfo);
283        assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition);
284
285        glm.mAnchorInfo.mPosition = 35;
286        glm.onAnchorReady(state, glm.mAnchorInfo);
287        assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition);
288    }
289
290    public void testSpanLookup() {
291        spanLookupTest(false);
292    }
293
294    public void testSpanLookupWithCache() {
295        spanLookupTest(true);
296    }
297
298    public void testSpanLookupCache() {
299        final GridLayoutManager.SpanSizeLookup ssl
300                = new GridLayoutManager.SpanSizeLookup() {
301            @Override
302            public int getSpanSize(int position) {
303                if (position > 6) {
304                    return 2;
305                }
306                return 1;
307            }
308        };
309        ssl.setSpanIndexCacheEnabled(true);
310        assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(2));
311        ssl.getCachedSpanIndex(4, 5);
312        assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(3));
313        // this should not happen and if happens, it is better to return -1
314        assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
315        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(5));
316        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(100));
317        ssl.getCachedSpanIndex(6, 5);
318        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
319        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(6));
320        assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
321        ssl.getCachedSpanIndex(12, 5);
322        assertEquals("reference child before", 12, ssl.findReferenceIndexFromCache(13));
323        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(12));
324        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
325        for (int i = 0; i < 6; i++) {
326            ssl.getCachedSpanIndex(i, 5);
327        }
328
329        for (int i = 1; i < 7; i++) {
330            assertEquals("reference child right before " + i, i - 1,
331                    ssl.findReferenceIndexFromCache(i));
332        }
333        assertEquals("reference child before 0 ", -1, ssl.findReferenceIndexFromCache(0));
334    }
335
336    public void spanLookupTest(boolean enableCache) {
337        final GridLayoutManager.SpanSizeLookup ssl
338                = new GridLayoutManager.SpanSizeLookup() {
339            @Override
340            public int getSpanSize(int position) {
341                if (position > 200) {
342                    return 100;
343                }
344                if (position > 6) {
345                    return 2;
346                }
347                return 1;
348            }
349        };
350        ssl.setSpanIndexCacheEnabled(enableCache);
351        assertEquals(0, ssl.getCachedSpanIndex(0, 5));
352        assertEquals(4, ssl.getCachedSpanIndex(4, 5));
353        assertEquals(0, ssl.getCachedSpanIndex(5, 5));
354        assertEquals(1, ssl.getCachedSpanIndex(6, 5));
355        assertEquals(2, ssl.getCachedSpanIndex(7, 5));
356        assertEquals(2, ssl.getCachedSpanIndex(9, 5));
357        assertEquals(0, ssl.getCachedSpanIndex(8, 5));
358    }
359
360    public void testSpanGroupIndex() {
361        final GridLayoutManager.SpanSizeLookup ssl
362                = new GridLayoutManager.SpanSizeLookup() {
363            @Override
364            public int getSpanSize(int position) {
365                if (position > 200) {
366                    return 100;
367                }
368                if (position > 6) {
369                    return 2;
370                }
371                return 1;
372            }
373        };
374        assertEquals(0, ssl.getSpanGroupIndex(0, 5));
375        assertEquals(0, ssl.getSpanGroupIndex(4, 5));
376        assertEquals(1, ssl.getSpanGroupIndex(5, 5));
377        assertEquals(1, ssl.getSpanGroupIndex(6, 5));
378        assertEquals(1, ssl.getSpanGroupIndex(7, 5));
379        assertEquals(2, ssl.getSpanGroupIndex(9, 5));
380        assertEquals(2, ssl.getSpanGroupIndex(8, 5));
381    }
382
383    public void testNotifyDataSetChange() throws Throwable {
384        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
385        final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup();
386        ssl.setSpanIndexCacheEnabled(true);
387        waitForFirstLayout(recyclerView);
388        assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0);
389        final Callback callback = new Callback() {
390            @Override
391            public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
392                if (!state.isPreLayout()) {
393                    assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size());
394                }
395            }
396
397            @Override
398            public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
399                if (!state.isPreLayout()) {
400                    assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0);
401                }
402            }
403        };
404        mGlm.mCallbacks.add(callback);
405        mGlm.expectLayout(2);
406        mAdapter.deleteAndNotify(2, 3);
407        mGlm.waitForLayout(2);
408        checkForMainThreadException();
409    }
410
411    public void testUnevenHeights() throws Throwable {
412        final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
413                new HashMap<Integer, RecyclerView.ViewHolder>();
414        RecyclerView recyclerView = setupBasic(new Config(3, 3), new GridTestAdapter(3) {
415            @Override
416            public void onBindViewHolder(TestViewHolder holder,
417                    int position) {
418                super.onBindViewHolder(holder, position);
419                final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
420                glp.height = 50 + position * 50;
421                viewHolderMap.put(position, holder);
422            }
423        });
424        waitForFirstLayout(recyclerView);
425        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
426            assertEquals("all items should get max height", 150,
427                    vh.itemView.getHeight());
428        }
429
430        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
431            assertEquals("all items should have measured the max height", 150,
432                    vh.itemView.getMeasuredHeight());
433        }
434    }
435
436    public void testUnevenWidths() throws Throwable {
437        final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
438                new HashMap<Integer, RecyclerView.ViewHolder>();
439        RecyclerView recyclerView = setupBasic(new Config(3, HORIZONTAL, false),
440                new GridTestAdapter(3) {
441                    @Override
442                    public void onBindViewHolder(TestViewHolder holder,
443                            int position) {
444                        super.onBindViewHolder(holder, position);
445                        final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
446                        glp.width = 50 + position * 50;
447                        viewHolderMap.put(position, holder);
448                    }
449                });
450        waitForFirstLayout(recyclerView);
451        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
452            assertEquals("all items should get max width", 150,
453                    vh.itemView.getWidth());
454        }
455
456        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
457            assertEquals("all items should have measured the max width", 150,
458                    vh.itemView.getMeasuredWidth());
459        }
460    }
461
462    public void testScrollBackAndPreservePositions() throws Throwable {
463        for (Config config : mBaseVariations) {
464            config.mItemCount = 150;
465            scrollBackAndPreservePositionsTest(config);
466            removeRecyclerView();
467        }
468    }
469
470    public void testCacheSpanIndices() throws Throwable {
471        final RecyclerView rv = setupBasic(new Config(3, 100));
472        mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true);
473        waitForFirstLayout(rv);
474        GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
475        assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0);
476        assertEquals("item index 5 should be in span 2", 2,
477                getLp(mGlm.findViewByPosition(5)).getSpanIndex());
478        mGlm.expectLayout(2);
479        mAdapter.mFullSpanItems.add(4);
480        mAdapter.changeAndNotify(4, 1);
481        mGlm.waitForLayout(2);
482        assertEquals("item index 5 should be in span 2", 0,
483                getLp(mGlm.findViewByPosition(5)).getSpanIndex());
484    }
485
486    GridLayoutManager.LayoutParams getLp(View view) {
487        return (GridLayoutManager.LayoutParams) view.getLayoutParams();
488    }
489
490    public void scrollBackAndPreservePositionsTest(final Config config) throws Throwable {
491        final RecyclerView rv = setupBasic(config);
492        for (int i = 1; i < mAdapter.getItemCount(); i += config.mSpanCount + 2) {
493            mAdapter.setFullSpan(i);
494        }
495        waitForFirstLayout(rv);
496        final int[] globalPositions = new int[mAdapter.getItemCount()];
497        Arrays.fill(globalPositions, Integer.MIN_VALUE);
498        final int scrollStep = (mGlm.mOrientationHelper.getTotalSpace() / 20)
499                * (config.mReverseLayout ? -1 : 1);
500        final String logPrefix = config.toString();
501        final int[] globalPos = new int[1];
502        runTestOnUiThread(new Runnable() {
503            @Override
504            public void run() {
505                int globalScrollPosition = 0;
506                int visited = 0;
507                while (visited < mAdapter.getItemCount()) {
508                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
509                        View child = mRecyclerView.getChildAt(i);
510                        final int pos = mRecyclerView.getChildLayoutPosition(child);
511                        if (globalPositions[pos] != Integer.MIN_VALUE) {
512                            continue;
513                        }
514                        visited++;
515                        GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
516                                child.getLayoutParams();
517                        if (config.mReverseLayout) {
518                            globalPositions[pos] = globalScrollPosition +
519                                    mGlm.mOrientationHelper.getDecoratedEnd(child);
520                        } else {
521                            globalPositions[pos] = globalScrollPosition +
522                                    mGlm.mOrientationHelper.getDecoratedStart(child);
523                        }
524                        assertEquals(logPrefix + " span index should match",
525                                mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
526                                lp.getSpanIndex());
527                    }
528                    int scrolled = mGlm.scrollBy(scrollStep,
529                            mRecyclerView.mRecycler, mRecyclerView.mState);
530                    globalScrollPosition += scrolled;
531                    if (scrolled == 0) {
532                        assertEquals(
533                                logPrefix + " If scroll is complete, all views should be visited",
534                                visited, mAdapter.getItemCount());
535                    }
536                }
537                if (DEBUG) {
538                    Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
539                }
540                globalPos[0] = globalScrollPosition;
541            }
542        });
543        checkForMainThreadException();
544        runTestOnUiThread(new Runnable() {
545            @Override
546            public void run() {
547                int globalScrollPosition = globalPos[0];
548                // now scroll back and make sure global positions match
549                BitSet shouldTest = new BitSet(mAdapter.getItemCount());
550                shouldTest.set(0, mAdapter.getItemCount() - 1, true);
551                String assertPrefix = config
552                        + " global pos must match when scrolling in reverse for position ";
553                int scrollAmount = Integer.MAX_VALUE;
554                while (!shouldTest.isEmpty() && scrollAmount != 0) {
555                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
556                        View child = mRecyclerView.getChildAt(i);
557                        int pos = mRecyclerView.getChildLayoutPosition(child);
558                        if (!shouldTest.get(pos)) {
559                            continue;
560                        }
561                        GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
562                                child.getLayoutParams();
563                        shouldTest.clear(pos);
564                        int globalPos;
565                        if (config.mReverseLayout) {
566                            globalPos = globalScrollPosition +
567                                    mGlm.mOrientationHelper.getDecoratedEnd(child);
568                        } else {
569                            globalPos = globalScrollPosition +
570                                    mGlm.mOrientationHelper.getDecoratedStart(child);
571                        }
572                        assertEquals(assertPrefix + pos,
573                                globalPositions[pos], globalPos);
574                        assertEquals("span index should match",
575                                mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
576                                lp.getSpanIndex());
577                    }
578                    scrollAmount = mGlm.scrollBy(-scrollStep,
579                            mRecyclerView.mRecycler, mRecyclerView.mState);
580                    globalScrollPosition += scrollAmount;
581                }
582                assertTrue("all views should be seen", shouldTest.isEmpty());
583            }
584        });
585        checkForMainThreadException();
586    }
587
588    class WrappedGridLayoutManager extends GridLayoutManager {
589
590        CountDownLatch mLayoutLatch;
591
592        List<Callback> mCallbacks = new ArrayList<Callback>();
593
594        public WrappedGridLayoutManager(Context context, int spanCount) {
595            super(context, spanCount);
596        }
597
598        public WrappedGridLayoutManager(Context context, int spanCount, int orientation,
599                boolean reverseLayout) {
600            super(context, spanCount, orientation, reverseLayout);
601        }
602
603        @Override
604        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
605            try {
606                for (Callback callback : mCallbacks) {
607                    callback.onBeforeLayout(recycler, state);
608                }
609                super.onLayoutChildren(recycler, state);
610                for (Callback callback : mCallbacks) {
611                    callback.onAfterLayout(recycler, state);
612                }
613            } catch (Throwable t) {
614                postExceptionToInstrumentation(t);
615            }
616            mLayoutLatch.countDown();
617        }
618
619        public void expectLayout(int layoutCount) {
620            mLayoutLatch = new CountDownLatch(layoutCount);
621        }
622
623        public void waitForLayout(int seconds) throws InterruptedException {
624            mLayoutLatch.await(seconds, SECONDS);
625        }
626    }
627
628    class Config {
629
630        int mSpanCount;
631        int mOrientation = GridLayoutManager.VERTICAL;
632        int mItemCount = 1000;
633        boolean mReverseLayout = false;
634
635        Config(int spanCount, int itemCount) {
636            mSpanCount = spanCount;
637            mItemCount = itemCount;
638        }
639
640        public Config(int spanCount, int orientation, boolean reverseLayout) {
641            mSpanCount = spanCount;
642            mOrientation = orientation;
643            mReverseLayout = reverseLayout;
644        }
645
646        Config orientation(int orientation) {
647            mOrientation = orientation;
648            return this;
649        }
650
651        @Override
652        public String toString() {
653            return "Config{" +
654                    "mSpanCount=" + mSpanCount +
655                    ", mOrientation=" + (mOrientation == GridLayoutManager.HORIZONTAL ? "h" : "v") +
656                    ", mItemCount=" + mItemCount +
657                    ", mReverseLayout=" + mReverseLayout +
658                    '}';
659        }
660    }
661
662    class GridTestAdapter extends TestAdapter {
663
664        Set<Integer> mFullSpanItems = new HashSet<Integer>();
665
666        GridTestAdapter(int count) {
667            super(count);
668        }
669
670        void setFullSpan(int... items) {
671            for (int i : items) {
672                mFullSpanItems.add(i);
673            }
674        }
675
676        void assignSpanSizeLookup(final GridLayoutManager glm) {
677            glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
678                @Override
679                public int getSpanSize(int position) {
680                    return mFullSpanItems.contains(position) ? glm.getSpanCount() : 1;
681                }
682            });
683        }
684    }
685
686    class Callback {
687
688        public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
689        }
690
691        public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
692        }
693    }
694}
695