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
17
18package android.support.v7.widget;
19
20
21import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
22import static android.support.v7.widget.StaggeredGridLayoutManager
23        .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 android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams;
27
28import static org.junit.Assert.assertEquals;
29import static org.junit.Assert.assertFalse;
30import static org.junit.Assert.assertNotNull;
31import static org.junit.Assert.assertNull;
32import static org.junit.Assert.assertSame;
33import static org.junit.Assert.assertThat;
34import static org.junit.Assert.assertTrue;
35
36import android.graphics.Color;
37import android.graphics.Rect;
38import android.graphics.drawable.ColorDrawable;
39import android.graphics.drawable.StateListDrawable;
40import android.os.Parcel;
41import android.os.Parcelable;
42import android.support.v4.view.AccessibilityDelegateCompat;
43import android.support.v4.view.accessibility.AccessibilityEventCompat;
44import android.support.v4.view.accessibility.AccessibilityRecordCompat;
45import android.test.suitebuilder.annotation.MediumTest;
46import android.text.TextUtils;
47import android.util.Log;
48import android.util.StateSet;
49import android.view.View;
50import android.view.ViewGroup;
51import android.view.accessibility.AccessibilityEvent;
52import android.widget.EditText;
53import android.widget.FrameLayout;
54
55import org.hamcrest.CoreMatchers;
56import org.hamcrest.MatcherAssert;
57import org.junit.Test;
58
59import java.util.HashMap;
60import java.util.Map;
61import java.util.UUID;
62
63
64@MediumTest
65public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManagerTest {
66    @Test
67    public void forceLayoutOnDetach() throws Throwable {
68        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
69        waitFirstLayout();
70        assertFalse("test sanity", mRecyclerView.isLayoutRequested());
71        runTestOnUiThread(new Runnable() {
72            @Override
73            public void run() {
74                mLayoutManager.onDetachedFromWindow(mRecyclerView, mRecyclerView.mRecycler);
75            }
76        });
77        assertTrue(mRecyclerView.isLayoutRequested());
78    }
79    @Test
80    public void areAllStartsTheSame() throws Throwable {
81        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300));
82        waitFirstLayout();
83        smoothScrollToPosition(100);
84        mLayoutManager.expectLayouts(1);
85        mAdapter.deleteAndNotify(0, 2);
86        mLayoutManager.waitForLayout(2);
87        smoothScrollToPosition(0);
88        assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual());
89    }
90
91    @Test
92    public void areAllEndsTheSame() throws Throwable {
93        setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300));
94        waitFirstLayout();
95        smoothScrollToPosition(100);
96        mLayoutManager.expectLayouts(1);
97        mAdapter.deleteAndNotify(0, 2);
98        mLayoutManager.waitForLayout(2);
99        smoothScrollToPosition(0);
100        assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual());
101    }
102
103    @Test
104    public void getPositionsBeforeInitialization() throws Throwable {
105        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
106        int[] positions = mLayoutManager.findFirstCompletelyVisibleItemPositions(null);
107        MatcherAssert.assertThat(positions,
108                CoreMatchers.is(new int[]{RecyclerView.NO_POSITION, RecyclerView.NO_POSITION,
109                        RecyclerView.NO_POSITION}));
110    }
111
112    @Test
113    public void findLastInUnevenDistribution() throws Throwable {
114        setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
115                .itemCount(5));
116        mAdapter.mOnBindCallback = new OnBindCallback() {
117            @Override
118            void onBoundItem(TestViewHolder vh, int position) {
119                LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
120                if (position == 1) {
121                    lp.height = mRecyclerView.getHeight() - 10;
122                } else {
123                    lp.height = 5;
124                }
125                vh.itemView.setMinimumHeight(0);
126            }
127        };
128        waitFirstLayout();
129        int[] into = new int[2];
130        mLayoutManager.findFirstCompletelyVisibleItemPositions(into);
131        assertEquals("first completely visible item from span 0 should be 0", 0, into[0]);
132        assertEquals("first completely visible item from span 1 should be 1", 1, into[1]);
133        mLayoutManager.findLastCompletelyVisibleItemPositions(into);
134        assertEquals("last completely visible item from span 0 should be 4", 4, into[0]);
135        assertEquals("last completely visible item from span 1 should be 1", 1, into[1]);
136        assertEquals("first fully visible child should be at position",
137                0, mRecyclerView.getChildViewHolder(mLayoutManager.
138                        findFirstVisibleItemClosestToStart(true, true)).getPosition());
139        assertEquals("last fully visible child should be at position",
140                4, mRecyclerView.getChildViewHolder(mLayoutManager.
141                        findFirstVisibleItemClosestToEnd(true, true)).getPosition());
142
143        assertEquals("first visible child should be at position",
144                0, mRecyclerView.getChildViewHolder(mLayoutManager.
145                        findFirstVisibleItemClosestToStart(false, true)).getPosition());
146        assertEquals("last visible child should be at position",
147                4, mRecyclerView.getChildViewHolder(mLayoutManager.
148                        findFirstVisibleItemClosestToEnd(false, true)).getPosition());
149
150    }
151
152    @Test
153    public void customWidthInHorizontal() throws Throwable {
154        customSizeInScrollDirectionTest(
155                new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
156    }
157
158    @Test
159    public void customHeightInVertical() throws Throwable {
160        customSizeInScrollDirectionTest(
161                new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
162    }
163
164    public void customSizeInScrollDirectionTest(final Config config) throws Throwable {
165        setupByConfig(config);
166        final Map<View, Integer> sizeMap = new HashMap<View, Integer>();
167        mAdapter.mOnBindCallback = new OnBindCallback() {
168            @Override
169            void onBoundItem(TestViewHolder vh, int position) {
170                final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
171                final int size = 1 + position * 5;
172                if (config.mOrientation == HORIZONTAL) {
173                    layoutParams.width = size;
174                } else {
175                    layoutParams.height = size;
176                }
177                sizeMap.put(vh.itemView, size);
178                if (position == 3) {
179                    getLp(vh.itemView).setFullSpan(true);
180                }
181            }
182
183            @Override
184            boolean assignRandomSize() {
185                return false;
186            }
187        };
188        waitFirstLayout();
189        assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 0);
190        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
191            View child = mRecyclerView.getChildAt(i);
192            final int size = config.mOrientation == HORIZONTAL ? child.getWidth()
193                    : child.getHeight();
194            assertEquals("child " + i + " should have the size specified in its layout params",
195                    sizeMap.get(child).intValue(), size);
196        }
197        checkForMainThreadException();
198    }
199
200    @Test
201    public void gapHandlingWhenItemMovesToTop() throws Throwable {
202        gapHandlingWhenItemMovesToTopTest();
203    }
204
205    @Test
206    public void gapHandlingWhenItemMovesToTopWithFullSpan() throws Throwable {
207        gapHandlingWhenItemMovesToTopTest(0);
208    }
209
210    @Test
211    public void gapHandlingWhenItemMovesToTopWithFullSpan2() throws Throwable {
212        gapHandlingWhenItemMovesToTopTest(1);
213    }
214
215    public void gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices) throws Throwable {
216        Config config = new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
217        config.itemCount(3);
218        setupByConfig(config);
219        mAdapter.mOnBindCallback = new OnBindCallback() {
220            @Override
221            void onBoundItem(TestViewHolder vh, int position) {
222            }
223
224            @Override
225            boolean assignRandomSize() {
226                return false;
227            }
228        };
229        for (int i : fullSpanIndices) {
230            mAdapter.mFullSpanItems.add(i);
231        }
232        waitFirstLayout();
233        mLayoutManager.expectLayouts(1);
234        mAdapter.moveItem(1, 0, true);
235        mLayoutManager.waitForLayout(2);
236        final Map<Item, Rect> desiredPositions = mLayoutManager.collectChildCoordinates();
237        // move back.
238        mLayoutManager.expectLayouts(1);
239        mAdapter.moveItem(0, 1, true);
240        mLayoutManager.waitForLayout(2);
241        mLayoutManager.expectLayouts(2);
242        mAdapter.moveAndNotify(1, 0);
243        mLayoutManager.waitForLayout(2);
244        Thread.sleep(1000);
245        getInstrumentation().waitForIdleSync();
246        checkForMainThreadException();
247        // item should be positioned properly
248        assertRectSetsEqual("final position after a move", desiredPositions,
249                mLayoutManager.collectChildCoordinates());
250
251    }
252
253    @Test
254    public void focusSearchFailureUp() throws Throwable {
255        focusSearchFailure(false);
256    }
257
258    @Test
259    public void focusSearchFailureDown() throws Throwable {
260        focusSearchFailure(true);
261    }
262
263    @Test
264    public void focusSearchFailureFromSubChild() throws Throwable {
265        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
266                new GridTestAdapter(1000, VERTICAL) {
267
268                    @Override
269                    public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
270                        FrameLayout fl = new FrameLayout(parent.getContext());
271                        EditText editText = new EditText(parent.getContext());
272                        fl.addView(editText);
273                        editText.setEllipsize(TextUtils.TruncateAt.END);
274                        return new TestViewHolder(fl);
275                    }
276
277                    @Override
278                    public void onBindViewHolder(TestViewHolder holder, int position) {
279                        Item item = mItems.get(position);
280                        holder.mBoundItem = item;
281                        ((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText(
282                                item.mText + " (" + item.mId + ")");
283                    }
284                });
285        waitFirstLayout();
286        ViewGroup lastChild = (ViewGroup) mRecyclerView.getChildAt(
287                mRecyclerView.getChildCount() - 1);
288        RecyclerView.ViewHolder lastViewHolder = mRecyclerView.getChildViewHolder(lastChild);
289        View subChildToFocus = lastChild.getChildAt(0);
290        requestFocus(subChildToFocus, true);
291        assertThat("test sanity", subChildToFocus.isFocused(), CoreMatchers.is(true));
292        focusSearch(subChildToFocus, View.FOCUS_FORWARD);
293        waitForIdleScroll(mRecyclerView);
294        checkForMainThreadException();
295        View focusedChild = mRecyclerView.getFocusedChild();
296        if (focusedChild == subChildToFocus.getParent()) {
297            focusSearch(focusedChild, View.FOCUS_FORWARD);
298            waitForIdleScroll(mRecyclerView);
299            focusedChild = mRecyclerView.getFocusedChild();
300        }
301        RecyclerView.ViewHolder containingViewHolder = mRecyclerView.findContainingViewHolder(
302                focusedChild);
303        assertTrue("new focused view should have a larger position "
304                        + lastViewHolder.getAdapterPosition() + " vs "
305                        + containingViewHolder.getAdapterPosition(),
306                lastViewHolder.getAdapterPosition() < containingViewHolder.getAdapterPosition());
307    }
308
309    public void focusSearchFailure(boolean scrollDown) throws Throwable {
310        int focusDir = scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP;
311        setupByConfig(new Config(VERTICAL, !scrollDown, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
312                , new GridTestAdapter(31, 1) {
313                    RecyclerView mAttachedRv;
314
315                    @Override
316                    public TestViewHolder onCreateViewHolder(ViewGroup parent,
317                            int viewType) {
318                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
319                        testViewHolder.itemView.setFocusable(true);
320                        testViewHolder.itemView.setFocusableInTouchMode(true);
321                        // Good to have colors for debugging
322                        StateListDrawable stl = new StateListDrawable();
323                        stl.addState(new int[]{android.R.attr.state_focused},
324                                new ColorDrawable(Color.RED));
325                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
326                        testViewHolder.itemView.setBackground(stl);
327                        return testViewHolder;
328                    }
329
330                    @Override
331                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
332                        mAttachedRv = recyclerView;
333                    }
334
335                    @Override
336                    public void onBindViewHolder(TestViewHolder holder,
337                            int position) {
338                        super.onBindViewHolder(holder, position);
339                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3);
340                    }
341                });
342        /**
343         * 0  1  2
344         * 3  4  5
345         * 6  7  8
346         * 9  10 11
347         * 12 13 14
348         * 15 16 17
349         * 18 18 18
350         * 19
351         * 20 20 20
352         * 21 22
353         * 23 23 23
354         * 24 25 26
355         * 27 28 29
356         * 30
357         */
358        mAdapter.mFullSpanItems.add(18);
359        mAdapter.mFullSpanItems.add(20);
360        mAdapter.mFullSpanItems.add(23);
361        waitFirstLayout();
362        View viewToFocus = mRecyclerView.findViewHolderForAdapterPosition(1).itemView;
363        assertTrue(requestFocus(viewToFocus, true));
364        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
365        int pos = 1;
366        View focusedView = viewToFocus;
367        while (pos < 16) {
368            focusSearchAndWaitForScroll(focusedView, focusDir);
369            focusedView = mRecyclerView.getFocusedChild();
370            assertEquals(pos + 3,
371                    mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
372            pos += 3;
373        }
374        for (int i : new int[]{18, 19, 20, 21, 23, 24}) {
375            focusSearchAndWaitForScroll(focusedView, focusDir);
376            focusedView = mRecyclerView.getFocusedChild();
377            assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
378        }
379        // now move right
380        focusSearch(focusedView, View.FOCUS_RIGHT);
381        waitForIdleScroll(mRecyclerView);
382        focusedView = mRecyclerView.getFocusedChild();
383        assertEquals(25, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
384        for (int i : new int[]{28, 30}) {
385            focusSearchAndWaitForScroll(focusedView, focusDir);
386            focusedView = mRecyclerView.getFocusedChild();
387            assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
388        }
389    }
390
391    private void focusSearchAndWaitForScroll(View focused, int dir) throws Throwable {
392        focusSearch(focused, dir);
393        waitForIdleScroll(mRecyclerView);
394    }
395
396
397    @Test
398    public void scrollToPositionWithPredictive() throws Throwable {
399        scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
400        removeRecyclerView();
401        scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
402                LinearLayoutManager.INVALID_OFFSET);
403        removeRecyclerView();
404        scrollToPositionWithPredictive(9, 20);
405        removeRecyclerView();
406        scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
407
408    }
409
410    public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
411            throws Throwable {
412        setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL,
413                false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE));
414        waitFirstLayout();
415        mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
416            @Override
417            void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
418                RecyclerView rv = mLayoutManager.mRecyclerView;
419                if (state.isPreLayout()) {
420                    assertEquals("pending scroll position should still be pending",
421                            scrollPosition, mLayoutManager.mPendingScrollPosition);
422                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
423                        assertEquals("pending scroll position offset should still be pending",
424                                scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
425                    }
426                } else {
427                    RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition);
428                    assertNotNull("scroll to position should work", vh);
429                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
430                        assertEquals("scroll offset should be applied properly",
431                                mLayoutManager.getPaddingTop() + scrollOffset
432                                        + ((RecyclerView.LayoutParams) vh.itemView
433                                        .getLayoutParams()).topMargin,
434                                mLayoutManager.getDecoratedTop(vh.itemView));
435                    }
436                }
437            }
438        };
439        mLayoutManager.expectLayouts(2);
440        runTestOnUiThread(new Runnable() {
441            @Override
442            public void run() {
443                try {
444                    mAdapter.addAndNotify(0, 1);
445                    if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
446                        mLayoutManager.scrollToPosition(scrollPosition);
447                    } else {
448                        mLayoutManager.scrollToPositionWithOffset(scrollPosition,
449                                scrollOffset);
450                    }
451
452                } catch (Throwable throwable) {
453                    throwable.printStackTrace();
454                }
455
456            }
457        });
458        mLayoutManager.waitForLayout(2);
459        checkForMainThreadException();
460    }
461
462    @Test
463    public void moveGapHandling() throws Throwable {
464        Config config = new Config().spanCount(2).itemCount(40);
465        setupByConfig(config);
466        waitFirstLayout();
467        mLayoutManager.expectLayouts(2);
468        mAdapter.moveAndNotify(4, 1);
469        mLayoutManager.waitForLayout(2);
470        assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix());
471    }
472
473    @Test
474    public void updateAfterFullSpan() throws Throwable {
475        updateAfterFullSpanGapHandlingTest(0);
476    }
477
478    @Test
479    public void updateAfterFullSpan2() throws Throwable {
480        updateAfterFullSpanGapHandlingTest(20);
481    }
482
483    @Test
484    public void temporaryGapHandling() throws Throwable {
485        int fullSpanIndex = 200;
486        setupByConfig(new Config().spanCount(2).itemCount(500));
487        mAdapter.mFullSpanItems.add(fullSpanIndex);
488        waitFirstLayout();
489        smoothScrollToPosition(fullSpanIndex + 200);// go far away
490        assertNull("test sanity. full span item should not be visible",
491                mRecyclerView.findViewHolderForAdapterPosition(fullSpanIndex));
492        mLayoutManager.expectLayouts(1);
493        mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
494        mLayoutManager.waitForLayout(1);
495        smoothScrollToPosition(0);
496        mLayoutManager.expectLayouts(1);
497        smoothScrollToPosition(fullSpanIndex + 2 * (AVG_ITEM_PER_VIEW - 1));
498        String log = mLayoutManager.layoutToString("post gap");
499        mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a "
500                + "relayout " + log, 2);
501        View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
502        assertNotNull("full span item should be there:\n" + log, fullSpan);
503        View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
504        assertNotNull("next view should be there\n" + log, view1);
505        View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
506        assertNotNull("+2 view should be there\n" + log, view2);
507
508        LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
509        LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
510        assertEquals("view 1 span index", 0, lp1.getSpanIndex());
511        assertEquals("view 2 span index", 1, lp2.getSpanIndex());
512        assertEquals("no gap between span and view 1",
513                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
514                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
515        assertEquals("no gap between span and view 2",
516                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
517                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
518    }
519
520    public void updateAfterFullSpanGapHandlingTest(int fullSpanIndex) throws Throwable {
521        setupByConfig(new Config().spanCount(2).itemCount(100));
522        mAdapter.mFullSpanItems.add(fullSpanIndex);
523        waitFirstLayout();
524        smoothScrollToPosition(fullSpanIndex + 30);
525        mLayoutManager.expectLayouts(1);
526        mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
527        mLayoutManager.waitForLayout(1);
528        smoothScrollToPosition(fullSpanIndex);
529        // give it some time to fix the gap
530        Thread.sleep(500);
531        View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
532
533        View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
534        View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
535
536        LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
537        LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
538        assertEquals("view 1 span index", 0, lp1.getSpanIndex());
539        assertEquals("view 2 span index", 1, lp2.getSpanIndex());
540        assertEquals("no gap between span and view 1",
541                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
542                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
543        assertEquals("no gap between span and view 2",
544                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
545                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
546    }
547
548    @Test
549    public void innerGapHandling() throws Throwable {
550        innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
551        innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
552    }
553
554    public void innerGapHandlingTest(int strategy) throws Throwable {
555        Config config = new Config().spanCount(3).itemCount(500);
556        setupByConfig(config);
557        mLayoutManager.setGapStrategy(strategy);
558        mAdapter.mFullSpanItems.add(100);
559        mAdapter.mFullSpanItems.add(104);
560        mAdapter.mViewsHaveEqualSize = true;
561        mAdapter.mOnBindCallback = new OnBindCallback() {
562            @Override
563            void onBoundItem(TestViewHolder vh, int position) {
564
565            }
566
567            @Override
568            void onCreatedViewHolder(TestViewHolder vh) {
569                super.onCreatedViewHolder(vh);
570                //make sure we have enough views
571                mAdapter.mSizeReference = mRecyclerView.getHeight() / 5;
572            }
573        };
574        waitFirstLayout();
575        mLayoutManager.expectLayouts(1);
576        scrollToPosition(400);
577        mLayoutManager.waitForLayout(2);
578        View view400 = mLayoutManager.findViewByPosition(400);
579        assertNotNull("test sanity, scrollToPos should succeed", view400);
580        assertTrue("test sanity, view should be visible top",
581                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view400) >=
582                        mLayoutManager.mPrimaryOrientation.getStartAfterPadding());
583        assertTrue("test sanity, view should be visible bottom",
584                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(view400) <=
585                        mLayoutManager.mPrimaryOrientation.getEndAfterPadding());
586        mLayoutManager.expectLayouts(2);
587        mAdapter.addAndNotify(101, 1);
588        mLayoutManager.waitForLayout(2);
589        checkForMainThreadException();
590        if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
591            mLayoutManager.expectLayouts(1);
592        }
593        // state
594        // now smooth scroll to 99 to trigger a layout around 100
595        mLayoutManager.validateChildren();
596        smoothScrollToPosition(99);
597        switch (strategy) {
598            case GAP_HANDLING_NONE:
599                assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0},
600                        new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2},
601                        new int[]{105, 0});
602                break;
603            case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
604                mLayoutManager.waitForLayout(2);
605                assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0},
606                        new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0});
607                break;
608        }
609
610    }
611
612    @Test
613    public void fullSizeSpans() throws Throwable {
614        Config config = new Config().spanCount(5).itemCount(30);
615        setupByConfig(config);
616        mAdapter.mFullSpanItems.add(3);
617        waitFirstLayout();
618        assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2},
619                new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2},
620                new int[]{7, 3}, new int[]{8, 4});
621    }
622
623    void assertSpans(String msg, int[]... childSpanTuples) {
624        msg = msg + mLayoutManager.layoutToString("\n\n");
625        for (int i = 0; i < childSpanTuples.length; i++) {
626            assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
627        }
628    }
629
630    void assertSpan(String msg, int childPosition, int expectedSpan) {
631        View view = mLayoutManager.findViewByPosition(childPosition);
632        assertNotNull(msg + " view at position " + childPosition + " should exists", view);
633        assertEquals(msg + "[child:" + childPosition + "]", expectedSpan,
634                getLp(view).mSpan.mIndex);
635    }
636
637    @Test
638    public void partialSpanInvalidation() throws Throwable {
639        Config config = new Config().spanCount(5).itemCount(100);
640        setupByConfig(config);
641        for (int i = 20; i < mAdapter.getItemCount(); i += 20) {
642            mAdapter.mFullSpanItems.add(i);
643        }
644        waitFirstLayout();
645        smoothScrollToPosition(50);
646        int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30];
647        mAdapter.changeAndNotify(15, 2);
648        Thread.sleep(200);
649        assertEquals("Invalidation should happen within full span item boundaries", prevSpanId,
650                mLayoutManager.mLazySpanLookup.mData[30]);
651        assertEquals("item in invalidated range should have clear span id",
652                LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
653        smoothScrollToPosition(85);
654        int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85);
655        mAdapter.deleteAndNotify(55, 2);
656        Thread.sleep(200);
657        assertEquals("item in invalidated range should have clear span id",
658                LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
659        int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83);
660        assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans,
661                newSpans, 0, 0, newSpans.length);
662    }
663
664    // Same as Arrays.copyOfRange but for API 7
665    private int[] copyOfRange(int[] original, int from, int to) {
666        int newLength = to - from;
667        if (newLength < 0) {
668            throw new IllegalArgumentException(from + " > " + to);
669        }
670        int[] copy = new int[newLength];
671        System.arraycopy(original, from, copy, 0,
672                Math.min(original.length - from, newLength));
673        return copy;
674    }
675
676    @Test
677    public void spanReassignmentsOnItemChange() throws Throwable {
678        Config config = new Config().spanCount(5);
679        setupByConfig(config);
680        waitFirstLayout();
681        smoothScrollToPosition(mAdapter.getItemCount() / 2);
682        final int changePosition = mAdapter.getItemCount() / 4;
683        mLayoutManager.expectLayouts(1);
684        mAdapter.changeAndNotify(changePosition, 1);
685        mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated",
686                1);
687        // delete an item before visible area
688        int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2;
689        assertTrue("test sanity", deletedPosition >= 0);
690        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
691        if (DEBUG) {
692            Log.d(TAG, "before:");
693            for (Map.Entry<Item, Rect> entry : before.entrySet()) {
694                Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
695            }
696        }
697        mLayoutManager.expectLayouts(1);
698        mAdapter.deleteAndNotify(deletedPosition, 1);
699        mLayoutManager.waitForLayout(2);
700        assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it "
701                        + "should not affect the layout if it is not visible", before,
702                mLayoutManager.collectChildCoordinates()
703        );
704        deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2));
705        mLayoutManager.expectLayouts(1);
706        mAdapter.deleteAndNotify(deletedPosition, 1);
707        mLayoutManager.waitForLayout(2);
708        assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the "
709                + "layout", before, mLayoutManager.collectChildCoordinates());
710    }
711
712    void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2,
713            int length) {
714        for (int i = 0; i < length; i++) {
715            assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i],
716                    set2[start2 + i]);
717        }
718    }
719
720    @Test
721    public void spanCountChangeOnRestoreSavedState() throws Throwable {
722        Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE);
723        setupByConfig(config);
724        waitFirstLayout();
725
726        int beforeChildCount = mLayoutManager.getChildCount();
727        Parcelable savedState = mRecyclerView.onSaveInstanceState();
728        // we append a suffix to the parcelable to test out of bounds
729        String parcelSuffix = UUID.randomUUID().toString();
730        Parcel parcel = Parcel.obtain();
731        savedState.writeToParcel(parcel, 0);
732        parcel.writeString(parcelSuffix);
733        removeRecyclerView();
734        // reset for reading
735        parcel.setDataPosition(0);
736        // re-create
737        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
738        removeRecyclerView();
739
740        RecyclerView restored = new RecyclerView(getActivity());
741        mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
742        mLayoutManager.setReverseLayout(config.mReverseLayout);
743        mLayoutManager.setGapStrategy(config.mGapStrategy);
744        restored.setLayoutManager(mLayoutManager);
745        // use the same adapter for Rect matching
746        restored.setAdapter(mAdapter);
747        restored.onRestoreInstanceState(savedState);
748        mLayoutManager.setSpanCount(1);
749        mLayoutManager.expectLayouts(1);
750        setRecyclerView(restored);
751        mLayoutManager.waitForLayout(2);
752        assertEquals("on saved state, reverse layout should be preserved",
753                config.mReverseLayout, mLayoutManager.getReverseLayout());
754        assertEquals("on saved state, orientation should be preserved",
755                config.mOrientation, mLayoutManager.getOrientation());
756        assertEquals("after setting new span count, layout manager should keep new value",
757                1, mLayoutManager.getSpanCount());
758        assertEquals("on saved state, gap strategy should be preserved",
759                config.mGapStrategy, mLayoutManager.getGapStrategy());
760        assertTrue("when span count is dramatically changed after restore, # of child views "
761                + "should change", beforeChildCount > mLayoutManager.getChildCount());
762        // make sure LLM can layout all children. is some span info is leaked, this would crash
763        smoothScrollToPosition(mAdapter.getItemCount() - 1);
764    }
765
766    @Test
767    public void scrollAndClear() throws Throwable {
768        setupByConfig(new Config());
769        waitFirstLayout();
770
771        assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
772
773        mLayoutManager.expectLayouts(1);
774        runTestOnUiThread(new Runnable() {
775            @Override
776            public void run() {
777                mLayoutManager.scrollToPositionWithOffset(1, 0);
778                mAdapter.clearOnUIThread();
779            }
780        });
781        mLayoutManager.waitForLayout(2);
782
783        assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
784    }
785
786    @Test
787    public void accessibilityPositions() throws Throwable {
788        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
789        waitFirstLayout();
790        final AccessibilityDelegateCompat delegateCompat = mRecyclerView
791                .getCompatAccessibilityDelegate();
792        final AccessibilityEvent event = AccessibilityEvent.obtain();
793        runTestOnUiThread(new Runnable() {
794            @Override
795            public void run() {
796                delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
797            }
798        });
799        final AccessibilityRecordCompat record = AccessibilityEventCompat
800                .asRecord(event);
801        final int start = mRecyclerView
802                .getChildLayoutPosition(
803                        mLayoutManager.findFirstVisibleItemClosestToStart(false, true));
804        final int end = mRecyclerView
805                .getChildLayoutPosition(
806                        mLayoutManager.findFirstVisibleItemClosestToEnd(false, true));
807        assertEquals("first item position should match",
808                Math.min(start, end), record.getFromIndex());
809        assertEquals("last item position should match",
810                Math.max(start, end), record.getToIndex());
811
812    }
813}
814