FileDocCategorySizeDatePackage
StaggeredGridLayoutManagerTest.javaAPI DocAndroid 5.1 API88517Thu Mar 12 22:22:56 GMT 2015android.support.v7.widget

StaggeredGridLayoutManagerTest

public class StaggeredGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest

Fields Summary
private static final boolean
DEBUG
private static final String
TAG
volatile WrappedLayoutManager
mLayoutManager
GridTestAdapter
mAdapter
final List
mBaseVariations
Constructors Summary
Methods Summary
public voidassertRectSetsEqual(java.lang.String message, java.util.Map before, java.util.Map after)

        StringBuilder log = new StringBuilder();
        if (DEBUG) {
            log.append("checking rectangle equality.\n");
            log.append("before:");
            for (Map.Entry<Item, Rect> entry : before.entrySet()) {
                log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
                        .append(entry.getValue());
            }
            log.append("\nafter:");
            for (Map.Entry<Item, Rect> entry : after.entrySet()) {
                log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
                        .append(entry.getValue());
            }
            message += "\n\n" + log.toString();
        }
        assertEquals(message + ": item counts should be equal", before.size()
                , after.size());
        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
            Rect afterRect = after.get(entry.getKey());
            assertNotNull(message + ": Same item should be visible after simple re-layout",
                    afterRect);
            assertEquals(message + ": Item should be laid out at the same coordinates",
                    entry.getValue(),
                    afterRect);
        }
    
public voidassertRectSetsNotEqual(java.lang.String message, java.util.Map before, java.util.Map after)

        Throwable throwable = null;
        try {
            assertRectSetsEqual("NOT " + message, before, after);
        } catch (Throwable t) {
            throwable = t;
        }
        assertNotNull(message + " two layout should be different", throwable);
    
voidassertSpan(java.lang.String msg, int childPosition, int expectedSpan)

        View view = mLayoutManager.findViewByPosition(childPosition);
        assertNotNull(msg + "view at position " + childPosition + " should exists", view);
        assertEquals(msg + "[child:" + childPosition + "]", expectedSpan,
                getLp(view).mSpan.mIndex);
    
voidassertSpanAssignmentEquality(java.lang.String msg, int[] set1, int[] set2, int start, int end)

        for (int i = start; i < end; i++) {
            assertEquals(msg + " ind:" + i, set1[i], set2[i]);
        }
    
voidassertSpanAssignmentEquality(java.lang.String msg, int[] set1, int[] set2, int start1, int start2, int length)

        for (int i = 0; i < length; i++) {
            assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i],
                    set2[start2 + i]);
        }
    
voidassertSpans(java.lang.String msg, int[] childSpanTuples)

        for (int i = 0; i < childSpanTuples.length; i++) {
            assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
        }
    
voidassertViewPositions(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        ArrayList<ArrayList<View>> viewsBySpan = mLayoutManager.collectChildrenBySpan();
        OrientationHelper orientationHelper = OrientationHelper
                .createOrientationHelper(mLayoutManager, config.mOrientation);
        for (ArrayList<View> span : viewsBySpan) {
            // validate all children's order. first child should have min start mPosition
            final int count = span.size();
            for (int i = 0, j = 1; j < count; i++, j++) {
                View prev = span.get(i);
                View next = span.get(j);
                assertTrue(config + " prev item should be above next item",
                        orientationHelper.getDecoratedEnd(prev) <= orientationHelper
                                .getDecoratedStart(next)
                );

            }
        }
    
public voidconsistentRelayoutTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config, boolean firstChildMultiSpan)

        setupByConfig(config);
        if (firstChildMultiSpan) {
            mAdapter.mFullSpanItems.add(0);
        }
        waitFirstLayout();
        // record all child positions
        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
        requestLayoutOnUIThread(mRecyclerView);
        Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
        assertRectSetsEqual(
                config + " simple re-layout, firstChildMultiSpan:" + firstChildMultiSpan, before,
                after);
        // scroll some to create inconsistency
        View firstChild = mLayoutManager.getChildAt(0);
        final int firstChildStartBeforeScroll = mLayoutManager.mPrimaryOrientation
                .getDecoratedStart(firstChild);
        int distance = mLayoutManager.mPrimaryOrientation.getDecoratedMeasurement(firstChild) / 2;
        if (config.mReverseLayout) {
            distance *= -1;
        }
        scrollBy(distance);
        waitForMainThread(2);
        assertTrue("scroll by should move children", firstChildStartBeforeScroll !=
                mLayoutManager.mPrimaryOrientation.getDecoratedStart(firstChild));
        before = mLayoutManager.collectChildCoordinates();
        mLayoutManager.expectLayouts(1);
        requestLayoutOnUIThread(mRecyclerView);
        mLayoutManager.waitForLayout(2);
        after = mLayoutManager.collectChildCoordinates();
        assertRectSetsEqual(config + " simple re-layout after scroll", before, after);
    
private int[]copyOfRange(int[] original, int from, int to)

        int newLength = to - from;
        if (newLength < 0) {
            throw new IllegalArgumentException(from + " > " + to);
        }
        int[] copy = new int[newLength];
        System.arraycopy(original, from, copy, 0,
                Math.min(original.length - from, newLength));
        return copy;
    
public voidcustomSizeInScrollDirectionTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        setupByConfig(config);
        final Map<View, Integer> sizeMap = new HashMap<View, Integer>();
        mAdapter.mOnBindHandler = new OnBindHandler() {
            @Override
            void onBoundItem(TestViewHolder vh, int position) {
                final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
                final int size = 1 + position * 5;
                if (config.mOrientation == HORIZONTAL) {
                    layoutParams.width = size;
                } else {
                    layoutParams.height = size;
                }
                sizeMap.put(vh.itemView, size);
                if (position == 3) {
                    getLp(vh.itemView).setFullSpan(true);
                }
            }

            @Override
            boolean assignRandomSize() {
                return false;
            }
        };
        waitFirstLayout();
        assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 0);
        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
            View child = mRecyclerView.getChildAt(i);
            final int size = config.mOrientation == HORIZONTAL ? child.getWidth()
                    : child.getHeight();
            assertEquals("child " + i + " should have the size specified in its layout params",
                    sizeMap.get(child).intValue(), size);
        }
        checkForMainThreadException();
    
private TargetTuplefindInvisibleTarget(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE;
        for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
            View child = mLayoutManager.getChildAt(i);
            int position = mRecyclerView.getChildLayoutPosition(child);
            if (position < minPosition) {
                minPosition = position;
            }
            if (position > maxPosition) {
                maxPosition = position;
            }
        }
        final int tailTarget = maxPosition + (mAdapter.getItemCount() - maxPosition) / 2;
        final int headTarget = minPosition / 2;
        final int target;
        // where will the child come from ?
        final int itemLayoutDirection;
        if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) {
            target = tailTarget;
            itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END;
        } else {
            target = headTarget;
            itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START;
        }
        if (DEBUG) {
            Log.d(TAG,
                    config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition);
        }
        return new TargetTuple(target, itemLayoutDirection);
    
public voidgapAtTheBeginningOfTheListTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config, int deletePosition, int deleteCount)

        if (config.mSpanCount < 2 || config.mGapStrategy == GAP_HANDLING_NONE) {
            return;
        }
        if (config.mItemCount < 100) {
            config.itemCount(100);
        }
        final String logPrefix = config + ", deletePos:" + deletePosition + ", deleteCount:"
                + deleteCount;
        setupByConfig(config);
        final RecyclerView.Adapter adapter = mAdapter;
        waitFirstLayout();
        // scroll far away
        smoothScrollToPosition(config.mItemCount / 2);
        // assert to be deleted child is not visible
        assertNull(logPrefix + " test sanity, to be deleted child should be invisible",
                mRecyclerView.findViewHolderForLayoutPosition(deletePosition));
        // delete the child and notify
        mAdapter.deleteAndNotify(deletePosition, deleteCount);
        getInstrumentation().waitForIdleSync();
        mLayoutManager.expectLayouts(1);
        smoothScrollToPosition(0);
        mLayoutManager.waitForLayout(2);
        // due to data changes, first item may become visible before others which will cause
        // smooth scrolling to stop. Triggering it twice more is a naive hack.
        // Until we have time to consider it as a bug, this is the only workaround.
        smoothScrollToPosition(0);
        Thread.sleep(300);
        smoothScrollToPosition(0);
        Thread.sleep(500);
        // some animations should happen and we should recover layout
        final Map<Item, Rect> actualCoords = mLayoutManager.collectChildCoordinates();
        // now layout another RV with same adapter
        removeRecyclerView();
        setupByConfig(config);
        mRecyclerView.setAdapter(adapter);// use same adapter so that items can be matched
        waitFirstLayout();
        final Map<Item, Rect> desiredCoords = mLayoutManager.collectChildCoordinates();
        assertRectSetsEqual(logPrefix + " when an item from the start of the list is deleted, "
                        + "layout should recover the state once scrolling is stopped",
                desiredCoords, actualCoords);
    
public voidgapInTheMiddle(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)


    
public voidgetFirstLastChildrenTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config, boolean provideArr)

        setupByConfig(config);
        waitFirstLayout();
        Runnable viewInBoundsTest = new Runnable() {
            @Override
            public void run() {
                VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
                final String boundsLog = mLayoutManager.getBoundsLog();
                VisibleChildren queryResult = new VisibleChildren(mLayoutManager.getSpanCount());
                queryResult.firstFullyVisiblePositions = mLayoutManager
                        .findFirstCompletelyVisibleItemPositions(
                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
                queryResult.firstVisiblePositions = mLayoutManager
                        .findFirstVisibleItemPositions(
                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
                queryResult.lastFullyVisiblePositions = mLayoutManager
                        .findLastCompletelyVisibleItemPositions(
                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
                queryResult.lastVisiblePositions = mLayoutManager
                        .findLastVisibleItemPositions(
                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
                assertEquals(config + ":\nfirst visible child should match traversal result\n"
                                + "traversed:" + visibleChildren + "\n"
                                + "queried:" + queryResult + "\n"
                                + boundsLog, visibleChildren, queryResult
                );
            }
        };
        runTestOnUiThread(viewInBoundsTest);
        // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
        // case
        final int scrollPosition = mAdapter.getItemCount();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                mRecyclerView.smoothScrollToPosition(scrollPosition);
            }
        });
        while (mLayoutManager.isSmoothScrolling() ||
                mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
            runTestOnUiThread(viewInBoundsTest);
            checkForMainThreadException();
            Thread.sleep(400);
        }
        // delete all items
        mLayoutManager.expectLayouts(2);
        mAdapter.deleteAndNotify(0, mAdapter.getItemCount());
        mLayoutManager.waitForLayout(2);
        // test empty case
        runTestOnUiThread(viewInBoundsTest);
        // set a new adapter with huge items to test full bounds check
        mLayoutManager.expectLayouts(1);
        final int totalSpace = mLayoutManager.mPrimaryOrientation.getTotalSpace();
        final TestAdapter newAdapter = new TestAdapter(100) {
            @Override
            public void onBindViewHolder(TestViewHolder holder,
                    int position) {
                super.onBindViewHolder(holder, position);
                if (config.mOrientation == LinearLayoutManager.HORIZONTAL) {
                    holder.itemView.setMinimumWidth(totalSpace + 5);
                } else {
                    holder.itemView.setMinimumHeight(totalSpace + 5);
                }
            }
        };
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                mRecyclerView.setAdapter(newAdapter);
            }
        });
        mLayoutManager.waitForLayout(2);
        runTestOnUiThread(viewInBoundsTest);
        checkForMainThreadException();
    
LayoutParamsgetLp(android.view.View view)

        return (LayoutParams) view.getLayoutParams();
    
public voidinnerGapHandlingTest(int strategy)

        Config config = new Config().spanCount(3).itemCount(500);
        setupByConfig(config);
        mLayoutManager.setGapStrategy(strategy);
        mAdapter.mFullSpanItems.add(100);
        mAdapter.mFullSpanItems.add(104);
        mAdapter.mViewsHaveEqualSize = true;
        waitFirstLayout();
        mLayoutManager.expectLayouts(1);
        scrollToPosition(400);
        mLayoutManager.waitForLayout(2);
        mLayoutManager.expectLayouts(2);
        mAdapter.addAndNotify(101, 1);
        mLayoutManager.waitForLayout(2);
        if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
            mLayoutManager.expectLayouts(1);
        }
        // state
        // now smooth scroll to 99 to trigger a layout around 100
        smoothScrollToPosition(99);
        switch (strategy) {
            case GAP_HANDLING_NONE:
                assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0},
                        new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2},
                        new int[]{105, 0});
                break;
            case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
                mLayoutManager.waitForLayout(2);
                assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0},
                        new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0});
                break;
        }

    
public voidlayoutOrderTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        setupByConfig(config);
        assertViewPositions(config);
    
voidrtlTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config, boolean changeRtlAfter)

        if (config.mSpanCount == 1) {
            config.mSpanCount = 2;
        }
        String logPrefix = config + ", changeRtlAfterLayout:" + changeRtlAfter;
        setupByConfig(config.itemCount(5));
        if (changeRtlAfter) {
            waitFirstLayout();
            mLayoutManager.expectLayouts(1);
            mLayoutManager.setFakeRtl(true);
            mLayoutManager.waitForLayout(2);
        } else {
            mLayoutManager.mFakeRTL = true;
            waitFirstLayout();
        }

        assertEquals("view should become rtl", true, mLayoutManager.isLayoutRTL());
        OrientationHelper helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
        View child0 = mLayoutManager.findViewByPosition(0);
        View child1 = mLayoutManager.findViewByPosition(config.mOrientation == VERTICAL ? 1
                : config.mSpanCount);
        assertNotNull(logPrefix + " child position 0 should be laid out", child0);
        assertNotNull(logPrefix + " child position 0 should be laid out", child1);
        if (config.mOrientation == VERTICAL || !config.mReverseLayout) {
            assertTrue(logPrefix + " second child should be to the left of first child",
                    helper.getDecoratedStart(child0) >= helper.getDecoratedEnd(child1));
            assertEquals(logPrefix + " first child should be right aligned",
                    helper.getDecoratedEnd(child0), helper.getEndAfterPadding());
        } else {
            assertTrue(logPrefix + " first child should be to the left of second child",
                    helper.getDecoratedStart(child1) >= helper.getDecoratedEnd(child0));
            assertEquals(logPrefix + " first child should be left aligned",
                    helper.getDecoratedStart(child0), helper.getStartAfterPadding());
        }
        checkForMainThreadException();
    
private voidsaveRestore(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    Parcelable savedState = mRecyclerView.onSaveInstanceState();
                    // we append a suffix to the parcelable to test out of bounds
                    String parcelSuffix = UUID.randomUUID().toString();
                    Parcel parcel = Parcel.obtain();
                    savedState.writeToParcel(parcel, 0);
                    parcel.writeString(parcelSuffix);
                    removeRecyclerView();
                    // reset for reading
                    parcel.setDataPosition(0);
                    // re-create
                    savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
                    RecyclerView restored = new RecyclerView(getActivity());
                    mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
                            config.mOrientation);
                    mLayoutManager.setGapStrategy(config.mGapStrategy);
                    restored.setLayoutManager(mLayoutManager);
                    // use the same adapter for Rect matching
                    restored.setAdapter(mAdapter);
                    restored.onRestoreInstanceState(savedState);
                    if (Looper.myLooper() == Looper.getMainLooper()) {
                        mLayoutManager.expectLayouts(1);
                        setRecyclerView(restored);
                    } else {
                        mLayoutManager.expectLayouts(1);
                        setRecyclerView(restored);
                        mLayoutManager.waitForLayout(2);
                    }
                } catch (Throwable t) {
                    postExceptionToInstrumentation(t);
                }
            }
        });
        checkForMainThreadException();
    
public voidsavedStateTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config, boolean waitForLayout, android.support.v7.widget.StaggeredGridLayoutManagerTest$PostLayoutRunnable postLayoutOperations)

        if (DEBUG) {
            Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config "
                    + config + " post layout action " + postLayoutOperations.describe());
        }
        setupByConfig(config);
        waitFirstLayout();
        if (waitForLayout) {
            postLayoutOperations.run();
        }
        final int firstCompletelyVisiblePosition = mLayoutManager.findFirstVisibleItemPositionInt();
        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
        Parcelable savedState = mRecyclerView.onSaveInstanceState();
        // we append a suffix to the parcelable to test out of bounds
        String parcelSuffix = UUID.randomUUID().toString();
        Parcel parcel = Parcel.obtain();
        savedState.writeToParcel(parcel, 0);
        parcel.writeString(parcelSuffix);
        removeRecyclerView();
        // reset for reading
        parcel.setDataPosition(0);
        // re-create
        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
        removeRecyclerView();

        RecyclerView restored = new RecyclerView(getActivity());
        mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
        mLayoutManager.setGapStrategy(config.mGapStrategy);
        restored.setLayoutManager(mLayoutManager);
        // use the same adapter for Rect matching
        restored.setAdapter(mAdapter);
        restored.onRestoreInstanceState(savedState);
        assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
                parcel.readString());
        mLayoutManager.expectLayouts(1);
        setRecyclerView(restored);
        mLayoutManager.waitForLayout(2);
        assertEquals(config + " on saved state, reverse layout should be preserved",
                config.mReverseLayout, mLayoutManager.getReverseLayout());
        assertEquals(config + " on saved state, orientation should be preserved",
                config.mOrientation, mLayoutManager.getOrientation());
        assertEquals(config + " on saved state, span count should be preserved",
                config.mSpanCount, mLayoutManager.getSpanCount());
        assertEquals(config + " on saved state, gap strategy should be preserved",
                config.mGapStrategy, mLayoutManager.getGapStrategy());
        assertEquals(config + " on saved state, first completely visible child position should"
                        + " be preserved", firstCompletelyVisiblePosition,
                mLayoutManager.findFirstVisibleItemPositionInt());
        if (waitForLayout) {
            assertRectSetsEqual(config + "\npost layout op:" + postLayoutOperations.describe()
                            + ": on restore, previous view positions should be preserved",
                    before, mLayoutManager.collectChildCoordinates()
            );
        }
        // TODO add tests for changing values after restore before layout
    
public voidscrollBackAndPreservePositionsTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config, boolean saveRestoreInBetween)

        setupByConfig(config);
        mAdapter.mOnBindHandler = new OnBindHandler() {
            @Override
            public void onBoundItem(TestViewHolder vh, int position) {
                LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
                lp.setFullSpan((position * 7) % (config.mSpanCount + 1) == 0);
            }
        };
        waitFirstLayout();
        final int[] globalPositions = new int[mAdapter.getItemCount()];
        Arrays.fill(globalPositions, Integer.MIN_VALUE);
        final int scrollStep = (mLayoutManager.mPrimaryOrientation.getTotalSpace() / 10)
                * (config.mReverseLayout ? -1 : 1);

        final int[] globalPos = new int[1];
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                int globalScrollPosition = 0;
                while (globalPositions[mAdapter.getItemCount() - 1] == Integer.MIN_VALUE) {
                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
                        View child = mRecyclerView.getChildAt(i);
                        final int pos = mRecyclerView.getChildLayoutPosition(child);
                        if (globalPositions[pos] != Integer.MIN_VALUE) {
                            continue;
                        }
                        if (config.mReverseLayout) {
                            globalPositions[pos] = globalScrollPosition +
                                    mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
                        } else {
                            globalPositions[pos] = globalScrollPosition +
                                    mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
                        }
                    }
                    globalScrollPosition += mLayoutManager.scrollBy(scrollStep,
                            mRecyclerView.mRecycler, mRecyclerView.mState);
                }
                if (DEBUG) {
                    Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
                }
                globalPos[0] = globalScrollPosition;
            }
        });
        checkForMainThreadException();

        if (saveRestoreInBetween) {
            saveRestore(config);
        }

        checkForMainThreadException();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                int globalScrollPosition = globalPos[0];
                // now scroll back and make sure global positions match
                BitSet shouldTest = new BitSet(mAdapter.getItemCount());
                shouldTest.set(0, mAdapter.getItemCount() - 1, true);
                String assertPrefix = config + ", restored in between:" + saveRestoreInBetween
                        + " global pos must match when scrolling in reverse for position ";
                int scrollAmount = Integer.MAX_VALUE;
                while (!shouldTest.isEmpty() && scrollAmount != 0) {
                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
                        View child = mRecyclerView.getChildAt(i);
                        int pos = mRecyclerView.getChildLayoutPosition(child);
                        if (!shouldTest.get(pos)) {
                            continue;
                        }
                        shouldTest.clear(pos);
                        int globalPos;
                        if (config.mReverseLayout) {
                            globalPos = globalScrollPosition +
                                    mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
                        } else {
                            globalPos = globalScrollPosition +
                                    mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
                        }
                        assertEquals(assertPrefix + pos,
                                globalPositions[pos], globalPos);
                    }
                    scrollAmount = mLayoutManager.scrollBy(-scrollStep,
                            mRecyclerView.mRecycler, mRecyclerView.mState);
                    globalScrollPosition += scrollAmount;
                }
                assertTrue("all views should be seen", shouldTest.isEmpty());
            }
        });
        checkForMainThreadException();
    
public voidscrollByTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        setupByConfig(config);
        waitFirstLayout();
        // try invalid scroll. should not happen
        final View first = mLayoutManager.getChildAt(0);
        OrientationHelper primaryOrientation = OrientationHelper
                .createOrientationHelper(mLayoutManager, config.mOrientation);
        int scrollDist;
        if (config.mReverseLayout) {
            scrollDist = primaryOrientation.getDecoratedMeasurement(first) / 2;
        } else {
            scrollDist = -primaryOrientation.getDecoratedMeasurement(first) / 2;
        }
        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
        scrollBy(scrollDist);
        Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
        assertRectSetsEqual(
                config + " if there are no more items, scroll should not happen (dt:" + scrollDist
                        + ")",
                before, after
        );

        scrollDist = -scrollDist * 3;
        before = mLayoutManager.collectChildCoordinates();
        scrollBy(scrollDist);
        after = mLayoutManager.collectChildCoordinates();
        int layoutStart = primaryOrientation.getStartAfterPadding();
        int layoutEnd = primaryOrientation.getEndAfterPadding();
        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
            Rect afterRect = after.get(entry.getKey());
            // offset rect
            if (config.mOrientation == VERTICAL) {
                entry.getValue().offset(0, -scrollDist);
            } else {
                entry.getValue().offset(-scrollDist, 0);
            }
            if (afterRect == null || afterRect.isEmpty()) {
                // assert item is out of bounds
                int start, end;
                if (config.mOrientation == VERTICAL) {
                    start = entry.getValue().top;
                    end = entry.getValue().bottom;
                } else {
                    start = entry.getValue().left;
                    end = entry.getValue().right;
                }
                assertTrue(
                        config + " if item is missing after relayout, it should be out of bounds."
                                + "item start: " + start + ", end:" + end + " layout start:"
                                + layoutStart +
                                ", layout end:" + layoutEnd,
                        start <= layoutStart && end <= layoutEnd ||
                                start >= layoutEnd && end >= layoutEnd
                );
            } else {
                assertEquals(config + " Item should be laid out at the scroll offset coordinates",
                        entry.getValue(),
                        afterRect);
            }
        }
        assertViewPositions(config);
    
public voidscrollToPositionTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        setupByConfig(config);
        waitFirstLayout();
        OrientationHelper orientationHelper = OrientationHelper
                .createOrientationHelper(mLayoutManager, config.mOrientation);
        Rect layoutBounds = getDecoratedRecyclerViewBounds();
        for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
            View view = mLayoutManager.getChildAt(i);
            Rect bounds = mLayoutManager.getViewBounds(view);
            if (layoutBounds.contains(bounds)) {
                Map<Item, Rect> initialBounds = mLayoutManager.collectChildCoordinates();
                final int position = mRecyclerView.getChildLayoutPosition(view);
                LayoutParams layoutParams
                        = (LayoutParams) (view.getLayoutParams());
                TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder;
                assertEquals("recycler view mPosition should match adapter mPosition", position,
                        vh.mBoundItem.mAdapterIndex);
                if (DEBUG) {
                    Log.d(TAG, "testing scroll to visible mPosition at " + position
                            + " " + bounds + " inside " + layoutBounds);
                }
                mLayoutManager.expectLayouts(1);
                scrollToPosition(position);
                mLayoutManager.waitForLayout(2);
                if (DEBUG) {
                    view = mLayoutManager.findViewByPosition(position);
                    Rect newBounds = mLayoutManager.getViewBounds(view);
                    Log.d(TAG, "after scrolling to visible mPosition " +
                            bounds + " equals " + newBounds);
                }

                assertRectSetsEqual(
                        config + "scroll to mPosition on fully visible child should be no-op",
                        initialBounds, mLayoutManager.collectChildCoordinates());
            } else {
                final int position = mRecyclerView.getChildLayoutPosition(view);
                if (DEBUG) {
                    Log.d(TAG,
                            "child(" + position + ") not fully visible " + bounds + " not inside "
                                    + layoutBounds
                                    + mRecyclerView.getChildLayoutPosition(view)
                    );
                }
                mLayoutManager.expectLayouts(1);
                runTestOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mLayoutManager.scrollToPosition(position);
                    }
                });
                mLayoutManager.waitForLayout(2);
                view = mLayoutManager.findViewByPosition(position);
                bounds = mLayoutManager.getViewBounds(view);
                if (DEBUG) {
                    Log.d(TAG, "after scroll to partially visible child " + bounds + " in "
                            + layoutBounds);
                }
                assertTrue(config
                                + " after scrolling to a partially visible child, it should become fully "
                                + " visible. " + bounds + " not inside " + layoutBounds,
                        layoutBounds.contains(bounds)
                );
                assertTrue(config + " when scrolling to a partially visible item, one of its edges "
                        + "should be on the boundaries", orientationHelper.getStartAfterPadding() ==
                        orientationHelper.getDecoratedStart(view)
                        || orientationHelper.getEndAfterPadding() ==
                        orientationHelper.getDecoratedEnd(view));
            }
        }

        // try scrolling to invisible children
        int testCount = 10;
        while (testCount-- > 0) {
            final TargetTuple target = findInvisibleTarget(config);
            mLayoutManager.expectLayouts(1);
            scrollToPosition(target.mPosition);
            mLayoutManager.waitForLayout(2);
            final View child = mLayoutManager.findViewByPosition(target.mPosition);
            assertNotNull(config + " scrolling to a mPosition should lay it out", child);
            final Rect bounds = mLayoutManager.getViewBounds(child);
            if (DEBUG) {
                Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in "
                        + layoutBounds);
            }
            assertTrue(config + " scrolling to a mPosition should make it fully visible",
                    layoutBounds.contains(bounds));
            if (target.mLayoutDirection == LAYOUT_START) {
                assertEquals(
                        config + " when scrolling to an invisible child above, its start should"
                                + " align with recycler view's start",
                        orientationHelper.getStartAfterPadding(),
                        orientationHelper.getDecoratedStart(child)
                );
            } else {
                assertEquals(config + " when scrolling to an invisible child below, its end "
                                + "should align with recycler view's end",
                        orientationHelper.getEndAfterPadding(),
                        orientationHelper.getDecoratedEnd(child)
                );
            }
        }
    
private voidscrollToPositionWithOffset(int position, int offset)

        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                mLayoutManager.scrollToPositionWithOffset(position, offset);
            }
        });
    
public voidscrollToPositionWithOffsetTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        setupByConfig(config);
        waitFirstLayout();
        OrientationHelper orientationHelper = OrientationHelper
                .createOrientationHelper(mLayoutManager, config.mOrientation);
        Rect layoutBounds = getDecoratedRecyclerViewBounds();
        // try scrolling towards head, should not affect anything
        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
        scrollToPositionWithOffset(0, 20);
        assertRectSetsEqual(config + " trying to over scroll with offset should be no-op",
                before, mLayoutManager.collectChildCoordinates());
        // try offsetting some visible children
        int testCount = 10;
        while (testCount-- > 0) {
            // get middle child
            final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2);
            final int position = mRecyclerView.getChildLayoutPosition(child);
            final int startOffset = config.mReverseLayout ?
                    orientationHelper.getEndAfterPadding() - orientationHelper
                            .getDecoratedEnd(child)
                    : orientationHelper.getDecoratedStart(child) - orientationHelper
                            .getStartAfterPadding();
            final int scrollOffset = startOffset / 2;
            mLayoutManager.expectLayouts(1);
            scrollToPositionWithOffset(position, scrollOffset);
            mLayoutManager.waitForLayout(2);
            final int finalOffset = config.mReverseLayout ?
                    orientationHelper.getEndAfterPadding() - orientationHelper
                            .getDecoratedEnd(child)
                    : orientationHelper.getDecoratedStart(child) - orientationHelper
                            .getStartAfterPadding();
            assertEquals(config + " scroll with offset on a visible child should work fine",
                    scrollOffset, finalOffset);
        }

        // try scrolling to invisible children
        testCount = 10;
        // we test above and below, one by one
        int offsetMultiplier = -1;
        while (testCount-- > 0) {
            final TargetTuple target = findInvisibleTarget(config);
            mLayoutManager.expectLayouts(1);
            final int offset = offsetMultiplier
                    * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3;
            scrollToPositionWithOffset(target.mPosition, offset);
            mLayoutManager.waitForLayout(2);
            final View child = mLayoutManager.findViewByPosition(target.mPosition);
            assertNotNull(config + " scrolling to a mPosition with offset " + offset
                    + " should layout it", child);
            final Rect bounds = mLayoutManager.getViewBounds(child);
            if (DEBUG) {
                Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in "
                        + layoutBounds + " with offset " + offset);
            }

            if (config.mReverseLayout) {
                assertEquals(config + " when scrolling with offset to an invisible in reverse "
                                + "layout, its end should align with recycler view's end - offset",
                        orientationHelper.getEndAfterPadding() - offset,
                        orientationHelper.getDecoratedEnd(child)
                );
            } else {
                assertEquals(config + " when scrolling with offset to an invisible child in normal"
                                + " layout its start should align with recycler view's start + "
                                + "offset",
                        orientationHelper.getStartAfterPadding() + offset,
                        orientationHelper.getDecoratedStart(child)
                );
            }
            offsetMultiplier *= -1;
        }
    
public voidscrollToPositionWithPredictive(int scrollPosition, int scrollOffset)

        setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL,
                false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE));
        waitFirstLayout();
        mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
            @Override
            void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
                RecyclerView rv = mLayoutManager.mRecyclerView;
                if (state.isPreLayout()) {
                    assertEquals("pending scroll position should still be pending",
                            scrollPosition, mLayoutManager.mPendingScrollPosition);
                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
                        assertEquals("pending scroll position offset should still be pending",
                                scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
                    }
                } else {
                    RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition);
                    assertNotNull("scroll to position should work", vh);
                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
                        assertEquals("scroll offset should be applied properly",
                                mLayoutManager.getPaddingTop() + scrollOffset
                                        + ((RecyclerView.LayoutParams) vh.itemView
                                        .getLayoutParams()).topMargin,
                                mLayoutManager.getDecoratedTop(vh.itemView));
                    }
                }
            }
        };
        mLayoutManager.expectLayouts(2);
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    mAdapter.addAndNotify(0, 1);
                    if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
                        mLayoutManager.scrollToPosition(scrollPosition);
                    } else {
                        mLayoutManager.scrollToPositionWithOffset(scrollPosition,
                                scrollOffset);
                    }

                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }

            }
        });
        mLayoutManager.waitForLayout(2);
        checkForMainThreadException();
    
protected voidsetUp()


    
         
        super.setUp();
        for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
            for (boolean reverseLayout : new boolean[]{false, true}) {
                for (int spanCount : new int[]{1, 3}) {
                    for (int gapStrategy : new int[]{GAP_HANDLING_NONE,
                            GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}) {
                        mBaseVariations.add(new Config(orientation, reverseLayout, spanCount,
                                gapStrategy));
                    }
                }
            }
        }
    
voidsetupByConfig(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        mAdapter = new GridTestAdapter(config.mItemCount, config.mOrientation);
        mRecyclerView = new RecyclerView(getActivity());
        mRecyclerView.setAdapter(mAdapter);
        mRecyclerView.setHasFixedSize(true);
        mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
                config.mOrientation);
        mLayoutManager.setGapStrategy(config.mGapStrategy);
        mLayoutManager.setReverseLayout(config.mReverseLayout);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
                    RecyclerView.State state) {
                try {
                    LayoutParams lp = (LayoutParams) view.getLayoutParams();
                    assertNotNull("view should have layout params assigned", lp);
                    assertNotNull("when item offsets are requested, view should have a valid span",
                            lp.mSpan);
                } catch (Throwable t) {
                    postExceptionToInstrumentation(t);
                }
            }
        });
    
public voidtestAccessibilityPositions()

        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
        waitFirstLayout();
        final AccessibilityDelegateCompat delegateCompat = mRecyclerView
                .getCompatAccessibilityDelegate();
        final AccessibilityEvent event = AccessibilityEvent.obtain();
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
            }
        });
        final AccessibilityRecordCompat record = AccessibilityEventCompat
                .asRecord(event);
        final int start = mRecyclerView
                .getChildLayoutPosition(
                        mLayoutManager.findFirstVisibleItemClosestToStart(false, true));
        final int end = mRecyclerView
                .getChildLayoutPosition(
                        mLayoutManager.findFirstVisibleItemClosestToEnd(false, true));
        assertEquals("first item position should match",
                Math.min(start, end), record.getFromIndex());
        assertEquals("last item position should match",
                Math.max(start, end), record.getToIndex());

    
public voidtestAreAllEndsTheSame()

        setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300));
        waitFirstLayout();
        smoothScrollToPosition(100);
        mLayoutManager.expectLayouts(1);
        mAdapter.deleteAndNotify(0, 2);
        mLayoutManager.waitForLayout(2);
        smoothScrollToPosition(0);
        assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual());
    
public voidtestAreAllStartsTheSame()

        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300));
        waitFirstLayout();
        smoothScrollToPosition(100);
        mLayoutManager.expectLayouts(1);
        mAdapter.deleteAndNotify(0, 2);
        mLayoutManager.waitForLayout(2);
        smoothScrollToPosition(0);
        assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual());
    
public voidtestConsistentRelayout()

        for (Config config : mBaseVariations) {
            for (boolean firstChildMultiSpan : new boolean[]{false, true}) {
                consistentRelayoutTest(config, firstChildMultiSpan);
            }
            removeRecyclerView();
        }
    
public voidtestCustomHeightInVertical()

        customSizeInScrollDirectionTest(
                new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
    
public voidtestCustomWidthInHorizontal()

        customSizeInScrollDirectionTest(
                new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
    
public voidtestFindLastInUnevenDistribution()

        setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
                .itemCount(5));
        mAdapter.mOnBindHandler = new OnBindHandler() {
            @Override
            void onBoundItem(TestViewHolder vh, int position) {
                LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
                if (position == 1) {
                    lp.height = mRecyclerView.getHeight() - 10;
                } else {
                    lp.height = 5;
                }
            }
        };
        waitFirstLayout();
        int[] into = new int[2];
        mLayoutManager.findFirstCompletelyVisibleItemPositions(into);
        assertEquals("first completely visible item from span 0 should be 0", 0, into[0]);
        assertEquals("first completely visible item from span 1 should be 1", 1, into[1]);
        mLayoutManager.findLastCompletelyVisibleItemPositions(into);
        assertEquals("last completely visible item from span 0 should be 4", 4, into[0]);
        assertEquals("last completely visible item from span 1 should be 1", 1, into[1]);
        assertEquals("first fully visible child should be at position",
                0, mRecyclerView.getChildViewHolder(mLayoutManager.
                        findFirstVisibleItemClosestToStart(true, true)).getPosition());
        assertEquals("last fully visible child should be at position",
                4, mRecyclerView.getChildViewHolder(mLayoutManager.
                        findFirstVisibleItemClosestToEnd(true, true)).getPosition());

        assertEquals("first visible child should be at position",
                0, mRecyclerView.getChildViewHolder(mLayoutManager.
                        findFirstVisibleItemClosestToStart(false, true)).getPosition());
        assertEquals("last visible child should be at position",
                4, mRecyclerView.getChildViewHolder(mLayoutManager.
                        findFirstVisibleItemClosestToEnd(false, true)).getPosition());

    
public voidtestFullSizeSpans()

        Config config = new Config().spanCount(5).itemCount(30);
        setupByConfig(config);
        mAdapter.mFullSpanItems.add(3);
        waitFirstLayout();
        assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2},
                new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2},
                new int[]{7, 3}, new int[]{8, 4});
    
public voidtestGapAtTheBeginning()

        for (Config config : mBaseVariations) {
            for (int deleteCount = 1; deleteCount < config.mSpanCount * 2; deleteCount++) {
                for (int deletePosition = config.mSpanCount - 1;
                        deletePosition < config.mSpanCount + 2; deletePosition++) {
                    gapAtTheBeginningOfTheListTest(config, deletePosition, deleteCount);
                    removeRecyclerView();
                }
            }
        }
    
public voidtestGetFirstLastChildrenTest()

        for (boolean provideArr : new boolean[]{true, false}) {
            for (Config config : mBaseVariations) {
                getFirstLastChildrenTest(config, provideArr);
                removeRecyclerView();
            }
        }
    
public voidtestGrowLookup()

        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
        waitFirstLayout();
        mLayoutManager.expectLayouts(1);
        mAdapter.mItems.clear();
        mAdapter.dispatchDataSetChanged();
        mLayoutManager.waitForLayout(2);
        checkForMainThreadException();
        mLayoutManager.expectLayouts(2);
        mAdapter.addAndNotify(0, 30);
        mLayoutManager.waitForLayout(2);
        checkForMainThreadException();
    
public voidtestInnerGapHandling()

        innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
        innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
    
public voidtestLayoutOrder()

        for (Config config : mBaseVariations) {
            layoutOrderTest(config);
            removeRecyclerView();
        }
    
public voidtestMoveGapHandling()

        Config config = new Config().spanCount(2).itemCount(40);
        setupByConfig(config);
        waitFirstLayout();
        mLayoutManager.expectLayouts(2);
        mAdapter.moveAndNotify(4, 1);
        mLayoutManager.waitForLayout(2);
        assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix());
    
public voidtestPartialSpanInvalidation()

        Config config = new Config().spanCount(5).itemCount(100);
        setupByConfig(config);
        for (int i = 20; i < mAdapter.getItemCount(); i += 20) {
            mAdapter.mFullSpanItems.add(i);
        }
        waitFirstLayout();
        smoothScrollToPosition(50);
        int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30];
        mAdapter.changeAndNotify(15, 2);
        Thread.sleep(200);
        assertEquals("Invalidation should happen within full span item boundaries", prevSpanId,
                mLayoutManager.mLazySpanLookup.mData[30]);
        assertEquals("item in invalidated range should have clear span id",
                LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
        smoothScrollToPosition(85);
        int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85);
        mAdapter.deleteAndNotify(55, 2);
        Thread.sleep(200);
        assertEquals("item in invalidated range should have clear span id",
                LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
        int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83);
        assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans,
                newSpans, 0, 0, newSpans.length);
    
public voidtestRTL()

        for (boolean changeRtlAfter : new boolean[]{false, true}) {
            for (Config config : mBaseVariations) {
                rtlTest(config, changeRtlAfter);
                removeRecyclerView();
            }
        }
    
public voidtestSavedState()

        PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{
                new PostLayoutRunnable() {
                    @Override
                    public void run() throws Throwable {
                        // do nothing
                    }

                    @Override
                    public String describe() {
                        return "doing nothing";
                    }
                },
                new PostLayoutRunnable() {
                    @Override
                    public void run() throws Throwable {
                        mLayoutManager.expectLayouts(1);
                        scrollToPosition(mAdapter.getItemCount() * 3 / 4);
                        mLayoutManager.waitForLayout(2);
                    }

                    @Override
                    public String describe() {
                        return "scroll to position " + (mAdapter == null ? "" :
                                mAdapter.getItemCount() * 3 / 4);
                    }
                },
                new PostLayoutRunnable() {
                    @Override
                    public void run() throws Throwable {
                        mLayoutManager.expectLayouts(1);
                        scrollToPositionWithOffset(mAdapter.getItemCount() / 3,
                                50);
                        mLayoutManager.waitForLayout(2);
                    }

                    @Override
                    public String describe() {
                        return "scroll to position " + (mAdapter == null ? "" :
                                mAdapter.getItemCount() / 3) + "with positive offset";
                    }
                },
                new PostLayoutRunnable() {
                    @Override
                    public void run() throws Throwable {
                        mLayoutManager.expectLayouts(1);
                        scrollToPositionWithOffset(mAdapter.getItemCount() * 2 / 3,
                                -50);
                        mLayoutManager.waitForLayout(2);
                    }

                    @Override
                    public String describe() {
                        return "scroll to position with negative offset";
                    }
                }
        };
        boolean[] waitForLayoutOptions = new boolean[]{false, true};
        List<Config> testVariations = new ArrayList<Config>();
        testVariations.addAll(mBaseVariations);
        for (Config config : mBaseVariations) {
            if (config.mSpanCount < 2) {
                continue;
            }
            final Config clone = (Config) config.clone();
            clone.mItemCount = clone.mSpanCount - 1;
            testVariations.add(clone);
        }

        for (Config config : testVariations) {
            for (PostLayoutRunnable runnable : postLayoutOptions) {
                for (boolean waitForLayout : waitForLayoutOptions) {
                    savedStateTest(config, waitForLayout, runnable);
                    removeRecyclerView();
                }
            }
        }
    
public voidtestScrollBackAndPreservePositions()

        for (boolean saveRestore : new boolean[]{false, true}) {
            for (Config config : mBaseVariations) {
                scrollBackAndPreservePositionsTest(config, saveRestore);
                removeRecyclerView();
            }
        }
    
public voidtestScrollBy()

        for (Config config : mBaseVariations) {
            scrollByTest(config);
            removeRecyclerView();
        }
    
public voidtestScrollToPosition()

        for (Config config : mBaseVariations) {
            scrollToPositionTest(config);
            removeRecyclerView();
        }
    
public voidtestScrollToPositionWithOffset()

        for (Config config : mBaseVariations) {
            scrollToPositionWithOffsetTest(config);
            removeRecyclerView();
        }
    
public voidtestScrollToPositionWithPredictive()

        scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
        removeRecyclerView();
        scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
                LinearLayoutManager.INVALID_OFFSET);
        removeRecyclerView();
        scrollToPositionWithPredictive(9, 20);
        removeRecyclerView();
        scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);

    
public voidtestSpanCountChangeOnRestoreSavedState()

        Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE);
        setupByConfig(config);
        waitFirstLayout();

        int beforeChildCount = mLayoutManager.getChildCount();
        Parcelable savedState = mRecyclerView.onSaveInstanceState();
        // we append a suffix to the parcelable to test out of bounds
        String parcelSuffix = UUID.randomUUID().toString();
        Parcel parcel = Parcel.obtain();
        savedState.writeToParcel(parcel, 0);
        parcel.writeString(parcelSuffix);
        removeRecyclerView();
        // reset for reading
        parcel.setDataPosition(0);
        // re-create
        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
        removeRecyclerView();

        RecyclerView restored = new RecyclerView(getActivity());
        mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
        mLayoutManager.setReverseLayout(config.mReverseLayout);
        mLayoutManager.setGapStrategy(config.mGapStrategy);
        restored.setLayoutManager(mLayoutManager);
        // use the same adapter for Rect matching
        restored.setAdapter(mAdapter);
        restored.onRestoreInstanceState(savedState);
        mLayoutManager.setSpanCount(1);
        mLayoutManager.expectLayouts(1);
        setRecyclerView(restored);
        mLayoutManager.waitForLayout(2);
        assertEquals("on saved state, reverse layout should be preserved",
                config.mReverseLayout, mLayoutManager.getReverseLayout());
        assertEquals("on saved state, orientation should be preserved",
                config.mOrientation, mLayoutManager.getOrientation());
        assertEquals("after setting new span count, layout manager should keep new value",
                1, mLayoutManager.getSpanCount());
        assertEquals("on saved state, gap strategy should be preserved",
                config.mGapStrategy, mLayoutManager.getGapStrategy());
        assertTrue("when span count is dramatically changed after restore, # of child views "
                + "should change", beforeChildCount > mLayoutManager.getChildCount());
        // make sure LLM can layout all children. is some span info is leaked, this would crash
        smoothScrollToPosition(mAdapter.getItemCount() - 1);
    
public voidtestSpanReassignmentsOnItemChange()

        Config config = new Config().spanCount(5);
        setupByConfig(config);
        waitFirstLayout();
        smoothScrollToPosition(mAdapter.getItemCount() / 2);
        final int changePosition = mAdapter.getItemCount() / 4;
        mLayoutManager.expectLayouts(1);
        mAdapter.changeAndNotify(changePosition, 1);
        mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated",
                1);
        // delete an item before visible area
        int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2;
        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
        if (DEBUG) {
            Log.d(TAG, "before:");
            for (Map.Entry<Item, Rect> entry : before.entrySet()) {
                Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
            }
        }
        mLayoutManager.expectLayouts(1);
        mAdapter.deleteAndNotify(deletedPosition, 1);
        mLayoutManager.waitForLayout(2);
        assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it "
                        + "should not affect the layout if it is not visible", before,
                mLayoutManager.collectChildCoordinates()
        );
        deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2));
        mLayoutManager.expectLayouts(1);
        mAdapter.deleteAndNotify(deletedPosition, 1);
        mLayoutManager.waitForLayout(2);
        assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the "
                + "layout", before, mLayoutManager.collectChildCoordinates());
    
public voidtestTemporaryGapHandling()

        int fullSpanIndex = 200;
        setupByConfig(new Config().spanCount(2).itemCount(500));
        mAdapter.mFullSpanItems.add(fullSpanIndex);
        waitFirstLayout();
        smoothScrollToPosition(fullSpanIndex + 30);
        mLayoutManager.expectLayouts(1);
        mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
        mLayoutManager.waitForLayout(1);
        smoothScrollToPosition(0);
        mLayoutManager.expectLayouts(1);
        smoothScrollToPosition(fullSpanIndex + 5);
        mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a "
                + "relayout", 2);
        View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);

        View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
        View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);

        LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
        LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
        assertEquals("view 1 span index", 0, lp1.getSpanIndex());
        assertEquals("view 2 span index", 1, lp2.getSpanIndex());
        assertEquals("no gap between span and view 1",
                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
        assertEquals("no gap between span and view 2",
                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
    
public voidtestUpdateAfterFullSpan()

        updateAfterFullSpanGapHandlingTest(0);
    
public voidtestUpdateAfterFullSpan2()

        updateAfterFullSpanGapHandlingTest(20);
    
public voidtestViewSnapping()

        for (Config config : mBaseVariations) {
            viewSnapTest(config.itemCount(config.mSpanCount + 1));
            removeRecyclerView();
        }
    
public voidupdateAfterFullSpanGapHandlingTest(int fullSpanIndex)

        setupByConfig(new Config().spanCount(2).itemCount(100));
        mAdapter.mFullSpanItems.add(fullSpanIndex);
        waitFirstLayout();
        smoothScrollToPosition(fullSpanIndex + 30);
        mLayoutManager.expectLayouts(1);
        mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
        mLayoutManager.waitForLayout(1);
        smoothScrollToPosition(fullSpanIndex);
        // give it some time to fix the gap
        Thread.sleep(500);
        View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);

        View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
        View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);

        LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
        LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
        assertEquals("view 1 span index", 0, lp1.getSpanIndex());
        assertEquals("view 2 span index", 1, lp2.getSpanIndex());
        assertEquals("no gap between span and view 1",
                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
        assertEquals("no gap between span and view 2",
                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
    
public voidviewSnapTest(android.support.v7.widget.StaggeredGridLayoutManagerTest$Config config)

        setupByConfig(config);
        waitFirstLayout();
        // run these tests twice. once initial layout, once after scroll
        String logSuffix = "";
        for (int i = 0; i < 2; i++) {
            Map<Item, Rect> itemRectMap = mLayoutManager.collectChildCoordinates();
            Rect recyclerViewBounds = getDecoratedRecyclerViewBounds();
            Rect usedLayoutBounds = new Rect();
            for (Rect rect : itemRectMap.values()) {
                usedLayoutBounds.union(rect);
            }
            if (DEBUG) {
                Log.d(TAG, "testing view snapping (" + logSuffix + ") for config " + config);
            }
            if (config.mOrientation == VERTICAL) {
                assertEquals(config + " there should be no gap on left" + logSuffix,
                        usedLayoutBounds.left, recyclerViewBounds.left);
                assertEquals(config + " there should be no gap on right" + logSuffix,
                        usedLayoutBounds.right, recyclerViewBounds.right);
                if (config.mReverseLayout) {
                    assertEquals(config + " there should be no gap on bottom" + logSuffix,
                            usedLayoutBounds.bottom, recyclerViewBounds.bottom);
                    assertTrue(config + " there should be some gap on top" + logSuffix,
                            usedLayoutBounds.top > recyclerViewBounds.top);
                } else {
                    assertEquals(config + " there should be no gap on top" + logSuffix,
                            usedLayoutBounds.top, recyclerViewBounds.top);
                    assertTrue(config + " there should be some gap at the bottom" + logSuffix,
                            usedLayoutBounds.bottom < recyclerViewBounds.bottom);
                }
            } else {
                assertEquals(config + " there should be no gap on top" + logSuffix,
                        usedLayoutBounds.top, recyclerViewBounds.top);
                assertEquals(config + " there should be no gap at the bottom" + logSuffix,
                        usedLayoutBounds.bottom, recyclerViewBounds.bottom);
                if (config.mReverseLayout) {
                    assertEquals(config + " there should be no on right" + logSuffix,
                            usedLayoutBounds.right, recyclerViewBounds.right);
                    assertTrue(config + " there should be some gap on left" + logSuffix,
                            usedLayoutBounds.left > recyclerViewBounds.left);
                } else {
                    assertEquals(config + " there should be no gap on left" + logSuffix,
                            usedLayoutBounds.left, recyclerViewBounds.left);
                    assertTrue(config + " there should be some gap on right" + logSuffix,
                            usedLayoutBounds.right < recyclerViewBounds.right);
                }
            }
            final int scroll = config.mReverseLayout ? -500 : 500;
            scrollBy(scroll);
            logSuffix = " scrolled " + scroll;
        }

    
voidwaitFirstLayout()

        mLayoutManager.expectLayouts(1);
        setRecyclerView(mRecyclerView);
        mLayoutManager.waitForLayout(2);
        getInstrumentation().waitForIdleSync();
    
private voidwaitForMainThread(int count)
enqueues an empty runnable to main thread so that we can be assured it did run

param
count Number of times to run

        final AtomicInteger i = new AtomicInteger(count);
        while (i.get() > 0) {
            runTestOnUiThread(new Runnable() {
                @Override
                public void run() {
                    i.decrementAndGet();
                }
            });
        }