StaggeredGridLayoutManagerBaseConfigSetTest.java revision b31c3281d870e9abb673db239234d580dcc4feff
1/*
2 * Copyright 2018 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 androidx.recyclerview.widget;
18
19import static androidx.recyclerview.widget.LayoutState.LAYOUT_START;
20import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
21import static androidx.recyclerview.widget.StaggeredGridLayoutManager.HORIZONTAL;
22
23import static org.hamcrest.CoreMatchers.hasItem;
24import static org.hamcrest.CoreMatchers.is;
25import static org.hamcrest.CoreMatchers.not;
26import static org.hamcrest.CoreMatchers.sameInstance;
27import static org.junit.Assert.assertEquals;
28import static org.junit.Assert.assertNotNull;
29import static org.junit.Assert.assertThat;
30import static org.junit.Assert.assertTrue;
31
32import android.graphics.Rect;
33import android.os.Looper;
34import android.os.Parcel;
35import android.os.Parcelable;
36import androidx.annotation.NonNull;
37import android.support.test.filters.FlakyTest;
38import android.support.test.filters.LargeTest;
39import android.support.test.filters.Suppress;
40import android.util.Log;
41import android.view.View;
42import android.view.ViewParent;
43
44import org.junit.Test;
45import org.junit.runner.RunWith;
46import org.junit.runners.Parameterized;
47
48import java.util.Arrays;
49import java.util.BitSet;
50import java.util.List;
51import java.util.Map;
52import java.util.UUID;
53
54@RunWith(Parameterized.class)
55@LargeTest
56public class StaggeredGridLayoutManagerBaseConfigSetTest
57        extends BaseStaggeredGridLayoutManagerTest {
58
59    @Parameterized.Parameters(name = "{0}")
60    public static List<Config> getParams() {
61        return createBaseVariations();
62    }
63
64    private final Config mConfig;
65
66    public StaggeredGridLayoutManagerBaseConfigSetTest(Config config)
67            throws CloneNotSupportedException {
68        mConfig = (Config) config.clone();
69    }
70
71    @Test
72    public void rTL() throws Throwable {
73        rtlTest(false, false);
74    }
75
76    @Test
77    public void rTLChangeAfter() throws Throwable {
78        rtlTest(true, false);
79    }
80
81    @Test
82    public void rTLItemWrapContent() throws Throwable {
83        rtlTest(false, true);
84    }
85
86    @Test
87    public void rTLChangeAfterItemWrapContent() throws Throwable {
88        rtlTest(true, true);
89    }
90
91    void rtlTest(boolean changeRtlAfter, final boolean wrapContent) throws Throwable {
92        if (mConfig.mSpanCount == 1) {
93            mConfig.mSpanCount = 2;
94        }
95        String logPrefix = mConfig + ", changeRtlAfterLayout:" + changeRtlAfter;
96        setupByConfig(mConfig.itemCount(5),
97                new GridTestAdapter(mConfig.mItemCount, mConfig.mOrientation) {
98                    @Override
99                    public void onBindViewHolder(@NonNull TestViewHolder holder,
100                            int position) {
101                        super.onBindViewHolder(holder, position);
102                        if (wrapContent) {
103                            if (mOrientation == HORIZONTAL) {
104                                holder.itemView.getLayoutParams().height
105                                        = RecyclerView.LayoutParams.WRAP_CONTENT;
106                            } else {
107                                holder.itemView.getLayoutParams().width
108                                        = RecyclerView.LayoutParams.MATCH_PARENT;
109                            }
110                        }
111                    }
112                });
113        if (changeRtlAfter) {
114            waitFirstLayout();
115            mLayoutManager.expectLayouts(1);
116            mLayoutManager.setFakeRtl(true);
117            mLayoutManager.waitForLayout(2);
118        } else {
119            mLayoutManager.mFakeRTL = true;
120            waitFirstLayout();
121        }
122
123        assertEquals("view should become rtl", true, mLayoutManager.isLayoutRTL());
124        OrientationHelper helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
125        View child0 = mLayoutManager.findViewByPosition(0);
126        View child1 = mLayoutManager.findViewByPosition(mConfig.mOrientation == VERTICAL ? 1
127                : mConfig.mSpanCount);
128        assertNotNull(logPrefix + " child position 0 should be laid out", child0);
129        assertNotNull(logPrefix + " child position 0 should be laid out", child1);
130        logPrefix += " child1 pos:" + mLayoutManager.getPosition(child1);
131        if (mConfig.mOrientation == VERTICAL || !mConfig.mReverseLayout) {
132            assertTrue(logPrefix + " second child should be to the left of first child",
133                    helper.getDecoratedEnd(child0) > helper.getDecoratedEnd(child1));
134            assertEquals(logPrefix + " first child should be right aligned",
135                    helper.getDecoratedEnd(child0), helper.getEndAfterPadding());
136        } else {
137            assertTrue(logPrefix + " first child should be to the left of second child",
138                    helper.getDecoratedStart(child1) >= helper.getDecoratedStart(child0));
139            assertEquals(logPrefix + " first child should be left aligned",
140                    helper.getDecoratedStart(child0), helper.getStartAfterPadding());
141        }
142        checkForMainThreadException();
143    }
144
145    @Test
146    public void scrollBackAndPreservePositions() throws Throwable {
147        scrollBackAndPreservePositionsTest(false);
148    }
149
150    @Test
151    public void scrollBackAndPreservePositionsWithRestore() throws Throwable {
152        scrollBackAndPreservePositionsTest(true);
153    }
154
155    public void scrollBackAndPreservePositionsTest(final boolean saveRestoreInBetween)
156            throws Throwable {
157        setupByConfig(mConfig);
158        mAdapter.mOnBindCallback = new OnBindCallback() {
159            @Override
160            public void onBoundItem(TestViewHolder vh, int position) {
161                StaggeredGridLayoutManager.LayoutParams
162                        lp = (StaggeredGridLayoutManager.LayoutParams) vh.itemView
163                        .getLayoutParams();
164                lp.setFullSpan((position * 7) % (mConfig.mSpanCount + 1) == 0);
165            }
166        };
167        waitFirstLayout();
168        final int[] globalPositions = new int[mAdapter.getItemCount()];
169        Arrays.fill(globalPositions, Integer.MIN_VALUE);
170        final int scrollStep = (mLayoutManager.mPrimaryOrientation.getTotalSpace() / 10)
171                * (mConfig.mReverseLayout ? -1 : 1);
172
173        final int[] globalPos = new int[1];
174        mActivityRule.runOnUiThread(new Runnable() {
175            @Override
176            public void run() {
177                int globalScrollPosition = 0;
178                while (globalPositions[mAdapter.getItemCount() - 1] == Integer.MIN_VALUE) {
179                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
180                        View child = mRecyclerView.getChildAt(i);
181                        final int pos = mRecyclerView.getChildLayoutPosition(child);
182                        if (globalPositions[pos] != Integer.MIN_VALUE) {
183                            continue;
184                        }
185                        if (mConfig.mReverseLayout) {
186                            globalPositions[pos] = globalScrollPosition +
187                                    mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
188                        } else {
189                            globalPositions[pos] = globalScrollPosition +
190                                    mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
191                        }
192                    }
193                    globalScrollPosition += mLayoutManager.scrollBy(scrollStep,
194                            mRecyclerView.mRecycler, mRecyclerView.mState);
195                }
196                if (DEBUG) {
197                    Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
198                }
199                globalPos[0] = globalScrollPosition;
200            }
201        });
202        checkForMainThreadException();
203
204        if (saveRestoreInBetween) {
205            saveRestore(mConfig);
206        }
207
208        checkForMainThreadException();
209        mActivityRule.runOnUiThread(new Runnable() {
210            @Override
211            public void run() {
212                int globalScrollPosition = globalPos[0];
213                // now scroll back and make sure global positions match
214                BitSet shouldTest = new BitSet(mAdapter.getItemCount());
215                shouldTest.set(0, mAdapter.getItemCount() - 1, true);
216                String assertPrefix = mConfig + ", restored in between:" + saveRestoreInBetween
217                        + " global pos must match when scrolling in reverse for position ";
218                int scrollAmount = Integer.MAX_VALUE;
219                while (!shouldTest.isEmpty() && scrollAmount != 0) {
220                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
221                        View child = mRecyclerView.getChildAt(i);
222                        int pos = mRecyclerView.getChildLayoutPosition(child);
223                        if (!shouldTest.get(pos)) {
224                            continue;
225                        }
226                        shouldTest.clear(pos);
227                        int globalPos;
228                        if (mConfig.mReverseLayout) {
229                            globalPos = globalScrollPosition +
230                                    mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
231                        } else {
232                            globalPos = globalScrollPosition +
233                                    mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
234                        }
235                        assertEquals(assertPrefix + pos,
236                                globalPositions[pos], globalPos);
237                    }
238                    scrollAmount = mLayoutManager.scrollBy(-scrollStep,
239                            mRecyclerView.mRecycler, mRecyclerView.mState);
240                    globalScrollPosition += scrollAmount;
241                }
242                assertTrue("all views should be seen", shouldTest.isEmpty());
243            }
244        });
245        checkForMainThreadException();
246    }
247
248    private void saveRestore(final Config config) throws Throwable {
249        mActivityRule.runOnUiThread(new Runnable() {
250            @Override
251            public void run() {
252                try {
253                    Parcelable savedState = mRecyclerView.onSaveInstanceState();
254                    // we append a suffix to the parcelable to test out of bounds
255                    String parcelSuffix = UUID.randomUUID().toString();
256                    Parcel parcel = Parcel.obtain();
257                    savedState.writeToParcel(parcel, 0);
258                    parcel.writeString(parcelSuffix);
259                    removeRecyclerView();
260                    // reset for reading
261                    parcel.setDataPosition(0);
262                    // re-create
263                    savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
264                    RecyclerView restored = new RecyclerView(getActivity());
265                    mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
266                            config.mOrientation);
267                    mLayoutManager.setGapStrategy(config.mGapStrategy);
268                    restored.setLayoutManager(mLayoutManager);
269                    // use the same adapter for Rect matching
270                    restored.setAdapter(mAdapter);
271                    restored.onRestoreInstanceState(savedState);
272                    if (Looper.myLooper() == Looper.getMainLooper()) {
273                        mLayoutManager.expectLayouts(1);
274                        setRecyclerView(restored);
275                    } else {
276                        mLayoutManager.expectLayouts(1);
277                        setRecyclerView(restored);
278                        mLayoutManager.waitForLayout(2);
279                    }
280                } catch (Throwable t) {
281                    postExceptionToInstrumentation(t);
282                }
283            }
284        });
285        checkForMainThreadException();
286    }
287
288    @Test
289    public void getFirstLastChildrenTest() throws Throwable {
290        getFirstLastChildrenTest(false);
291    }
292
293    @Test
294    public void getFirstLastChildrenTestProvideArray() throws Throwable {
295        getFirstLastChildrenTest(true);
296    }
297
298    public void getFirstLastChildrenTest(final boolean provideArr) throws Throwable {
299        setupByConfig(mConfig);
300        waitFirstLayout();
301        Runnable viewInBoundsTest = new Runnable() {
302            @Override
303            public void run() {
304                VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
305                final String boundsLog = mLayoutManager.getBoundsLog();
306                VisibleChildren queryResult = new VisibleChildren(mLayoutManager.getSpanCount());
307                queryResult.findFirstPartialVisibleClosestToStart = mLayoutManager
308                        .findFirstVisibleItemClosestToStart(false);
309                queryResult.findFirstPartialVisibleClosestToEnd = mLayoutManager
310                        .findFirstVisibleItemClosestToEnd(false);
311                queryResult.firstFullyVisiblePositions = mLayoutManager
312                        .findFirstCompletelyVisibleItemPositions(
313                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
314                queryResult.firstVisiblePositions = mLayoutManager
315                        .findFirstVisibleItemPositions(
316                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
317                queryResult.lastFullyVisiblePositions = mLayoutManager
318                        .findLastCompletelyVisibleItemPositions(
319                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
320                queryResult.lastVisiblePositions = mLayoutManager
321                        .findLastVisibleItemPositions(
322                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
323                assertEquals(mConfig + ":\nfirst visible child should match traversal result\n"
324                        + "traversed:" + visibleChildren + "\n"
325                        + "queried:" + queryResult + "\n"
326                        + boundsLog, visibleChildren, queryResult
327                );
328            }
329        };
330        mActivityRule.runOnUiThread(viewInBoundsTest);
331        // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
332        // case
333        final int scrollPosition = mAdapter.getItemCount();
334        mActivityRule.runOnUiThread(new Runnable() {
335            @Override
336            public void run() {
337                mRecyclerView.smoothScrollToPosition(scrollPosition);
338            }
339        });
340        while (mLayoutManager.isSmoothScrolling() ||
341                mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
342            mActivityRule.runOnUiThread(viewInBoundsTest);
343            checkForMainThreadException();
344            Thread.sleep(400);
345        }
346        // delete all items
347        mLayoutManager.expectLayouts(2);
348        mAdapter.deleteAndNotify(0, mAdapter.getItemCount());
349        mLayoutManager.waitForLayout(2);
350        // test empty case
351        mActivityRule.runOnUiThread(viewInBoundsTest);
352        // set a new adapter with huge items to test full bounds check
353        mLayoutManager.expectLayouts(1);
354        final int totalSpace = mLayoutManager.mPrimaryOrientation.getTotalSpace();
355        final TestAdapter newAdapter = new TestAdapter(100) {
356            @Override
357            public void onBindViewHolder(@NonNull TestViewHolder holder,
358                    int position) {
359                super.onBindViewHolder(holder, position);
360                if (mConfig.mOrientation == LinearLayoutManager.HORIZONTAL) {
361                    holder.itemView.setMinimumWidth(totalSpace + 100);
362                } else {
363                    holder.itemView.setMinimumHeight(totalSpace + 100);
364                }
365            }
366        };
367        mActivityRule.runOnUiThread(new Runnable() {
368            @Override
369            public void run() {
370                mRecyclerView.setAdapter(newAdapter);
371            }
372        });
373        mLayoutManager.waitForLayout(2);
374        mActivityRule.runOnUiThread(viewInBoundsTest);
375        checkForMainThreadException();
376
377        // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
378        // case
379        mActivityRule.runOnUiThread(new Runnable() {
380            @Override
381            public void run() {
382                final int diff;
383                if (mConfig.mReverseLayout) {
384                    diff = -1;
385                } else {
386                    diff = 1;
387                }
388                final int distance = diff * 10;
389                if (mConfig.mOrientation == HORIZONTAL) {
390                    mRecyclerView.scrollBy(distance, 0);
391                } else {
392                    mRecyclerView.scrollBy(0, distance);
393                }
394            }
395        });
396        mActivityRule.runOnUiThread(viewInBoundsTest);
397        checkForMainThreadException();
398    }
399
400    @Test
401    public void viewSnapTest() throws Throwable {
402        final Config config = ((Config) mConfig.clone()).itemCount(mConfig.mSpanCount + 1);
403        setupByConfig(config);
404        mAdapter.mOnBindCallback = new OnBindCallback() {
405            @Override
406            void onBoundItem(TestViewHolder vh, int position) {
407                StaggeredGridLayoutManager.LayoutParams
408                        lp = (StaggeredGridLayoutManager.LayoutParams) vh.itemView
409                        .getLayoutParams();
410                if (config.mOrientation == HORIZONTAL) {
411                    lp.width = mRecyclerView.getWidth() / 3;
412                } else {
413                    lp.height = mRecyclerView.getHeight() / 3;
414                }
415            }
416
417            @Override
418            boolean assignRandomSize() {
419                return false;
420            }
421        };
422        waitFirstLayout();
423        // run these tests twice. once initial layout, once after scroll
424        String logSuffix = "";
425        for (int i = 0; i < 2; i++) {
426            Map<Item, Rect> itemRectMap = mLayoutManager.collectChildCoordinates();
427            Rect recyclerViewBounds = getDecoratedRecyclerViewBounds();
428            // workaround for SGLM's span distribution issue. Right now, it may leave gaps so we
429            // avoid it by setting its layout params directly
430            if (config.mOrientation == HORIZONTAL) {
431                recyclerViewBounds.bottom -= recyclerViewBounds.height() % config.mSpanCount;
432            } else {
433                recyclerViewBounds.right -= recyclerViewBounds.width() % config.mSpanCount;
434            }
435
436            Rect usedLayoutBounds = new Rect();
437            for (Rect rect : itemRectMap.values()) {
438                usedLayoutBounds.union(rect);
439            }
440
441            if (DEBUG) {
442                Log.d(TAG, "testing view snapping (" + logSuffix + ") for config " + config);
443            }
444            if (config.mOrientation == VERTICAL) {
445                assertEquals(config + " there should be no gap on left" + logSuffix,
446                        usedLayoutBounds.left, recyclerViewBounds.left);
447                assertEquals(config + " there should be no gap on right" + logSuffix,
448                        usedLayoutBounds.right, recyclerViewBounds.right);
449                if (config.mReverseLayout) {
450                    assertEquals(config + " there should be no gap on bottom" + logSuffix,
451                            usedLayoutBounds.bottom, recyclerViewBounds.bottom);
452                    assertTrue(config + " there should be some gap on top" + logSuffix,
453                            usedLayoutBounds.top > recyclerViewBounds.top);
454                } else {
455                    assertEquals(config + " there should be no gap on top" + logSuffix,
456                            usedLayoutBounds.top, recyclerViewBounds.top);
457                    assertTrue(config + " there should be some gap at the bottom" + logSuffix,
458                            usedLayoutBounds.bottom < recyclerViewBounds.bottom);
459                }
460            } else {
461                assertEquals(config + " there should be no gap on top" + logSuffix,
462                        usedLayoutBounds.top, recyclerViewBounds.top);
463                assertEquals(config + " there should be no gap at the bottom" + logSuffix,
464                        usedLayoutBounds.bottom, recyclerViewBounds.bottom);
465                if (config.mReverseLayout) {
466                    assertEquals(config + " there should be no on right" + logSuffix,
467                            usedLayoutBounds.right, recyclerViewBounds.right);
468                    assertTrue(config + " there should be some gap on left" + logSuffix,
469                            usedLayoutBounds.left > recyclerViewBounds.left);
470                } else {
471                    assertEquals(config + " there should be no gap on left" + logSuffix,
472                            usedLayoutBounds.left, recyclerViewBounds.left);
473                    assertTrue(config + " there should be some gap on right" + logSuffix,
474                            usedLayoutBounds.right < recyclerViewBounds.right);
475                }
476            }
477            final int scroll = config.mReverseLayout ? -500 : 500;
478            scrollBy(scroll);
479            logSuffix = " scrolled " + scroll;
480        }
481    }
482
483    @Test
484    public void scrollToPositionWithOffsetTest() throws Throwable {
485        setupByConfig(mConfig);
486        waitFirstLayout();
487        OrientationHelper orientationHelper = OrientationHelper
488                .createOrientationHelper(mLayoutManager, mConfig.mOrientation);
489        Rect layoutBounds = getDecoratedRecyclerViewBounds();
490        // try scrolling towards head, should not affect anything
491        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
492        scrollToPositionWithOffset(0, 20);
493        assertRectSetsEqual(mConfig + " trying to over scroll with offset should be no-op",
494                before, mLayoutManager.collectChildCoordinates());
495        // try offsetting some visible children
496        int testCount = 10;
497        while (testCount-- > 0) {
498            // get middle child
499            final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2);
500            final int position = mRecyclerView.getChildLayoutPosition(child);
501            final int startOffset = mConfig.mReverseLayout ?
502                    orientationHelper.getEndAfterPadding() - orientationHelper
503                            .getDecoratedEnd(child)
504                    : orientationHelper.getDecoratedStart(child) - orientationHelper
505                            .getStartAfterPadding();
506            final int scrollOffset = startOffset / 2;
507            mLayoutManager.expectLayouts(1);
508            scrollToPositionWithOffset(position, scrollOffset);
509            mLayoutManager.waitForLayout(2);
510            final int finalOffset = mConfig.mReverseLayout ?
511                    orientationHelper.getEndAfterPadding() - orientationHelper
512                            .getDecoratedEnd(child)
513                    : orientationHelper.getDecoratedStart(child) - orientationHelper
514                            .getStartAfterPadding();
515            assertEquals(mConfig + " scroll with offset on a visible child should work fine",
516                    scrollOffset, finalOffset);
517        }
518
519        // try scrolling to invisible children
520        testCount = 10;
521        // we test above and below, one by one
522        int offsetMultiplier = -1;
523        while (testCount-- > 0) {
524            final TargetTuple target = findInvisibleTarget(mConfig);
525            mLayoutManager.expectLayouts(1);
526            final int offset = offsetMultiplier
527                    * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3;
528            scrollToPositionWithOffset(target.mPosition, offset);
529            mLayoutManager.waitForLayout(2);
530            final View child = mLayoutManager.findViewByPosition(target.mPosition);
531            assertNotNull(mConfig + " scrolling to a mPosition with offset " + offset
532                    + " should layout it", child);
533            final Rect bounds = mLayoutManager.getViewBounds(child);
534            if (DEBUG) {
535                Log.d(TAG, mConfig + " post scroll to invisible mPosition " + bounds + " in "
536                        + layoutBounds + " with offset " + offset);
537            }
538
539            if (mConfig.mReverseLayout) {
540                assertEquals(mConfig + " when scrolling with offset to an invisible in reverse "
541                                + "layout, its end should align with recycler view's end - offset",
542                        orientationHelper.getEndAfterPadding() - offset,
543                        orientationHelper.getDecoratedEnd(child)
544                );
545            } else {
546                assertEquals(mConfig + " when scrolling with offset to an invisible child in normal"
547                                + " layout its start should align with recycler view's start + "
548                                + "offset",
549                        orientationHelper.getStartAfterPadding() + offset,
550                        orientationHelper.getDecoratedStart(child)
551                );
552            }
553            offsetMultiplier *= -1;
554        }
555    }
556
557    @Test
558    public void scrollToPositionTest() throws Throwable {
559        setupByConfig(mConfig);
560        waitFirstLayout();
561        OrientationHelper orientationHelper = OrientationHelper
562                .createOrientationHelper(mLayoutManager, mConfig.mOrientation);
563        Rect layoutBounds = getDecoratedRecyclerViewBounds();
564        for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
565            View view = mLayoutManager.getChildAt(i);
566            Rect bounds = mLayoutManager.getViewBounds(view);
567            if (layoutBounds.contains(bounds)) {
568                Map<Item, Rect> initialBounds = mLayoutManager.collectChildCoordinates();
569                final int position = mRecyclerView.getChildLayoutPosition(view);
570                StaggeredGridLayoutManager.LayoutParams layoutParams
571                        = (StaggeredGridLayoutManager.LayoutParams) (view.getLayoutParams());
572                TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder;
573                assertEquals("recycler view mPosition should match adapter mPosition", position,
574                        vh.mBoundItem.mAdapterIndex);
575                if (DEBUG) {
576                    Log.d(TAG, "testing scroll to visible mPosition at " + position
577                            + " " + bounds + " inside " + layoutBounds);
578                }
579                mLayoutManager.expectLayouts(1);
580                scrollToPosition(position);
581                mLayoutManager.waitForLayout(2);
582                if (DEBUG) {
583                    view = mLayoutManager.findViewByPosition(position);
584                    Rect newBounds = mLayoutManager.getViewBounds(view);
585                    Log.d(TAG, "after scrolling to visible mPosition " +
586                            bounds + " equals " + newBounds);
587                }
588
589                assertRectSetsEqual(
590                        mConfig + "scroll to mPosition on fully visible child should be no-op",
591                        initialBounds, mLayoutManager.collectChildCoordinates());
592            } else {
593                final int position = mRecyclerView.getChildLayoutPosition(view);
594                if (DEBUG) {
595                    Log.d(TAG,
596                            "child(" + position + ") not fully visible " + bounds + " not inside "
597                                    + layoutBounds
598                                    + mRecyclerView.getChildLayoutPosition(view)
599                    );
600                }
601                mLayoutManager.expectLayouts(1);
602                mActivityRule.runOnUiThread(new Runnable() {
603                    @Override
604                    public void run() {
605                        mLayoutManager.scrollToPosition(position);
606                    }
607                });
608                mLayoutManager.waitForLayout(2);
609                view = mLayoutManager.findViewByPosition(position);
610                bounds = mLayoutManager.getViewBounds(view);
611                if (DEBUG) {
612                    Log.d(TAG, "after scroll to partially visible child " + bounds + " in "
613                            + layoutBounds);
614                }
615                assertTrue(mConfig
616                                + " after scrolling to a partially visible child, it should become fully "
617                                + " visible. " + bounds + " not inside " + layoutBounds,
618                        layoutBounds.contains(bounds)
619                );
620                assertTrue(
621                        mConfig + " when scrolling to a partially visible item, one of its edges "
622                                + "should be on the boundaries",
623                        orientationHelper.getStartAfterPadding() ==
624                                orientationHelper.getDecoratedStart(view)
625                                || orientationHelper.getEndAfterPadding() ==
626                                orientationHelper.getDecoratedEnd(view));
627            }
628        }
629
630        // try scrolling to invisible children
631        int testCount = 10;
632        while (testCount-- > 0) {
633            final TargetTuple target = findInvisibleTarget(mConfig);
634            mLayoutManager.expectLayouts(1);
635            scrollToPosition(target.mPosition);
636            mLayoutManager.waitForLayout(2);
637            final View child = mLayoutManager.findViewByPosition(target.mPosition);
638            assertNotNull(mConfig + " scrolling to a mPosition should lay it out", child);
639            final Rect bounds = mLayoutManager.getViewBounds(child);
640            if (DEBUG) {
641                Log.d(TAG, mConfig + " post scroll to invisible mPosition " + bounds + " in "
642                        + layoutBounds);
643            }
644            assertTrue(mConfig + " scrolling to a mPosition should make it fully visible",
645                    layoutBounds.contains(bounds));
646            if (target.mLayoutDirection == LAYOUT_START) {
647                assertEquals(
648                        mConfig + " when scrolling to an invisible child above, its start should"
649                                + " align with recycler view's start",
650                        orientationHelper.getStartAfterPadding(),
651                        orientationHelper.getDecoratedStart(child)
652                );
653            } else {
654                assertEquals(mConfig + " when scrolling to an invisible child below, its end "
655                                + "should align with recycler view's end",
656                        orientationHelper.getEndAfterPadding(),
657                        orientationHelper.getDecoratedEnd(child)
658                );
659            }
660        }
661    }
662
663    @Test
664    public void scollByTest() throws Throwable {
665        setupByConfig(mConfig);
666        waitFirstLayout();
667        // try invalid scroll. should not happen
668        final View first = mLayoutManager.getChildAt(0);
669        OrientationHelper primaryOrientation = OrientationHelper
670                .createOrientationHelper(mLayoutManager, mConfig.mOrientation);
671        int scrollDist;
672        if (mConfig.mReverseLayout) {
673            scrollDist = primaryOrientation.getDecoratedMeasurement(first) / 2;
674        } else {
675            scrollDist = -primaryOrientation.getDecoratedMeasurement(first) / 2;
676        }
677        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
678        scrollBy(scrollDist);
679        Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
680        assertRectSetsEqual(
681                mConfig + " if there are no more items, scroll should not happen (dt:" + scrollDist
682                        + ")",
683                before, after
684        );
685
686        scrollDist = -scrollDist * 3;
687        before = mLayoutManager.collectChildCoordinates();
688        scrollBy(scrollDist);
689        after = mLayoutManager.collectChildCoordinates();
690        int layoutStart = primaryOrientation.getStartAfterPadding();
691        int layoutEnd = primaryOrientation.getEndAfterPadding();
692        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
693            Rect afterRect = after.get(entry.getKey());
694            // offset rect
695            if (mConfig.mOrientation == VERTICAL) {
696                entry.getValue().offset(0, -scrollDist);
697            } else {
698                entry.getValue().offset(-scrollDist, 0);
699            }
700            if (afterRect == null || afterRect.isEmpty()) {
701                // assert item is out of bounds
702                int start, end;
703                if (mConfig.mOrientation == VERTICAL) {
704                    start = entry.getValue().top;
705                    end = entry.getValue().bottom;
706                } else {
707                    start = entry.getValue().left;
708                    end = entry.getValue().right;
709                }
710                assertTrue(
711                        mConfig + " if item is missing after relayout, it should be out of bounds."
712                                + "item start: " + start + ", end:" + end + " layout start:"
713                                + layoutStart +
714                                ", layout end:" + layoutEnd,
715                        start <= layoutStart && end <= layoutEnd ||
716                                start >= layoutEnd && end >= layoutEnd
717                );
718            } else {
719                assertEquals(mConfig + " Item should be laid out at the scroll offset coordinates",
720                        entry.getValue(),
721                        afterRect);
722            }
723        }
724        assertViewPositions(mConfig);
725    }
726
727    @Test
728    public void layoutOrderTest() throws Throwable {
729        setupByConfig(mConfig);
730        assertViewPositions(mConfig);
731    }
732
733    @Test
734    public void consistentRelayout() throws Throwable {
735        consistentRelayoutTest(mConfig, false);
736    }
737
738    @Test
739    public void consistentRelayoutWithFullSpanFirstChild() throws Throwable {
740        consistentRelayoutTest(mConfig, true);
741    }
742
743    @Suppress
744    @FlakyTest(bugId = 34158822)
745    @Test
746    @LargeTest
747    public void dontRecycleViewsTranslatedOutOfBoundsFromStart() throws Throwable {
748        final Config config = ((Config) mConfig.clone()).itemCount(1000);
749        setupByConfig(config);
750        waitFirstLayout();
751        // pick position from child count so that it is not too far away
752        int pos = mRecyclerView.getChildCount() * 2;
753        smoothScrollToPosition(pos, true);
754        final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(pos);
755        OrientationHelper helper = mLayoutManager.mPrimaryOrientation;
756        int gap = helper.getDecoratedStart(vh.itemView);
757        scrollBy(gap);
758        gap = helper.getDecoratedStart(vh.itemView);
759        assertThat("test sanity", gap, is(0));
760
761        final int size = helper.getDecoratedMeasurement(vh.itemView);
762        AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView);
763        mActivityRule.runOnUiThread(new Runnable() {
764            @Override
765            public void run() {
766                if (mConfig.mOrientation == HORIZONTAL) {
767                    vh.itemView.setTranslationX(size * 2);
768                } else {
769                    vh.itemView.setTranslationY(size * 2);
770                }
771            }
772        });
773        scrollBy(size * 2);
774        assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView))));
775        assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView));
776        assertThat(vh.getAdapterPosition(), is(pos));
777        scrollBy(size * 2);
778        assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView)));
779    }
780
781    @Test
782    public void dontRecycleViewsTranslatedOutOfBoundsFromEnd() throws Throwable {
783        final Config config = ((Config) mConfig.clone()).itemCount(1000);
784        setupByConfig(config);
785        waitFirstLayout();
786        // pick position from child count so that it is not too far away
787        int pos = mRecyclerView.getChildCount() * 2;
788        mLayoutManager.expectLayouts(1);
789        scrollToPosition(pos);
790        mLayoutManager.waitForLayout(2);
791        final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(pos);
792        OrientationHelper helper = mLayoutManager.mPrimaryOrientation;
793        int gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView);
794        scrollBy(-gap);
795        gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView);
796        assertThat("test sanity", gap, is(0));
797
798        final int size = helper.getDecoratedMeasurement(vh.itemView);
799        AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView);
800        mActivityRule.runOnUiThread(new Runnable() {
801            @Override
802            public void run() {
803                if (mConfig.mOrientation == HORIZONTAL) {
804                    vh.itemView.setTranslationX(-size * 2);
805                } else {
806                    vh.itemView.setTranslationY(-size * 2);
807                }
808            }
809        });
810        scrollBy(-size * 2);
811        assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView))));
812        assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView));
813        assertThat(vh.getAdapterPosition(), is(pos));
814        scrollBy(-size * 2);
815        assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView)));
816    }
817
818    public void consistentRelayoutTest(Config config, boolean firstChildMultiSpan)
819            throws Throwable {
820        setupByConfig(config);
821        if (firstChildMultiSpan) {
822            mAdapter.mFullSpanItems.add(0);
823        }
824        waitFirstLayout();
825        // record all child positions
826        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
827        requestLayoutOnUIThread(mRecyclerView);
828        Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
829        assertRectSetsEqual(
830                config + " simple re-layout, firstChildMultiSpan:" + firstChildMultiSpan, before,
831                after);
832        // scroll some to create inconsistency
833        View firstChild = mLayoutManager.getChildAt(0);
834        final int firstChildStartBeforeScroll = mLayoutManager.mPrimaryOrientation
835                .getDecoratedStart(firstChild);
836        int distance = mLayoutManager.mPrimaryOrientation.getDecoratedMeasurement(firstChild) / 2;
837        if (config.mReverseLayout) {
838            distance *= -1;
839        }
840        scrollBy(distance);
841        waitForMainThread(2);
842        assertTrue("scroll by should move children", firstChildStartBeforeScroll !=
843                mLayoutManager.mPrimaryOrientation.getDecoratedStart(firstChild));
844        before = mLayoutManager.collectChildCoordinates();
845        mLayoutManager.expectLayouts(1);
846        requestLayoutOnUIThread(mRecyclerView);
847        mLayoutManager.waitForLayout(2);
848        after = mLayoutManager.collectChildCoordinates();
849        assertRectSetsEqual(config + " simple re-layout after scroll", before, after);
850    }
851}
852