GridLayoutManagerTest.java revision a910619e83d0052e1d81aa5fe532821a2f99d76c
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.HashSet;
30import java.util.List;
31import java.util.Set;
32import java.util.concurrent.CountDownLatch;
33
34import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
35import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
36import static java.util.concurrent.TimeUnit.SECONDS;
37
38public class GridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
39
40    static final String TAG = "GridLayoutManagerTest";
41
42    static final boolean DEBUG = false;
43
44    WrappedGridLayoutManager mGlm;
45
46    GridTestAdapter mAdapter;
47
48    final List<Config> mBaseVariations = new ArrayList<Config>();
49
50    @Override
51    protected void setUp() throws Exception {
52        super.setUp();
53        for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
54            for (boolean reverseLayout : new boolean[]{false, true}) {
55                for (int spanCount : new int[]{1, 3, 4}) {
56                    mBaseVariations.add(new Config(spanCount, orientation, reverseLayout));
57                }
58            }
59        }
60    }
61
62    public RecyclerView setupBasic(Config config) throws Throwable {
63        return setupBasic(config, new GridTestAdapter(config.mItemCount));
64    }
65
66    public RecyclerView setupBasic(Config config, GridTestAdapter testAdapter) throws Throwable {
67        RecyclerView recyclerView = new RecyclerView(getActivity());
68        mAdapter = testAdapter;
69        mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
70                config.mReverseLayout);
71        mAdapter.assignSpanSizeLookup(mGlm);
72        recyclerView.setAdapter(mAdapter);
73        recyclerView.setLayoutManager(mGlm);
74        return recyclerView;
75    }
76
77    public void waitForFirstLayout(RecyclerView recyclerView) throws Throwable {
78        mGlm.expectLayout(1);
79        setRecyclerView(recyclerView);
80        mGlm.waitForLayout(2);
81    }
82
83    public void testLayoutParams() throws Throwable {
84        layoutParamsTest(GridLayoutManager.HORIZONTAL);
85        removeRecyclerView();
86        layoutParamsTest(GridLayoutManager.VERTICAL);
87    }
88
89    public void testHorizontalAccessibilitySpanIndices() throws Throwable {
90        accessibilitySpanIndicesTest(HORIZONTAL);
91    }
92
93    public void testVerticalAccessibilitySpanIndices() throws Throwable {
94        accessibilitySpanIndicesTest(VERTICAL);
95    }
96
97    public void accessibilitySpanIndicesTest(int orientation) throws Throwable {
98        final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false));
99        waitForFirstLayout(recyclerView);
100        final AccessibilityDelegateCompat delegateCompat = mRecyclerView
101                .getCompatAccessibilityDelegate().getItemDelegate();
102        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
103        final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2);
104        final int position = recyclerView.getChildPosition(chosen);
105        runTestOnUiThread(new Runnable() {
106            @Override
107            public void run() {
108                delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info);
109            }
110        });
111        GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
112        AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info
113                .getCollectionItemInfo();
114        assertNotNull(itemInfo);
115        assertEquals("result should have span group position",
116                ssl.getSpanGroupIndex(position, mGlm.getSpanCount()),
117                orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex());
118        assertEquals("result should have span index",
119                ssl.getSpanIndex(position, mGlm.getSpanCount()),
120                orientation == HORIZONTAL ? itemInfo.getRowIndex() :  itemInfo.getColumnIndex());
121        assertEquals("result should have span size",
122                ssl.getSpanSize(position),
123                orientation == HORIZONTAL ? itemInfo.getRowSpan() :  itemInfo.getColumnSpan());
124    }
125
126    public void layoutParamsTest(final int orientation) throws Throwable {
127        final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation),
128                new GridTestAdapter(100) {
129                    @Override
130                    public void onBindViewHolder(TestViewHolder holder,
131                            int position) {
132                        super.onBindViewHolder(holder, position);
133                        ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
134                        GridLayoutManager.LayoutParams glp = null;
135                        if (lp == null) {
136                            glp = (GridLayoutManager.LayoutParams) mGlm
137                                    .generateDefaultLayoutParams();
138                        } else {
139                            glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp);
140                        }
141                        int val = 0;
142                        switch (position % 5) {
143                            case 0:
144                                val = 10;
145                                break;
146                            case 1:
147                                val = 30;
148                                break;
149                            case 2:
150                                val = GridLayoutManager.LayoutParams.WRAP_CONTENT;
151                                break;
152                            case 3:
153                                val = GridLayoutManager.LayoutParams.FILL_PARENT;
154                                break;
155                            case 4:
156                                val = 200;
157                                break;
158                        }
159                        if (orientation == GridLayoutManager.VERTICAL) {
160                            glp.height = val;
161                        } else {
162                            glp.width = val;
163                        }
164                        holder.itemView.setLayoutParams(glp);
165                    }
166                });
167        waitForFirstLayout(rv);
168        final OrientationHelper helper = mGlm.mOrientationHelper;
169        final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2)));
170        assertEquals(10, helper.getDecoratedMeasurement(mGlm.findViewByPosition(0)));
171        assertEquals(30, helper.getDecoratedMeasurement(mGlm.findViewByPosition(1)));
172        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0)));
173        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1)));
174        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2)));
175
176        final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3)));
177        assertEquals(200, helper.getDecoratedMeasurement(mGlm.findViewByPosition(4)));
178        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3)));
179        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4)));
180        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5)));
181    }
182
183    private int getSize(View view) {
184        if (mGlm.getOrientation() == GridLayoutManager.HORIZONTAL) {
185            return view.getWidth();
186        }
187        return view.getHeight();
188    }
189
190    public void testAnchorUpdate() throws InterruptedException {
191        GridLayoutManager glm = new GridLayoutManager(getActivity(), 11);
192        final GridLayoutManager.SpanSizeLookup spanSizeLookup
193                = new GridLayoutManager.SpanSizeLookup() {
194            @Override
195            public int getSpanSize(int position) {
196                if (position > 200) {
197                    return 100;
198                }
199                if (position > 20) {
200                    return 2;
201                }
202                return 1;
203            }
204        };
205        glm.setSpanSizeLookup(spanSizeLookup);
206        glm.mAnchorInfo.mPosition = 11;
207        RecyclerView.State state = new RecyclerView.State();
208        state.mItemCount = 1000;
209        glm.onAnchorReady(state, glm.mAnchorInfo);
210        assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition);
211
212        glm.mAnchorInfo.mPosition = 13;
213        glm.onAnchorReady(state, glm.mAnchorInfo);
214        assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition);
215
216        glm.mAnchorInfo.mPosition = 23;
217        glm.onAnchorReady(state, glm.mAnchorInfo);
218        assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition);
219
220        glm.mAnchorInfo.mPosition = 35;
221        glm.onAnchorReady(state, glm.mAnchorInfo);
222        assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition);
223    }
224
225    public void testSpanLookup() {
226        spanLookupTest(false);
227    }
228
229    public void testSpanLookupWithCache() {
230        spanLookupTest(true);
231    }
232
233    public void testSpanLookupCache() {
234        final GridLayoutManager.SpanSizeLookup ssl
235                = new GridLayoutManager.SpanSizeLookup() {
236            @Override
237            public int getSpanSize(int position) {
238                if (position > 6) {
239                    return 2;
240                }
241                return 1;
242            }
243        };
244        ssl.setSpanIndexCacheEnabled(true);
245        assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(2));
246        ssl.getCachedSpanIndex(4, 5);
247        assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(3));
248        // this should not happen and if happens, it is better to return -1
249        assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
250        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(5));
251        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(100));
252        ssl.getCachedSpanIndex(6, 5);
253        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
254        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(6));
255        assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
256        ssl.getCachedSpanIndex(12, 5);
257        assertEquals("reference child before", 12, ssl.findReferenceIndexFromCache(13));
258        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(12));
259        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
260        for (int i = 0; i < 6; i++) {
261            ssl.getCachedSpanIndex(i, 5);
262        }
263
264        for (int i = 1; i < 7; i++) {
265            assertEquals("reference child right before " + i, i - 1,
266                    ssl.findReferenceIndexFromCache(i));
267        }
268        assertEquals("reference child before 0 ", -1, ssl.findReferenceIndexFromCache(0));
269    }
270
271    public void spanLookupTest(boolean enableCache) {
272        final GridLayoutManager.SpanSizeLookup ssl
273                = new GridLayoutManager.SpanSizeLookup() {
274            @Override
275            public int getSpanSize(int position) {
276                if (position > 200) {
277                    return 100;
278                }
279                if (position > 6) {
280                    return 2;
281                }
282                return 1;
283            }
284        };
285        ssl.setSpanIndexCacheEnabled(enableCache);
286        assertEquals(0, ssl.getCachedSpanIndex(0, 5));
287        assertEquals(4, ssl.getCachedSpanIndex(4, 5));
288        assertEquals(0, ssl.getCachedSpanIndex(5, 5));
289        assertEquals(1, ssl.getCachedSpanIndex(6, 5));
290        assertEquals(2, ssl.getCachedSpanIndex(7, 5));
291        assertEquals(2, ssl.getCachedSpanIndex(9, 5));
292        assertEquals(0, ssl.getCachedSpanIndex(8, 5));
293    }
294
295    public void testSpanGroupIndex() {
296        final GridLayoutManager.SpanSizeLookup ssl
297                = new GridLayoutManager.SpanSizeLookup() {
298            @Override
299            public int getSpanSize(int position) {
300                if (position > 200) {
301                    return 100;
302                }
303                if (position > 6) {
304                    return 2;
305                }
306                return 1;
307            }
308        };
309        assertEquals(0, ssl.getSpanGroupIndex(0, 5));
310        assertEquals(0, ssl.getSpanGroupIndex(4, 5));
311        assertEquals(1, ssl.getSpanGroupIndex(5, 5));
312        assertEquals(1, ssl.getSpanGroupIndex(6, 5));
313        assertEquals(1, ssl.getSpanGroupIndex(7, 5));
314        assertEquals(2, ssl.getSpanGroupIndex(9, 5));
315        assertEquals(2, ssl.getSpanGroupIndex(8, 5));
316    }
317
318    public void testNotifyDataSetChange() throws Throwable {
319        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
320        final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup();
321        ssl.setSpanIndexCacheEnabled(true);
322        waitForFirstLayout(recyclerView);
323        assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0);
324        final Callback callback = new Callback() {
325            @Override
326            public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
327                if (!state.isPreLayout()) {
328                    assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size());
329                }
330            }
331
332            @Override
333            public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
334                if (!state.isPreLayout()) {
335                    assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0);
336                }
337            }
338        };
339        mGlm.mCallbacks.add(callback);
340        mGlm.expectLayout(2);
341        mAdapter.deleteAndNotify(2, 3);
342        mGlm.waitForLayout(2);
343        checkForMainThreadException();
344    }
345
346    public void testScrollBackAndPreservePositions() throws Throwable {
347        for (Config config : mBaseVariations) {
348            config.mItemCount = 150;
349            scrollBackAndPreservePositionsTest(config);
350            removeRecyclerView();
351        }
352    }
353
354    public void testCacheSpanIndices() throws Throwable {
355        final RecyclerView rv = setupBasic(new Config(3, 100));
356        mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true);
357        waitForFirstLayout(rv);
358        GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
359        assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0);
360        assertEquals("item index 5 should be in span 2", 2,
361                getLp(mGlm.findViewByPosition(5)).getSpanIndex());
362        mGlm.expectLayout(2);
363        mAdapter.mFullSpanItems.add(4);
364        mAdapter.changeAndNotify(4, 1);
365        mGlm.waitForLayout(2);
366        assertEquals("item index 5 should be in span 2", 0,
367                getLp(mGlm.findViewByPosition(5)).getSpanIndex());
368    }
369
370    GridLayoutManager.LayoutParams getLp(View view) {
371        return (GridLayoutManager.LayoutParams) view.getLayoutParams();
372    }
373
374    public void scrollBackAndPreservePositionsTest(final Config config) throws Throwable {
375        final RecyclerView rv = setupBasic(config);
376        for (int i = 1; i < mAdapter.getItemCount(); i += config.mSpanCount + 2) {
377            mAdapter.setFullSpan(i);
378        }
379        waitForFirstLayout(rv);
380        final int[] globalPositions = new int[mAdapter.getItemCount()];
381        Arrays.fill(globalPositions, Integer.MIN_VALUE);
382        final int scrollStep = (mGlm.mOrientationHelper.getTotalSpace() / 20)
383                * (config.mReverseLayout ? -1 : 1);
384        final String logPrefix = config.toString();
385        final int[] globalPos = new int[1];
386        runTestOnUiThread(new Runnable() {
387            @Override
388            public void run() {
389                int globalScrollPosition = 0;
390                int visited = 0;
391                while (visited < mAdapter.getItemCount()) {
392                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
393                        View child = mRecyclerView.getChildAt(i);
394                        final int pos = mRecyclerView.getChildPosition(child);
395                        if (globalPositions[pos] != Integer.MIN_VALUE) {
396                            continue;
397                        }
398                        visited++;
399                        GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
400                                child.getLayoutParams();
401                        if (config.mReverseLayout) {
402                            globalPositions[pos] = globalScrollPosition +
403                                    mGlm.mOrientationHelper.getDecoratedEnd(child);
404                        } else {
405                            globalPositions[pos] = globalScrollPosition +
406                                    mGlm.mOrientationHelper.getDecoratedStart(child);
407                        }
408                        assertEquals(logPrefix + " span index should match",
409                                mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
410                                lp.getSpanIndex());
411                    }
412                    int scrolled = mGlm.scrollBy(scrollStep,
413                            mRecyclerView.mRecycler, mRecyclerView.mState);
414                    globalScrollPosition += scrolled;
415                    if (scrolled == 0) {
416                        assertEquals(
417                                logPrefix + " If scroll is complete, all views should be visited",
418                                visited, mAdapter.getItemCount());
419                    }
420                }
421                if (DEBUG) {
422                    Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
423                }
424                globalPos[0] = globalScrollPosition;
425            }
426        });
427        checkForMainThreadException();
428        runTestOnUiThread(new Runnable() {
429            @Override
430            public void run() {
431                int globalScrollPosition = globalPos[0];
432                // now scroll back and make sure global positions match
433                BitSet shouldTest = new BitSet(mAdapter.getItemCount());
434                shouldTest.set(0, mAdapter.getItemCount() - 1, true);
435                String assertPrefix = config
436                        + " global pos must match when scrolling in reverse for position ";
437                int scrollAmount = Integer.MAX_VALUE;
438                while (!shouldTest.isEmpty() && scrollAmount != 0) {
439                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
440                        View child = mRecyclerView.getChildAt(i);
441                        int pos = mRecyclerView.getChildPosition(child);
442                        if (!shouldTest.get(pos)) {
443                            continue;
444                        }
445                        GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
446                                child.getLayoutParams();
447                        shouldTest.clear(pos);
448                        int globalPos;
449                        if (config.mReverseLayout) {
450                            globalPos = globalScrollPosition +
451                                    mGlm.mOrientationHelper.getDecoratedEnd(child);
452                        } else {
453                            globalPos = globalScrollPosition +
454                                    mGlm.mOrientationHelper.getDecoratedStart(child);
455                        }
456                        assertEquals(assertPrefix + pos,
457                                globalPositions[pos], globalPos);
458                        assertEquals("span index should match",
459                                mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
460                                lp.getSpanIndex());
461                    }
462                    scrollAmount = mGlm.scrollBy(-scrollStep,
463                            mRecyclerView.mRecycler, mRecyclerView.mState);
464                    globalScrollPosition += scrollAmount;
465                }
466                assertTrue("all views should be seen", shouldTest.isEmpty());
467            }
468        });
469        checkForMainThreadException();
470    }
471
472    class WrappedGridLayoutManager extends GridLayoutManager {
473
474        CountDownLatch mLayoutLatch;
475
476        List<Callback> mCallbacks = new ArrayList<Callback>();
477
478        public WrappedGridLayoutManager(Context context, int spanCount) {
479            super(context, spanCount);
480        }
481
482        public WrappedGridLayoutManager(Context context, int spanCount, int orientation,
483                boolean reverseLayout) {
484            super(context, spanCount, orientation, reverseLayout);
485        }
486
487        @Override
488        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
489            try {
490                for (Callback callback : mCallbacks) {
491                    callback.onBeforeLayout(recycler, state);
492                }
493                super.onLayoutChildren(recycler, state);
494                for (Callback callback : mCallbacks) {
495                    callback.onAfterLayout(recycler, state);
496                }
497            } catch (Throwable t) {
498                postExceptionToInstrumentation(t);
499            }
500            mLayoutLatch.countDown();
501        }
502
503        public void expectLayout(int layoutCount) {
504            mLayoutLatch = new CountDownLatch(layoutCount);
505        }
506
507        public void waitForLayout(int seconds) throws InterruptedException {
508            mLayoutLatch.await(seconds, SECONDS);
509        }
510    }
511
512    class Config {
513
514        int mSpanCount;
515        int mOrientation = GridLayoutManager.VERTICAL;
516        int mItemCount = 1000;
517        boolean mReverseLayout = false;
518
519        Config(int spanCount, int itemCount) {
520            mSpanCount = spanCount;
521            mItemCount = itemCount;
522        }
523
524        public Config(int spanCount, int orientation, boolean reverseLayout) {
525            mSpanCount = spanCount;
526            mOrientation = orientation;
527            mReverseLayout = reverseLayout;
528        }
529
530        Config orientation(int orientation) {
531            mOrientation = orientation;
532            return this;
533        }
534
535        @Override
536        public String toString() {
537            return "Config{" +
538                    "mSpanCount=" + mSpanCount +
539                    ", mOrientation=" + (mOrientation == GridLayoutManager.HORIZONTAL ? "h" : "v") +
540                    ", mItemCount=" + mItemCount +
541                    ", mReverseLayout=" + mReverseLayout +
542                    '}';
543        }
544    }
545
546    class GridTestAdapter extends TestAdapter {
547
548        Set<Integer> mFullSpanItems = new HashSet<Integer>();
549
550        GridTestAdapter(int count) {
551            super(count);
552        }
553
554        void setFullSpan(int... items) {
555            for (int i : items) {
556                mFullSpanItems.add(i);
557            }
558        }
559
560        void assignSpanSizeLookup(final GridLayoutManager glm) {
561            glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
562                @Override
563                public int getSpanSize(int position) {
564                    return mFullSpanItems.contains(position) ? glm.getSpanCount() : 1;
565                }
566            });
567        }
568    }
569
570    class Callback {
571
572        public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
573        }
574
575        public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
576        }
577    }
578}
579