StaggeredGridLayoutManager for recycler view

Change-Id: Ie5466806aa0428a9f9f2b9eb6b85dbcfbbaebe5f
diff --git a/v7/recyclerview/src/android/support/v7/widget/LayoutState.java b/v7/recyclerview/src/android/support/v7/widget/LayoutState.java
new file mode 100644
index 0000000..e62a80a
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/LayoutState.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific languag`e governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+import android.view.View;
+
+/**
+ * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
+ * space.
+ */
+class LayoutState {
+
+    final static String TAG = "LayoutState";
+
+    final static int LAYOUT_START = -1;
+
+    final static int LAYOUT_END = 1;
+
+    final static int INVALID_LAYOUT = Integer.MIN_VALUE;
+
+    final static int ITEM_DIRECTION_HEAD = -1;
+
+    final static int ITEM_DIRECTION_TAIL = 1;
+
+    final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
+
+    /**
+     * Number of pixels that we should fill, in the layout direction.
+     */
+    int mAvailable;
+
+    /**
+     * Current position on the adapter to get the next item.
+     */
+    int mCurrentPosition;
+
+    /**
+     * Defines the direction in which the data adapter is traversed.
+     * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
+     */
+    int mItemDirection;
+
+    /**
+     * Defines the direction in which the layout is filled.
+     * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
+     */
+    int mLayoutDirection;
+
+    /**
+     * Used if you want to pre-layout items that are not yet visible.
+     * The difference with {@link #mAvailable} is that, when recycling, distance rendered for
+     * {@link #mExtra} is not considered not to recycle visible children.
+     */
+    int mExtra = 0;
+
+    /**
+     * @return true if there are more items in the data adapter
+     */
+    boolean hasMore(RecyclerView.State state) {
+        return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
+    }
+
+    /**
+     * Gets the view for the next element that we should render.
+     * Also updates current item index to the next item, based on {@link #mItemDirection}
+     *
+     * @return The next element that we should render.
+     */
+    View next(RecyclerView.Recycler recycler) {
+        final View view = recycler.getViewForPosition(mCurrentPosition);
+        mCurrentPosition += mItemDirection;
+        return view;
+    }
+}
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
new file mode 100644
index 0000000..7904f8b
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -0,0 +1,1909 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.List;
+
+
+import static android.support.v7.widget.LayoutState.LAYOUT_START;
+import static android.support.v7.widget.LayoutState.LAYOUT_END;
+import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD;
+import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL;
+import static android.support.v7.widget.OrientationHelper.HORIZONTAL;
+import static android.support.v7.widget.OrientationHelper.VERTICAL;
+
+/**
+ * A LayoutManager that lays out children in a staggered grid formation.
+ * It supports horizontal & vertical layout as well as an ability to layout children in reverse.
+ * <p>
+ * Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps,
+ * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can
+ * control this behavior via {@link #setGapStrategy(int)}.
+ */
+public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager {
+
+    public static final String TAG = "StaggeredGridLayoutManager";
+
+    private static final boolean DEBUG = false;
+
+    /**
+     * Does not do anything to hide gaps
+     */
+    public static final int GAP_HANDLING_NONE = 0;
+
+    /**
+     * Scroll the shorter span slower to avoid gaps in the UI.
+     * <p>
+     * For example, if LayoutManager ends up with the following layout:
+     * <code>
+     * BXC
+     * DEF
+     * </code>
+     * Where B has two spans height, if user scrolls down it will keep the positions of 2nd and 3rd
+     * columns,
+     * which will result in:
+     * <code>
+     * BXC
+     * BEF
+     * </code>
+     * instead of
+     * <code>
+     * B
+     * BEF
+     * </code>
+     */
+    public static final int GAP_HANDLING_LAZY = 1;
+
+    /**
+     * On scroll, LayoutManager checks for a view that is assigned to wrong span.
+     * When such a situation is detected, LayoutManager will wait until scroll is complete and then
+     * move children to their correct spans.
+     * <p>
+     * For example, if LayoutManager ends up with the following layout due to adapter changes:
+     * <code>
+     * AAA
+     * _BC
+     * DDD
+     * </code>
+     * It will animate to the following state:
+     * <code>
+     * AAA
+     * BC_
+     * DDD
+     * </code>
+     */
+    public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2;
+
+    private static final int INVALID_OFFSET = Integer.MIN_VALUE;
+
+    /**
+     * Number of spans
+     */
+    private int mSpanCount = -1;
+
+    private Span[] mSpans;
+
+    /**
+     * Primary orientation is the layout's orientation, secondary orientation is the orientation
+     * for spans. Having both makes code much cleaner for calculations.
+     */
+    OrientationHelper mPrimaryOrientation;
+    OrientationHelper mSecondaryOrientation;
+
+    private int mOrientation;
+
+    /**
+     * The width or height per span, depending on the orientation.
+     */
+    private int mSizePerSpan;
+
+    private LayoutState mLayoutState;
+
+    private boolean mReverseLayout = false;
+
+    /**
+     * Aggregated reverse layout value that takes RTL into account.
+     */
+    private boolean mShouldReverseLayout = false;
+
+    /**
+     * Temporary variable used during fill method to check which spans needs to be filled.
+     */
+    private BitSet mRemainingSpans;
+
+    /**
+     * When LayoutManager needs to scroll to a position, it sets this variable and requests a
+     * layout which will check this variable and re-layout accordingly.
+     */
+    private int mPendingScrollPosition = RecyclerView.NO_POSITION;
+
+    /**
+     * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is
+     * called.
+     */
+    private int mPendingScrollPositionOffset = INVALID_OFFSET;
+
+    /**
+     * Keeps the mapping between the adapter positions and spans. This is necessary to provide
+     * a consistent experience when user scrolls the list.
+     */
+    LazySpanLookup mLazySpanLookup = new LazySpanLookup();
+
+    /**
+     * how we handle gaps in UI.
+     */
+    private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
+
+    /**
+     * This list is used to keep track of views that we should remove. Due to complexity of not
+     * adding views, StaggeredGridLayoutManager keeps a list of children to be removed and removes
+     * them after first layout pass.
+     * TODO this will not be necessary when RV is updated not to require laying out not-added
+     * children
+     */
+    private final List<View> mChildrenToBeRemoved = new ArrayList<View>();
+
+    /**
+     * Saved state needs this information to properly layout on restore.
+     */
+    private boolean mLastLayoutFromEnd;
+
+    /**
+     * SavedState is not handled until a layout happens. This is where we keep it until next
+     * layout.
+     */
+    private SavedState mPendingSavedState;
+
+    /**
+     * If LayoutManager detects an unwanted gap in the layout, it sets this flag which will trigger
+     * a runnable after scrolling ends and will re-check. If invalid view state is still present,
+     * it will request a layout to fix it.
+     */
+    private boolean mHasGaps;
+
+    /**
+     * Creates a StaggeredGridLayoutManager with given parameters.
+     *
+     * @param spanCount   If orientation is vertical, spanCount is number of columns. If
+     *                    orientation is horizontal, spanCount is number of rows.
+     * @param orientation {@link OrientationHelper#VERTICAL} or {@link OrientationHelper#HORIZONTAL}
+     */
+    public StaggeredGridLayoutManager(int spanCount, int orientation) {
+        mOrientation = orientation;
+        setSpanCount(spanCount);
+    }
+
+    @Override
+    public void onScrollStateChanged(int state) {
+        if (state == RecyclerView.SCROLL_STATE_IDLE && mHasGaps) {
+            // re-check for gaps
+            View gapView = hasGapsToFix(0, getChildCount());
+            if (gapView == null) {
+                mHasGaps = false; // yay, gap disappeared :)
+                // We should invalidate positions after the last visible child. No reason to
+                // re-layout.
+                final int lastVisiblePosition = mShouldReverseLayout ? getFirstChildPosition()
+                        : getLastChildPosition();
+                mLazySpanLookup.invalidateAfter(lastVisiblePosition + 1);
+            } else {
+                mLazySpanLookup.invalidateAfter(getPosition(gapView));
+                requestLayout(); // Trigger a re-layout which will fix the layout assignments.
+            }
+        }
+    }
+
+    /**
+     * Sets the number of spans for the layout. This will invalidate all of the span assignments
+     * for Views.
+     * <p>
+     * Calling this method will automatically result in a new layout request unless the spanCount
+     * parameter is equal to current span count.
+     *
+     * @param spanCount Number of spans to layout
+     */
+    public void setSpanCount(int spanCount) {
+        if (mPendingSavedState != null && mPendingSavedState.mSpanCount != spanCount) {
+            // invalidate span info in saved state
+            mPendingSavedState.invalidateSpanInfo();
+            mPendingSavedState.mSpanCount = spanCount;
+        }
+        if (spanCount != mSpanCount) {
+            invalidateSpanAssignments();
+            mSpanCount = spanCount;
+            mRemainingSpans = new BitSet(mSpanCount);
+            mSpans = new Span[mSpanCount];
+            for (int i = 0; i < mSpanCount; i++) {
+                mSpans[i] = new Span(i);
+            }
+            requestLayout();
+        }
+    }
+
+    /**
+     * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep
+     * scroll position.
+     *
+     * @param orientation {@link OrientationHelper#HORIZONTAL} or {@link OrientationHelper#VERTICAL}
+     */
+    public void setOrientation(int orientation) {
+        if (orientation != HORIZONTAL && orientation != VERTICAL) {
+            throw new IllegalArgumentException("invalid orientation.");
+        }
+        if (mPendingSavedState != null && mPendingSavedState.mOrientation != orientation) {
+            // override pending state
+            mPendingSavedState.mOrientation = orientation;
+        }
+        if (orientation == mOrientation) {
+            return;
+        }
+        mOrientation = orientation;
+        if (mPrimaryOrientation != null && mSecondaryOrientation != null) {
+            // swap
+            OrientationHelper tmp = mPrimaryOrientation;
+            mPrimaryOrientation = mSecondaryOrientation;
+            mSecondaryOrientation = tmp;
+        }
+        requestLayout();
+    }
+
+    /**
+     * Sets whether LayoutManager should start laying out items from the end of the UI. The order
+     * items are traversed is not affected by this call.
+     * <p>
+     * This behaves similar to the layout change for RTL views. When set to true, first item is
+     * laid out at the end of the ViewGroup, second item is laid out before it etc.
+     * <p>
+     * For horizontal layouts, it depends on the layout direction.
+     * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if
+     * {@link RecyclerView}} is RTL, it will layout from LTR.
+     *
+     * @param reverseLayout Whether layout should be in reverse or not
+     */
+    public void setReverseLayout(boolean reverseLayout) {
+        if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) {
+            mPendingSavedState.mReverseLayout = reverseLayout;
+        }
+        mReverseLayout = reverseLayout;
+        requestLayout();
+    }
+
+    /**
+     * Returns the current gap handling strategy for StaggeredGridLayoutManager.
+     * <p>
+     * Staggered grid may have gaps in the layout as items may have different sizes. To avoid gaps,
+     * StaggeredGridLayoutManager provides 3 options. Check {@link #GAP_HANDLING_NONE},
+     * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}, {@link #GAP_HANDLING_LAZY} for details.
+     * <p>
+     * By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}.
+     *
+     * @return Current gap handling strategy.
+     * @see #setGapStrategy(int)
+     * @see #GAP_HANDLING_NONE
+     * @see #GAP_HANDLING_LAZY
+     * @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
+     */
+    public int getGapStrategy() {
+        return mGapStrategy;
+    }
+
+    /**
+     * Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter
+     * is different than the current strategy, calling this method will trigger a layout request.
+     *
+     * @param gapStrategy The new gap handling strategy. Should be {@link #GAP_HANDLING_LAZY}
+     *                    , {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or
+     *                    {@link #GAP_HANDLING_NONE}
+     * @see #getGapStrategy()
+     */
+    public void setGapStrategy(int gapStrategy) {
+        if (mPendingSavedState != null && mPendingSavedState.mGapStrategy != gapStrategy) {
+            mPendingSavedState.mGapStrategy = gapStrategy;
+        }
+        if (gapStrategy == mGapStrategy) {
+            return;
+        }
+        if (gapStrategy != GAP_HANDLING_LAZY && gapStrategy != GAP_HANDLING_NONE &&
+                gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
+            throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE "
+                    + ", GAP_HANDLING_LAZY or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS");
+        }
+        mGapStrategy = gapStrategy;
+        requestLayout();
+    }
+
+    /**
+     * Returns the number of spans laid out by StaggeredGridLayoutManager.
+     *
+     * @return Number of spans in the layout
+     */
+    public int getSpanCount() {
+        return mSpanCount;
+    }
+
+    /**
+     * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items.
+     * <p>
+     * If you need to cancel current assignments, you can call this method which will clear all
+     * assignments and request a new layout.
+     */
+    public void invalidateSpanAssignments() {
+        mLazySpanLookup.clear();
+        requestLayout();
+    }
+
+    private void ensureOrientationHelper() {
+        if (mPrimaryOrientation == null) {
+            mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation);
+            mSecondaryOrientation = OrientationHelper
+                    .createOrientationHelper(this, 1 - mOrientation);
+            mLayoutState = new LayoutState();
+        }
+    }
+
+    /**
+     * Calculates the views' layout order. (e.g. from end to start or start to end)
+     * RTL layout support is applied automatically. So if layout is RTL and
+     * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
+     */
+    private void resolveShouldLayoutReverse() {
+        // A == B is the same result, but we rather keep it readable
+        if (mOrientation == VERTICAL || !isLayoutRTL()) {
+            mShouldReverseLayout = mReverseLayout;
+        } else {
+            mShouldReverseLayout = !mReverseLayout;
+        }
+    }
+
+    private boolean isLayoutRTL() {
+        return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
+    }
+
+    /**
+     * Returns whether views are laid out in reverse order or not.
+     * <p>
+     * Not that this value is not affected by RecyclerView's layout direction.
+     *
+     * @return True if layout is reversed, false otherwise
+     * @see #setReverseLayout(boolean)
+     */
+    public boolean getReverseLayout() {
+        return mReverseLayout;
+    }
+
+    @Override
+    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+        ensureOrientationHelper();
+        // Update adapter size.
+        mLazySpanLookup.mAdapterSize = state.getItemCount();
+        int anchorItemPosition;
+        int anchorOffset;
+        // This value may change if we are jumping to a position.
+        boolean layoutFromEnd;
+
+        // If set to true, spans will clear their offsets and they'll be laid out from start
+        // depending on the layout direction. Invalidating span offsets is necessary to be able
+        // to jump to a position.
+        boolean invalidateSpanOffsets = false;
+
+        if (mPendingSavedState != null) {
+            if (DEBUG) {
+                Log.d(TAG, "found saved state: " + mPendingSavedState);
+            }
+            setOrientation(mPendingSavedState.mOrientation);
+            setSpanCount(mPendingSavedState.mSpanCount);
+            setGapStrategy(mPendingSavedState.mGapStrategy);
+            setReverseLayout(mPendingSavedState.mReverseLayout);
+            resolveShouldLayoutReverse();
+
+            if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) {
+                mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
+                layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
+            } else {
+                layoutFromEnd = mShouldReverseLayout;
+            }
+            if (mPendingSavedState.mHasSpanOffsets) {
+                for (int i = 0; i < mSpanCount; i++) {
+                    mSpans[i].clear();
+                    mSpans[i].setLine(mPendingSavedState.mSpanOffsets[i]);
+                }
+            }
+            if (mPendingSavedState.mSpanLookupSize > 1) {
+                mLazySpanLookup.mData = mPendingSavedState.mSpanLookup;
+            }
+
+        } else {
+            resolveShouldLayoutReverse();
+            layoutFromEnd = mShouldReverseLayout; // get updated value.
+        }
+
+        // Validate scroll position if exists.
+        if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
+            // Validate it.
+            if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
+                mPendingScrollPosition = RecyclerView.NO_POSITION;
+                mPendingScrollPositionOffset = INVALID_OFFSET;
+            }
+        }
+
+        if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
+            if (mPendingSavedState == null
+                    || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION
+                    || !mPendingSavedState.mHasSpanOffsets) {
+                // If item is visible, make it fully visible.
+                final View child = findViewByPosition(mPendingScrollPosition);
+                if (child != null) {
+                    if (mPendingScrollPositionOffset != INVALID_OFFSET) {
+                        // Use regular anchor position.
+                        anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
+                                : getFirstChildPosition();
+                        if (layoutFromEnd) {
+                            final int target = mPrimaryOrientation.getEndAfterPadding() -
+                                    mPendingScrollPositionOffset;
+                            anchorOffset = target - mPrimaryOrientation.getDecoratedEnd(child);
+                        } else {
+                            final int target = mPrimaryOrientation.getStartAfterPadding() +
+                                    mPendingScrollPositionOffset;
+                            anchorOffset = target - mPrimaryOrientation.getDecoratedStart(child);
+                        }
+                    } else {
+                        final int startGap = mPrimaryOrientation.getDecoratedStart(child)
+                                - mPrimaryOrientation.getStartAfterPadding();
+                        final int endGap = mPrimaryOrientation.getEndAfterPadding() -
+                                mPrimaryOrientation.getDecoratedEnd(child);
+                        final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child);
+                        // Use regular anchor item, just offset the layout.
+                        anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
+                                : getFirstChildPosition();
+                        if (childSize > mPrimaryOrientation.getTotalSpace()) {
+                            // Item does not fit. Fix depending on layout direction.
+                            anchorOffset = layoutFromEnd ? mPrimaryOrientation.getEndAfterPadding()
+                                    : mPrimaryOrientation.getStartAfterPadding();
+                        } else if (startGap < 0) {
+                            anchorOffset = -startGap;
+                        } else if (endGap < 0) {
+                            anchorOffset = endGap;
+                        } else {
+                            // Nothing to do, just layout normal.
+                            anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
+                                    : getFirstChildPosition();
+                            anchorOffset = INVALID_OFFSET;
+                        }
+                    }
+                } else {
+                    // Child is not visible. Set anchor coordinate depending on in which direction
+                    // child will be visible.
+                    anchorItemPosition = mPendingScrollPosition;
+                    if (mPendingScrollPositionOffset == INVALID_OFFSET) {
+                        final int position = calculateScrollDirectionForPosition(
+                                anchorItemPosition);
+                        if (position == LAYOUT_START) {
+                            anchorOffset = mPrimaryOrientation.getStartAfterPadding();
+                            layoutFromEnd = false;
+                        } else {
+                            anchorOffset = mPrimaryOrientation.getEndAfterPadding();
+                            layoutFromEnd = true;
+                        }
+                    } else {
+                        if (layoutFromEnd) {
+                            anchorOffset = mPrimaryOrientation.getEndAfterPadding()
+                                    - mPendingScrollPositionOffset;
+                        } else {
+                            anchorOffset = mPrimaryOrientation.getStartAfterPadding()
+                                    + mPendingScrollPositionOffset;
+                        }
+                    }
+                    invalidateSpanOffsets = true;
+                }
+            } else {
+                anchorOffset = INVALID_OFFSET;
+                anchorItemPosition = mPendingScrollPosition;
+            }
+
+        } else {
+            // We don't recycle views out of adapter order. This way, we can rely on the first or
+            // last child as the anchor position.
+            anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
+                    : getFirstChildPosition();
+            anchorOffset = INVALID_OFFSET;
+        }
+        if (getChildCount() > 0 && (mPendingSavedState == null
+                || !mPendingSavedState.mHasSpanOffsets)) {
+            if (invalidateSpanOffsets) {
+                for (int i = 0; i < mSpanCount; i++) {
+                    // Scroll to position is set, clear.
+                    mSpans[i].clear();
+                    if (anchorOffset != INVALID_OFFSET) {
+                        mSpans[i].setLine(anchorOffset);
+                    }
+                }
+            } else {
+                for (int i = 0; i < mSpanCount; i++) {
+                    mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout, anchorOffset);
+                }
+                if (DEBUG) {
+                    for (int i = 0; i < mSpanCount; i++) {
+                        Log.d(TAG, "cached start-end lines for " + i + ":" +
+                                mSpans[i].mCachedStart + ":" + mSpans[i].mCachedEnd);
+                    }
+                }
+            }
+        }
+        mSizePerSpan = mSecondaryOrientation.getTotalSpace() / mSpanCount;
+        detachAndScrapAttachedViews(recycler);
+        // Layout start.
+        updateLayoutStateToFillStart(anchorItemPosition, state);
+        if (!layoutFromEnd) {
+            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+        }
+        fill(recycler, mLayoutState, state);
+
+        // Layout end.
+        updateLayoutStateToFillEnd(anchorItemPosition, state);
+        if (layoutFromEnd) {
+            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+        }
+        fill(recycler, mLayoutState, state);
+
+        if (getChildCount() > 0) {
+            if (mShouldReverseLayout) {
+                fixEndGap(recycler, state, true);
+                fixStartGap(recycler, state, false);
+            } else {
+                fixStartGap(recycler, state, true);
+                fixEndGap(recycler, state, false);
+            }
+        }
+
+        // TODO remove this once RecyclerView is updated not to require this.
+        final int removedChildCount = mChildrenToBeRemoved.size();
+        for (int i = removedChildCount - 1; i >= 0; i--) {
+            View view = mChildrenToBeRemoved.get(i);
+            removeView(view);
+        }
+        mChildrenToBeRemoved.clear();
+        mPendingScrollPosition = RecyclerView.NO_POSITION;
+        mPendingScrollPositionOffset = INVALID_OFFSET;
+        mLastLayoutFromEnd = layoutFromEnd;
+        mPendingSavedState = null; // we don't need this anymore
+    }
+
+    /**
+     * Checks if a child is assigned to the non-optimal span.
+     *
+     * @param startChildIndex Starts checking after this child, inclusive
+     * @param endChildIndex   Starts checking until this child, exclusive
+     * @return The first View that is assigned to the wrong span.
+     */
+    View hasGapsToFix(int startChildIndex, int endChildIndex) {
+        // quick reject
+        if (startChildIndex >= endChildIndex) {
+            return null;
+        }
+        final int firstChildIndex, childLimit;
+        final int nextSpanDiff = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1;
+
+        if (mShouldReverseLayout) {
+            firstChildIndex = endChildIndex - 1;
+            childLimit = startChildIndex - 1;
+        } else {
+            firstChildIndex = startChildIndex;
+            childLimit = endChildIndex;
+        }
+        final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1;
+        for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) {
+            View child = getChildAt(i);
+            final int start = mPrimaryOrientation.getDecoratedStart(child);
+            final int end = mPrimaryOrientation.getDecoratedEnd(child);
+            LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+            if (layoutParams.mFullSpan) {
+                continue; // quick reject
+            }
+            int nextSpanIndex = layoutParams.getSpanIndex() + nextSpanDiff;
+            while (nextSpanIndex >= 0 && nextSpanIndex < mSpanCount) {
+                Span nextSpan = mSpans[nextSpanIndex];
+                if (nextSpan.isEmpty(start, end)) {
+                    return child;
+                }
+                nextSpanIndex += nextSpanDiff;
+            }
+        }
+        // everything looks good
+        return null;
+    }
+
+    @Override
+    public boolean supportsPredictiveItemAnimations() {
+        return true;
+    }
+
+    private void measureChildWithDecorationsAndMargin(View child, int widthSpec,
+            int heightSpec) {
+        final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+        LayoutParams lp = (LayoutParams) child.getLayoutParams();
+        widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + insets.left,
+                lp.rightMargin + insets.right);
+        heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + insets.top,
+                lp.bottomMargin + insets.bottom);
+        child.measure(widthSpec, heightSpec);
+    }
+
+    private int updateSpecWithExtra(int spec, int startInset, int endInset) {
+        if (startInset == 0 && endInset == 0) {
+            return spec;
+        }
+        final int mode = View.MeasureSpec.getMode(spec);
+        if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
+            return View.MeasureSpec.makeMeasureSpec(
+                    View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
+        }
+        return spec;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof SavedState) {
+            mPendingSavedState = (SavedState) state;
+            requestLayout();
+        } else if (DEBUG) {
+            Log.d(TAG, "invalid saved state class");
+        }
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        if (mPendingSavedState != null) {
+            return new SavedState(mPendingSavedState);
+        }
+        SavedState state = new SavedState();
+        state.mOrientation = mOrientation;
+        state.mReverseLayout = mReverseLayout;
+        state.mSpanCount = mSpanCount;
+        state.mAnchorLayoutFromEnd = mLastLayoutFromEnd;
+        state.mGapStrategy = mGapStrategy;
+
+        if (mLazySpanLookup != null && mLazySpanLookup.mData != null) {
+            state.mSpanLookup = mLazySpanLookup.mData;
+            state.mSpanLookupSize = state.mSpanLookup.length;
+        } else {
+            state.mSpanLookupSize = 0;
+        }
+
+        if (getChildCount() > 0) {
+            state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition()
+                    : getFirstChildPosition();
+            state.mHasSpanOffsets = true;
+            state.mSpanOffsets = new int[mSpanCount];
+            for (int i = 0; i < mSpanCount; i++) {
+                state.mSpanOffsets[i] = mLastLayoutFromEnd ? mSpans[i].getEndLine()
+                        : mSpans[i].getStartLine();
+            }
+        } else {
+            state.mAnchorPosition = RecyclerView.NO_POSITION;
+            state.mHasSpanOffsets = false;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "saved state:\n" + state);
+        }
+        return state;
+    }
+
+    private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state,
+            boolean canOffsetChildren) {
+        final int maxEndLine = getMaxEnd(mPrimaryOrientation.getEndAfterPadding());
+        int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine;
+        int fixOffset;
+        if (gap > 0) {
+            fixOffset = -scrollBy(-gap, recycler, state);
+        } else {
+            return; // nothing to fix
+        }
+        gap -= fixOffset;
+        if (canOffsetChildren && gap > 0) {
+            mPrimaryOrientation.offsetChildren(gap);
+        }
+    }
+
+    private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state,
+            boolean canOffsetChildren) {
+        final int minStartLine = getMinStart(mPrimaryOrientation.getStartAfterPadding());
+        int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding();
+        int fixOffset;
+        if (gap > 0) {
+            fixOffset = scrollBy(gap, recycler, state);
+        } else {
+            return; // nothing to fix
+        }
+        gap -= fixOffset;
+        if (canOffsetChildren && gap > 0) {
+            mPrimaryOrientation.offsetChildren(-gap);
+        }
+    }
+
+    private void updateLayoutStateToFillStart(int anchorPosition, RecyclerView.State state) {
+        mLayoutState.mAvailable = 0;
+        mLayoutState.mCurrentPosition = anchorPosition;
+        if (isSmoothScrolling()) {
+            final int targetPos = state.getTargetScrollPosition();
+            if (mShouldReverseLayout == targetPos < anchorPosition) {
+                mLayoutState.mExtra = 0;
+            } else {
+                mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace();
+            }
+        } else {
+            mLayoutState.mExtra = 0;
+        }
+        mLayoutState.mLayoutDirection = LAYOUT_START;
+        mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL
+                : ITEM_DIRECTION_HEAD;
+    }
+
+    private void updateLayoutStateToFillEnd(int anchorPosition, RecyclerView.State state) {
+        mLayoutState.mAvailable = 0;
+        mLayoutState.mCurrentPosition = anchorPosition;
+        if (isSmoothScrolling()) {
+            final int targetPos = state.getTargetScrollPosition();
+            if (mShouldReverseLayout == targetPos > anchorPosition) {
+                mLayoutState.mExtra = 0;
+            } else {
+                mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace();
+            }
+        } else {
+            mLayoutState.mExtra = 0;
+        }
+        mLayoutState.mLayoutDirection = LAYOUT_END;
+        mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD
+                : ITEM_DIRECTION_TAIL;
+    }
+
+    @Override
+    public void offsetChildrenHorizontal(int dx) {
+        super.offsetChildrenHorizontal(dx);
+        for (int i = 0; i < mSpanCount; i++) {
+            mSpans[i].onOffset(dx);
+        }
+    }
+
+    @Override
+    public void offsetChildrenVertical(int dy) {
+        super.offsetChildrenVertical(dy);
+        for (int i = 0; i < mSpanCount; i++) {
+            mSpans[i].onOffset(dy);
+        }
+    }
+
+    @Override
+    public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
+        if (!considerSpanInvalidate(positionStart, itemCount)) {
+            // If positions are not invalidated, move span offsets.
+            mLazySpanLookup.offsetForRemoval(positionStart, itemCount);
+        }
+    }
+
+    @Override
+    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+        if (!considerSpanInvalidate(positionStart, itemCount)) {
+            // If positions are not invalidated, move span offsets.
+            mLazySpanLookup.offsetForAddition(positionStart, itemCount);
+        }
+    }
+
+    /**
+     * Checks whether it should invalidate span assignments in response to an adapter change.
+     */
+    private boolean considerSpanInvalidate(int positionStart, int itemCount) {
+        int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition();
+        if (positionStart + itemCount <= minPosition) {
+            return false;// nothing to update.
+        }
+        int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition();
+        mLazySpanLookup.invalidateAfter(positionStart);
+        if (positionStart <= maxPosition) {
+            requestLayout();
+        }
+        return true;
+    }
+
+    private int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
+            RecyclerView.State state) {
+        mRemainingSpans.set(0, mSpanCount, true);
+        // The target position we are trying to reach.
+        final int targetLine;
+
+        /*
+        * The line until which we can recycle, as long as we add views.
+        * Keep in mind, it is still the line in layout direction which means; to calculate the
+        * actual recycle line, we should subtract/add the size in orientation.
+        */
+        final int recycleLine;
+        // Line of the furthest row.
+        if (layoutState.mLayoutDirection == LAYOUT_END) {
+            recycleLine = mPrimaryOrientation.getEndAfterPadding() + mLayoutState.mAvailable;
+            targetLine = recycleLine + mLayoutState.mExtra;
+            final int defaultLine = mPrimaryOrientation.getStartAfterPadding();
+            for (int i = 0; i < mSpanCount; i++) {
+                final Span span = mSpans[i];
+                final int line = span.getEndLine(defaultLine);
+                if (line > targetLine) {
+                    mRemainingSpans.set(i, false);
+                }
+            }
+        } else { // LAYOUT_START
+            recycleLine = mPrimaryOrientation.getStartAfterPadding() - mLayoutState.mAvailable;
+            targetLine = recycleLine - mLayoutState.mExtra;
+            for (int i = 0; i < mSpanCount; i++) {
+                final Span span = mSpans[i];
+                final int defaultLine = mPrimaryOrientation.getEndAfterPadding();
+                final int line = span.getStartLine(defaultLine);
+                if (line < targetLine) {
+                    mRemainingSpans.set(i, false);
+                }
+            }
+        }
+
+        final int widthSpec, heightSpec;
+        if (mOrientation == VERTICAL) {
+            widthSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY);
+            heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+        } else {
+            heightSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY);
+            widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+        }
+
+        while (layoutState.hasMore(state) && !mRemainingSpans.isEmpty()) {
+            View view = layoutState.next(recycler);
+            LayoutParams lp = ((LayoutParams) view.getLayoutParams());
+            if (layoutState.mLayoutDirection == LAYOUT_END) {
+                addView(view);
+            } else {
+                addView(view, 0);
+            }
+            if (lp.isItemRemoved()) {
+                mChildrenToBeRemoved.add(view);
+            }
+            if (lp.mFullSpan) {
+                final int fullSizeSpec = View.MeasureSpec.makeMeasureSpec(
+                        mSecondaryOrientation.getTotalSpace(), View.MeasureSpec.EXACTLY);
+                if (mOrientation == VERTICAL) {
+                    measureChildWithDecorationsAndMargin(view, fullSizeSpec, heightSpec);
+                } else {
+                    measureChildWithDecorationsAndMargin(view, widthSpec, fullSizeSpec);
+                }
+            } else {
+                measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
+            }
+
+            final int position = getPosition(view);
+            final int spanIndex = mLazySpanLookup.getSpan(position);
+            Span currentSpan;
+            if (spanIndex == LayoutParams.INVALID_SPAN_ID) {
+                if (lp.mFullSpan) {
+                    // assign full span items to first span
+                    currentSpan = mSpans[0];
+                } else {
+                    currentSpan = getNextSpan(layoutState);
+                }
+                mLazySpanLookup.setSpan(position, currentSpan);
+            } else {
+                currentSpan = mSpans[spanIndex];
+            }
+            final int start;
+            final int end;
+            if (layoutState.mLayoutDirection == LAYOUT_END) {
+                final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding()
+                        : mPrimaryOrientation.getStartAfterPadding();
+                start = lp.mFullSpan ? getMaxEnd(def) : currentSpan.getEndLine(def);
+                end = start + mPrimaryOrientation.getDecoratedMeasurement(view);
+                if (lp.mFullSpan) {
+                    for (int i = 0; i < mSpanCount; i++) {
+                        mSpans[i].appendToSpan(view);
+                    }
+                } else {
+                    currentSpan.appendToSpan(view);
+                }
+            } else {
+                final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding()
+                        : mPrimaryOrientation.getStartAfterPadding();
+                end = lp.mFullSpan ? getMinStart(def) : currentSpan.getStartLine(def);
+                start = end - mPrimaryOrientation.getDecoratedMeasurement(view);
+                if (lp.mFullSpan) {
+                    for (int i = 0; i < mSpanCount; i++) {
+                        mSpans[i].prependToSpan(view);
+                    }
+                } else {
+                    currentSpan.prependToSpan(view);
+                }
+
+            }
+            lp.mSpan = currentSpan;
+
+            if (DEBUG) {
+                Log.d(TAG, "adding view item " + lp.getViewPosition() + " between " + start + ","
+                        + end);
+            }
+
+            final int otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding()
+                    : currentSpan.mIndex * mSizePerSpan + mSecondaryOrientation
+                            .getStartAfterPadding();
+            final int otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view);
+            if (mOrientation == VERTICAL) {
+                layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end);
+            } else {
+                layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd);
+            }
+            if (lp.mFullSpan) {
+                for (int i = 0; i < mSpanCount; i++) {
+                    updateRemainingSpans(mSpans[i], mLayoutState.mLayoutDirection, targetLine);
+                }
+            } else {
+                updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine);
+            }
+            if (mLayoutState.mLayoutDirection == LAYOUT_START) {
+                // calculate recycle line
+                int maxStart = getMaxStart(currentSpan.getStartLine());
+                recycleFromEnd(recycler, Math.max(recycleLine, maxStart)
+                        + mPrimaryOrientation.getTotalSpace());
+            } else {
+                // calculate recycle line
+                int minEnd = getMinEnd(currentSpan.getEndLine());
+                recycleFromStart(recycler, Math.min(recycleLine, minEnd)
+                        - mPrimaryOrientation.getTotalSpace());
+            }
+        }
+        if (DEBUG) {
+            Log.d(TAG, "fill, " + getChildCount());
+        }
+        if (mLayoutState.mLayoutDirection == LAYOUT_START) {
+            final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding());
+            return Math.max(0, mLayoutState.mAvailable + (targetLine - minStart));
+        } else {
+            final int max = getMaxEnd(mPrimaryOrientation.getEndAfterPadding());
+            return Math.max(0, mLayoutState.mAvailable + (max - targetLine));
+        }
+    }
+
+    private void layoutDecoratedWithMargins(View child, int left, int top, int right,
+            int bottom) {
+        LayoutParams lp = (LayoutParams) child.getLayoutParams();
+        layoutDecorated(child, left + lp.leftMargin, top + lp.topMargin, right - lp.rightMargin
+                , bottom - lp.bottomMargin);
+    }
+
+    private void updateRemainingSpans(Span span, int layoutDir, int targetLine) {
+        final int deletedSize = span.getDeletedSize();
+        if (layoutDir == LAYOUT_START) {
+            final int line = span.getStartLine();
+            if (line + deletedSize < targetLine) {
+                mRemainingSpans.set(span.mIndex, false);
+            }
+        } else {
+            final int line = span.getEndLine();
+            if (line - deletedSize > targetLine) {
+                mRemainingSpans.set(span.mIndex, false);
+            }
+        }
+    }
+
+    private int getMaxStart(int def) {
+        int maxStart = mSpans[0].getStartLine(def);
+        for (int i = 1; i < mSpanCount; i++) {
+            final int spanStart = mSpans[i].getStartLine(def);
+            if (spanStart > maxStart) {
+                maxStart = spanStart;
+            }
+        }
+        return maxStart;
+    }
+
+    private int getMinStart(int def) {
+        int minStart = mSpans[0].getStartLine(def);
+        for (int i = 1; i < mSpanCount; i++) {
+            final int spanStart = mSpans[i].getStartLine(def);
+            if (spanStart < minStart) {
+                minStart = spanStart;
+            }
+        }
+        return minStart;
+    }
+
+    private int getMaxEnd(int def) {
+        int maxEnd = mSpans[0].getEndLine(def);
+        for (int i = 1; i < mSpanCount; i++) {
+            final int spanEnd = mSpans[i].getEndLine(def);
+            if (spanEnd > maxEnd) {
+                maxEnd = spanEnd;
+            }
+        }
+        return maxEnd;
+    }
+
+    private int getMinEnd(int def) {
+        int minEnd = mSpans[0].getEndLine(def);
+        for (int i = 1; i < mSpanCount; i++) {
+            final int spanEnd = mSpans[i].getEndLine(def);
+            if (spanEnd < minEnd) {
+                minEnd = spanEnd;
+            }
+        }
+        return minEnd;
+    }
+
+    private void recycleFromStart(RecyclerView.Recycler recycler, int line) {
+        if (DEBUG) {
+            Log.d(TAG, "recycling from start for line " + line);
+        }
+        while (getChildCount() > 0) {
+            View child = getChildAt(0);
+            if (mPrimaryOrientation.getDecoratedEnd(child) < line) {
+                LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp.mFullSpan) {
+                    for (int j = 0; j < mSpanCount; j++) {
+                        mSpans[j].popStart();
+                    }
+                } else {
+                    lp.mSpan.popStart();
+                }
+                removeAndRecycleView(child, recycler);
+            } else {
+                return;// done
+            }
+        }
+    }
+
+    private void recycleFromEnd(RecyclerView.Recycler recycler, int line) {
+        final int childCount = getChildCount();
+        int i;
+        for (i = childCount - 1; i >= 0; i--) {
+            View child = getChildAt(i);
+            if (mPrimaryOrientation.getDecoratedStart(child) > line) {
+                LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp.mFullSpan) {
+                    for (int j = 0; j < mSpanCount; j++) {
+                        mSpans[j].popEnd();
+                    }
+                } else {
+                    lp.mSpan.popEnd();
+                }
+                removeAndRecycleView(child, recycler);
+            } else {
+                return;// done
+            }
+        }
+    }
+
+    /**
+     * Finds the span for the next view.
+     */
+    private Span getNextSpan(LayoutState layoutState) {
+        final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL();
+        if (layoutState.mLayoutDirection == LAYOUT_END) {
+            Span min = mSpans[0];
+            int minLine = min.getEndLine(mPrimaryOrientation.getStartAfterPadding());
+            final int defaultLine = mPrimaryOrientation.getStartAfterPadding();
+            for (int i = 1; i < mSpanCount; i++) {
+                final Span other = mSpans[i];
+                final int otherLine = other.getEndLine(defaultLine);
+                if (otherLine < minLine || (otherLine == minLine && preferLastSpan)) {
+                    min = other;
+                    minLine = otherLine;
+                }
+            }
+            return min;
+        } else {
+            Span max = mSpans[0];
+            int maxLine = max.getStartLine(mPrimaryOrientation.getEndAfterPadding());
+            final int defaultLine = mPrimaryOrientation.getEndAfterPadding();
+            for (int i = 1; i < mSpanCount; i++) {
+                final Span other = mSpans[i];
+                final int otherLine = other.getStartLine(defaultLine);
+                if (otherLine > maxLine || (otherLine == maxLine && !preferLastSpan)) {
+                    max = other;
+                    maxLine = otherLine;
+                }
+            }
+            return max;
+        }
+    }
+
+    @Override
+    public boolean canScrollVertically() {
+        return mOrientation == VERTICAL;
+    }
+
+    @Override
+    public boolean canScrollHorizontally() {
+        return mOrientation == HORIZONTAL;
+    }
+
+    @Override
+    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
+            RecyclerView.State state) {
+        return scrollBy(dx, recycler, state);
+    }
+
+    @Override
+    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
+            RecyclerView.State state) {
+        return scrollBy(dy, recycler, state);
+    }
+
+    private int calculateScrollDirectionForPosition(int position) {
+        if (getChildCount() == 0) {
+            return mShouldReverseLayout ? LAYOUT_END : LAYOUT_START;
+        }
+        final int firstChildPos = getFirstChildPosition();
+        return position < firstChildPos != mShouldReverseLayout ? LAYOUT_START : LAYOUT_END;
+    }
+
+    @Override
+    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
+            int position) {
+        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
+            @Override
+            public PointF computeScrollVectorForPosition(int targetPosition) {
+                final int direction = calculateScrollDirectionForPosition(targetPosition);
+                if (direction == 0) {
+                    return null;
+                }
+                if (mOrientation == HORIZONTAL) {
+                    return new PointF(direction, 0);
+                } else {
+                    return new PointF(0, direction);
+                }
+            }
+        };
+        scroller.setTargetPosition(position);
+        startSmoothScroll(scroller);
+    }
+
+    @Override
+    public void scrollToPosition(int position) {
+        if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) {
+            mPendingSavedState.invalidateAnchorPositionInfo();
+        }
+        mPendingScrollPosition = position;
+        mPendingScrollPositionOffset = INVALID_OFFSET;
+        requestLayout();
+    }
+
+    /**
+     * Scroll to the specified adapter position with the given offset from layout start.
+     * <p>
+     * Note that scroll position change will not be reflected until the next layout call.
+     * <p>
+     * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
+     *
+     * @param position Index (starting at 0) of the reference item.
+     * @param offset   The distance (in pixels) between the start edge of the item view and
+     *                 start edge of the RecyclerView.
+     * @see #setReverseLayout(boolean)
+     * @see #scrollToPosition(int)
+     */
+    public void scrollToPositionWithOffset(int position, int offset) {
+        if (mPendingSavedState != null) {
+            mPendingSavedState.invalidateAnchorPositionInfo();
+        }
+        mPendingScrollPosition = position;
+        mPendingScrollPositionOffset = offset;
+        requestLayout();
+    }
+
+    private int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
+        ensureOrientationHelper();
+        final int referenceChildPosition;
+        if (dt > 0) { // layout towards end
+            mLayoutState.mLayoutDirection = LAYOUT_END;
+            mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD
+                    : ITEM_DIRECTION_TAIL;
+            referenceChildPosition = getLastChildPosition();
+        } else {
+            mLayoutState.mLayoutDirection = LAYOUT_START;
+            mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL
+                    : ITEM_DIRECTION_HEAD;
+            referenceChildPosition = getFirstChildPosition();
+        }
+        mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
+        final int absDt = Math.abs(dt);
+        mLayoutState.mAvailable = absDt;
+        mLayoutState.mExtra = isSmoothScrolling() ? mPrimaryOrientation.getTotalSpace() : 0;
+        int consumed = fill(recycler, mLayoutState, state);
+        final int totalScroll;
+        if (absDt < consumed) {
+            totalScroll = dt;
+        } else if (dt < 0) {
+            totalScroll = -consumed;
+        } else { // dt > 0
+            totalScroll = consumed;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "asked " + dt + " scrolled" + totalScroll);
+        }
+
+        if (mGapStrategy == GAP_HANDLING_LAZY
+                && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD) {
+            final int targetStart = mPrimaryOrientation.getStartAfterPadding();
+            final int targetEnd = mPrimaryOrientation.getEndAfterPadding();
+            lazyOffsetSpans(-totalScroll, targetStart, targetEnd);
+        } else {
+            mPrimaryOrientation.offsetChildren(-totalScroll);
+        }
+        // always reset this if we scroll for a proper save instance state
+        mLastLayoutFromEnd = mShouldReverseLayout;
+
+        if (totalScroll != 0 && mGapStrategy != GAP_HANDLING_NONE
+                && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD && !mHasGaps) {
+            final int addedChildCount = Math.abs(mLayoutState.mCurrentPosition
+                    - (referenceChildPosition + mLayoutState.mItemDirection));
+            if (addedChildCount > 0) {
+                // check if any child has been attached to wrong span. If so, trigger a re-layout
+                // after scroll
+                final View viewInWrongSpan;
+                final View referenceView = findViewByPosition(referenceChildPosition);
+                if (referenceView == null) {
+                    viewInWrongSpan = hasGapsToFix(0, getChildCount());
+                } else {
+                    if (mLayoutState.mLayoutDirection == LAYOUT_START) {
+                        viewInWrongSpan = hasGapsToFix(0, addedChildCount);
+                    } else {
+                        viewInWrongSpan = hasGapsToFix(getChildCount() - addedChildCount,
+                                getChildCount());
+                    }
+                }
+                mHasGaps = viewInWrongSpan != null;
+            }
+        }
+        return totalScroll;
+    }
+
+    /**
+     * The actual method that implements {@link #GAP_HANDLING_LAZY}
+     */
+    private void lazyOffsetSpans(int offset, int targetStart, int targetEnd) {
+        // For each span offset children one by one.
+        // When a fullSpan item is reached, stop and wait for other spans to reach to that span.
+        // When all reach, offset fullSpan to max of others and continue.
+        int childrenToOffset = getChildCount();
+        int[] indexPerSpan = new int[mSpanCount];
+        int[] offsetPerSpan = new int[mSpanCount];
+
+        final int childOrder = offset > 0 ? ITEM_DIRECTION_TAIL : ITEM_DIRECTION_HEAD;
+        if (offset > 0) {
+            Arrays.fill(indexPerSpan, 0);
+        } else {
+            for (int i = 0; i < mSpanCount; i++) {
+                indexPerSpan[i] = mSpans[i].mViews.size() - 1;
+            }
+        }
+
+        for (int i = 0; i < mSpanCount; i++) {
+            offsetPerSpan[i] = mSpans[i].getNormalizedOffset(offset, targetStart, targetEnd);
+        }
+        if (DEBUG) {
+            Log.d(TAG, "lazy offset start. normalized: " + Arrays.toString(offsetPerSpan));
+        }
+
+        while (childrenToOffset > 0) {
+            View fullSpanView = null;
+            for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) {
+                Span span = mSpans[spanIndex];
+                int viewIndex;
+                for (viewIndex = indexPerSpan[spanIndex];
+                        viewIndex < span.mViews.size() && viewIndex >= 0; viewIndex += childOrder) {
+                    View view = span.mViews.get(viewIndex);
+                    if (DEBUG) {
+                        Log.d(TAG, "span " + spanIndex + ", view:" + viewIndex + ", pos:"
+                                + getPosition(view));
+                    }
+                    LayoutParams lp = (LayoutParams) view.getLayoutParams();
+                    if (lp.mFullSpan) {
+                        if (DEBUG) {
+                            Log.d(TAG, "stopping on full span view on index " + viewIndex
+                                    + " in span " + spanIndex);
+                        }
+                        fullSpanView = view;
+                        viewIndex += childOrder;// move to next view
+                        break;
+                    }
+                    // offset this child normally
+                    mPrimaryOrientation.offsetChild(view, offsetPerSpan[spanIndex]);
+                    final int nextChildIndex = viewIndex + childOrder;
+                    if (nextChildIndex < span.mViews.size() && nextChildIndex >= 0) {
+                        View nextView = span.mViews.get(nextChildIndex);
+                        // find gap between, before offset
+                        if (childOrder == ITEM_DIRECTION_HEAD) {// negative
+                            offsetPerSpan[spanIndex] = Math
+                                    .min(0, mPrimaryOrientation.getDecoratedStart(view)
+                                            - mPrimaryOrientation.getDecoratedEnd(nextView));
+                        } else {
+                            offsetPerSpan[spanIndex] = Math
+                                    .max(0, mPrimaryOrientation.getDecoratedEnd(view) -
+                                            mPrimaryOrientation.getDecoratedStart(nextView));
+                        }
+                        if (DEBUG) {
+                            Log.d(TAG, "offset diff:" + offsetPerSpan[spanIndex] + " between "
+                                    + getPosition(nextView) + " and " + getPosition(view));
+                        }
+                    }
+                    childrenToOffset--;
+                }
+                indexPerSpan[spanIndex] = viewIndex;
+            }
+            if (fullSpanView != null) {
+                // we have to offset this view. We'll offset it as the biggest amount necessary
+                int winnerSpan = 0;
+                int winnerSpanOffset = Math.abs(offsetPerSpan[winnerSpan]);
+                for (int i = 1; i < mSpanCount; i++) {
+                    final int spanOffset = Math.abs(offsetPerSpan[i]);
+                    if (spanOffset > winnerSpanOffset) {
+                        winnerSpan = i;
+                        winnerSpanOffset = spanOffset;
+                    }
+                }
+                if (DEBUG) {
+                    Log.d(TAG, "winner offset:" + offsetPerSpan[winnerSpan] + " of " + winnerSpan);
+                }
+                mPrimaryOrientation.offsetChild(fullSpanView, offsetPerSpan[winnerSpan]);
+                childrenToOffset--;
+
+                for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) {
+                    final int nextViewIndex = indexPerSpan[spanIndex];
+                    final Span span = mSpans[spanIndex];
+                    if (nextViewIndex < span.mViews.size() && nextViewIndex > 0) {
+                        View nextView = span.mViews.get(nextViewIndex);
+                        // find gap between, before offset
+                        if (childOrder == ITEM_DIRECTION_HEAD) {// negative
+                            offsetPerSpan[spanIndex] = Math
+                                    .min(0, mPrimaryOrientation.getDecoratedStart(fullSpanView)
+                                            - mPrimaryOrientation.getDecoratedEnd(nextView));
+                        } else {
+                            offsetPerSpan[spanIndex] = Math
+                                    .max(0, mPrimaryOrientation.getDecoratedEnd(fullSpanView) -
+                                            mPrimaryOrientation.getDecoratedStart(nextView));
+                        }
+                    }
+                }
+            }
+        }
+        for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) {
+            mSpans[spanIndex].invalidateCache();
+        }
+    }
+
+    private int getLastChildPosition() {
+        final int childCount = getChildCount();
+        return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1));
+    }
+
+    private int getFirstChildPosition() {
+        final int childCount = getChildCount();
+        return childCount == 0 ? 0 : getPosition(getChildAt(0));
+    }
+
+    @Override
+    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
+        return new LayoutParams(c, attrs);
+    }
+
+    @Override
+    public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+        if (lp instanceof ViewGroup.MarginLayoutParams) {
+            return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
+        } else {
+            return new LayoutParams(lp);
+        }
+    }
+
+    @Override
+    public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
+        return lp instanceof LayoutParams;
+    }
+
+    public int getOrientation() {
+        return mOrientation;
+    }
+
+
+    /**
+     * LayoutParams used by StaggeredGridLayoutManager.
+     */
+    public static class LayoutParams extends RecyclerView.LayoutParams {
+
+        /**
+         * Span Id for Views that are not laid out yet.
+         */
+        public static final int INVALID_SPAN_ID = -1;
+
+        // Package scope to be able to access from tests.
+        Span mSpan;
+
+        boolean mFullSpan;
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+
+        public LayoutParams(ViewGroup.MarginLayoutParams source) {
+            super(source);
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+
+        public LayoutParams(RecyclerView.LayoutParams source) {
+            super(source);
+        }
+
+        /**
+         * When set to true, the item will layout using all span area. That means, if orientation
+         * is vertical, the view will have full width; if orientation is horizontal, the view will
+         * have full height.
+         *
+         * @param fullSpan True if this item should traverse all spans.
+         */
+        public void setFullSpan(boolean fullSpan) {
+            mFullSpan = fullSpan;
+        }
+
+        /**
+         * Returns the Span index to which this View is assigned.
+         *
+         * @return The Span index of the View. If View is not yet assigned to any span, returns
+         * {@link #INVALID_SPAN_ID}.
+         */
+        public final int getSpanIndex() {
+            if (mSpan == null) {
+                return INVALID_SPAN_ID;
+            }
+            return mSpan.mIndex;
+        }
+    }
+
+    // Package scoped to access from tests.
+    class Span {
+
+        final int INVALID_LINE = Integer.MIN_VALUE;
+
+        private ArrayList<View> mViews = new ArrayList<View>();
+
+        int mCachedStart = INVALID_LINE;
+
+        int mCachedEnd = INVALID_LINE;
+
+        int mDeletedSize = 0;
+
+        final int mIndex;
+
+        private Span(int index) {
+            mIndex = index;
+        }
+
+        int getStartLine(int def) {
+            if (mCachedStart != INVALID_LINE) {
+                return mCachedStart;
+            }
+            if (mViews.size() == 0) {
+                return def;
+            }
+            mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0));
+            return mCachedStart;
+        }
+
+        // Use this one when default value does not make sense and not having a value means a bug.
+        int getStartLine() {
+            if (mCachedStart != INVALID_LINE) {
+                return mCachedStart;
+            }
+            mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0));
+            return mCachedStart;
+        }
+
+        int getEndLine(int def) {
+            if (mCachedEnd != INVALID_LINE) {
+                return mCachedEnd;
+            }
+            final int size = mViews.size();
+            if (size == 0) {
+                return def;
+            }
+            mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(size - 1));
+            return mCachedEnd;
+        }
+
+        // Use this one when default value does not make sense and not having a value means a bug.
+        int getEndLine() {
+            if (mCachedEnd != INVALID_LINE) {
+                return mCachedEnd;
+            }
+            mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(mViews.size() - 1));
+            return mCachedEnd;
+        }
+
+        void prependToSpan(View view) {
+            LayoutParams lp = getLayoutParams(view);
+            lp.mSpan = this;
+            mViews.add(0, view);
+            mCachedStart = INVALID_LINE;
+            if (mViews.size() == 1) {
+                mCachedEnd = INVALID_LINE;
+            }
+            if (lp.isItemRemoved()) {
+                mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
+            }
+        }
+
+        void appendToSpan(View view) {
+            LayoutParams lp = getLayoutParams(view);
+            lp.mSpan = this;
+            mViews.add(view);
+            mCachedEnd = INVALID_LINE;
+            if (mViews.size() == 1) {
+                mCachedStart = INVALID_LINE;
+            }
+            if (lp.isItemRemoved()) {
+                mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
+            }
+        }
+
+        // Useful method to preserve positions on a re-layout.
+        void cacheReferenceLineAndClear(boolean reverseLayout, int offset) {
+            int reference;
+            if (reverseLayout) {
+                reference = getEndLine(INVALID_LINE);
+            } else {
+                reference = getStartLine(INVALID_LINE);
+            }
+            clear();
+            if (reference == INVALID_LINE) {
+                return;
+            }
+            if (offset != INVALID_OFFSET) {
+                reference += offset;
+            }
+            mCachedStart = mCachedEnd = reference;
+        }
+
+        void clear() {
+            mViews.clear();
+            invalidateCache();
+            mDeletedSize = 0;
+        }
+
+        void invalidateCache() {
+            mCachedStart = INVALID_LINE;
+            mCachedEnd = INVALID_LINE;
+        }
+
+        void setLine(int line) {
+            mCachedEnd = mCachedStart = line;
+        }
+
+        void popEnd() {
+            final int size = mViews.size();
+            View end = mViews.remove(size - 1);
+            final LayoutParams lp = getLayoutParams(end);
+            lp.mSpan = null;
+            if (lp.isItemRemoved()) {
+                mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end);
+            }
+            if (size == 1) {
+                mCachedStart = INVALID_LINE;
+            }
+            mCachedEnd = INVALID_LINE;
+        }
+
+        void popStart() {
+            View start = mViews.remove(0);
+            final LayoutParams lp = getLayoutParams(start);
+            lp.mSpan = null;
+            if (mViews.size() == 0) {
+                mCachedEnd = INVALID_LINE;
+            }
+            if (lp.isItemRemoved()) {
+                mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start);
+            }
+            mCachedStart = INVALID_LINE;
+        }
+
+        // TODO cache this.
+        public int getDeletedSize() {
+            return mDeletedSize;
+        }
+
+        LayoutParams getLayoutParams(View view) {
+            return (LayoutParams) view.getLayoutParams();
+        }
+
+        void onOffset(int dt) {
+            if (mCachedStart != INVALID_LINE) {
+                mCachedStart += dt;
+            }
+            if (mCachedEnd != INVALID_LINE) {
+                mCachedEnd += dt;
+            }
+        }
+
+        // normalized offset is how much this span can scroll
+        int getNormalizedOffset(int dt, int targetStart, int targetEnd) {
+            if (mViews.size() == 0) {
+                return 0;
+            }
+            if (dt < 0) {
+                final int endSpace = getEndLine() - targetEnd;
+                if (endSpace <= 0) {
+                    return 0;
+                }
+                return -dt > endSpace ? -endSpace : dt;
+            } else {
+                final int startSpace = targetStart - getStartLine();
+                if (startSpace <= 0) {
+                    return 0;
+                }
+                return startSpace < dt ? startSpace : dt;
+            }
+        }
+
+        /**
+         * Returns if there is no child between start-end lines
+         *
+         * @param start The start line
+         * @param end   The end line
+         * @return true if a new child can be added between start and end
+         */
+        boolean isEmpty(int start, int end) {
+            final int count = mViews.size();
+            for (int i = 0; i < count; i++) {
+                final View view = mViews.get(i);
+                if (mPrimaryOrientation.getDecoratedStart(view) < end &&
+                        mPrimaryOrientation.getDecoratedEnd(view) > start) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    /**
+     * An array of mappings from adapter position to span.
+     * This only grows when a write happens and it grows up to the size of the adapter.
+     */
+    static class LazySpanLookup {
+
+        private static final int MIN_SIZE = 10;
+
+        int[] mData;
+
+        int mAdapterSize; // we don't want to grow beyond that, unless it grows
+
+        void invalidateAfter(int position) {
+            if (mData == null) {
+                return;
+            }
+            if (position >= mData.length) {
+                return;
+            }
+            Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID);
+        }
+
+        int getSpan(int position) {
+            if (mData == null || position >= mData.length) {
+                return LayoutParams.INVALID_SPAN_ID;
+            } else {
+                return mData[position];
+            }
+        }
+
+        void setSpan(int position, Span span) {
+            ensureSize(position);
+            mData[position] = span.mIndex;
+        }
+
+        int sizeForPosition(int position) {
+            int len = mData.length;
+            while (len <= position) {
+                len *= 2;
+            }
+            if (len > mAdapterSize) {
+                len = mAdapterSize;
+            }
+            return len;
+        }
+
+        void ensureSize(int position) {
+            if (mData == null) {
+                mData = new int[Math.max(position, MIN_SIZE) + 1];
+                Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
+            } else if (position >= mData.length) {
+                int[] old = mData;
+                mData = new int[sizeForPosition(position)];
+                System.arraycopy(old, 0, mData, 0, old.length);
+                Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID);
+            }
+        }
+
+        void clear() {
+            if (mData != null) {
+                Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
+            }
+        }
+
+        void offsetForRemoval(int positionStart, int itemCount) {
+            ensureSize(positionStart + itemCount);
+            System.arraycopy(mData, positionStart + itemCount, mData, positionStart,
+                    mData.length - positionStart - itemCount);
+            Arrays.fill(mData, mData.length - itemCount, mData.length,
+                    LayoutParams.INVALID_SPAN_ID);
+        }
+
+        void offsetForAddition(int positionStart, int itemCount) {
+            ensureSize(positionStart + itemCount);
+            System.arraycopy(mData, positionStart, mData, positionStart + itemCount,
+                    mData.length - positionStart - itemCount);
+            Arrays.fill(mData, positionStart, positionStart + itemCount,
+                    LayoutParams.INVALID_SPAN_ID);
+        }
+    }
+
+    static class SavedState implements Parcelable {
+
+        int mOrientation;
+
+        int mSpanCount;
+
+        int mGapStrategy;
+
+        int mAnchorPosition;
+
+        int[] mSpanOffsets;
+
+        int mSpanLookupSize;
+
+        int[] mSpanLookup;
+
+        boolean mReverseLayout;
+
+        boolean mAnchorLayoutFromEnd;
+
+        boolean mHasSpanOffsets;
+
+        public SavedState() {
+        }
+
+        SavedState(Parcel in) {
+            mOrientation = in.readInt();
+            mSpanCount = in.readInt();
+            mGapStrategy = in.readInt();
+            mAnchorPosition = in.readInt();
+            mHasSpanOffsets = in.readInt() == 1;
+            if (mHasSpanOffsets) {
+                mSpanOffsets = new int[mSpanCount];
+                in.readIntArray(mSpanOffsets);
+            }
+
+            mSpanLookupSize = in.readInt();
+            if (mSpanLookupSize > 0) {
+                mSpanLookup = new int[mSpanLookupSize];
+                in.readIntArray(mSpanLookup);
+            }
+            mReverseLayout = in.readInt() == 1;
+            mAnchorLayoutFromEnd = in.readInt() == 1;
+        }
+
+        public SavedState(SavedState other) {
+            mOrientation = other.mOrientation;
+            mSpanCount = other.mSpanCount;
+            mGapStrategy = other.mGapStrategy;
+            mAnchorPosition = other.mAnchorPosition;
+            mHasSpanOffsets = other.mHasSpanOffsets;
+            mSpanOffsets = other.mSpanOffsets;
+            mSpanLookupSize = other.mSpanLookupSize;
+            mSpanLookup = other.mSpanLookup;
+            mReverseLayout = other.mReverseLayout;
+            mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
+        }
+
+        void invalidateSpanInfo() {
+            mSpanOffsets = null;
+            mHasSpanOffsets = false;
+            mSpanCount = -1;
+            mSpanLookupSize = 0;
+            mSpanLookup = null;
+        }
+
+        void invalidateAnchorPositionInfo() {
+            mSpanOffsets = null;
+            mHasSpanOffsets = false;
+            mAnchorPosition = RecyclerView.NO_POSITION;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mOrientation);
+            dest.writeInt(mSpanCount);
+            dest.writeInt(mGapStrategy);
+            dest.writeInt(mAnchorPosition);
+            dest.writeInt(mHasSpanOffsets ? 1 : 0);
+            if (mHasSpanOffsets) {
+                dest.writeIntArray(mSpanOffsets);
+            }
+            dest.writeInt(mSpanLookupSize);
+            if (mSpanLookupSize > 0) {
+                dest.writeIntArray(mSpanLookup);
+            }
+            dest.writeInt(mReverseLayout ? 1 : 0);
+            dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
+        }
+
+        @Override
+        public String toString() {
+            return "SavedState{" +
+                    "mOrientation=" + mOrientation +
+                    ", mSpanCount=" + mSpanCount +
+                    ", mGapStrategy=" + mGapStrategy +
+                    ", mAnchorPosition=" + mAnchorPosition +
+                    ", mSpanOffsets=" + Arrays.toString(mSpanOffsets) +
+                    ", mSpanLookupSize=" + mSpanLookupSize +
+                    ", mSpanLookup=" + Arrays.toString(mSpanLookup) +
+                    ", mReverseLayout=" + mReverseLayout +
+                    ", mAnchorLayoutFromEnd=" + mAnchorLayoutFromEnd +
+                    ", mHasSpanOffsets=" + mHasSpanOffsets +
+                    '}';
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index a92782c..432da1e 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -264,21 +264,21 @@
         final static AtomicInteger idCounter = new AtomicInteger(0);
         final public int mId = idCounter.incrementAndGet();
 
-        int originalIndex;
+        int mAdapterIndex;
 
-        final String text;
+        final String mText;
 
-        Item(int originalIndex, String text) {
-            this.originalIndex = originalIndex;
-            this.text = text;
+        Item(int adapterIndex, String text) {
+            mAdapterIndex = adapterIndex;
+            mText = text;
         }
 
         @Override
         public String toString() {
             return "Item{" +
                     "mId=" + mId +
-                    ", originalIndex=" + originalIndex +
-                    ", text='" + text + '\'' +
+                    ", originalIndex=" + mAdapterIndex +
+                    ", text='" + mText + '\'' +
                     '}';
         }
     }
@@ -303,7 +303,7 @@
         @Override
         public void onBindViewHolder(TestViewHolder holder, int position) {
             final Item item = mItems.get(position);
-            ((TextView) (holder.itemView)).setText(item.text);
+            ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")");
             holder.mBindedItem = item;
         }
 
@@ -327,6 +327,12 @@
             new AddRemoveRunnable(startCountTuples).runOnMainThread();
         }
 
+        public void offsetOriginalIndices(int start, int offset) {
+            for (int i = start; i < mItems.size(); i++) {
+                mItems.get(i).mAdapterIndex += offset;
+            }
+        }
+
         public void addAndNotify(final int start, final int count) throws Throwable {
             addAndNotify(new int[]{start, count});
         }
@@ -396,21 +402,21 @@
             }
 
             private void add(int[] tuple) {
+                // offset others
+                offsetOriginalIndices(tuple[0], tuple[1]);
                 for (int i = 0; i < tuple[1]; i++) {
                     mItems.add(tuple[0], new Item(i, "new item " + i));
                 }
-                // offset others
-                for (int i = tuple[0] + tuple[1]; i < mItems.size(); i++) {
-                    mItems.get(i).originalIndex += tuple[1];
-                }
                 notifyItemRangeInserted(tuple[0], tuple[1]);
             }
 
             private void delete(int[] tuple) {
-                for (int i = 0; i < -tuple[1]; i++) {
+                final int count = -tuple[1];
+                offsetOriginalIndices(tuple[0] + count, tuple[1]);
+                for (int i = 0; i < count; i++) {
                     mItems.remove(tuple[0]);
                 }
-                notifyItemRangeRemoved(tuple[0], -tuple[1]);
+                notifyItemRangeRemoved(tuple[0], count);
             }
         }
     }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
index 76bf541..adf73bf 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -431,11 +431,11 @@
             Log.d(TAG, "checking rectangle equality.");
             Log.d(TAG, "before:");
             for (Map.Entry<Item, Rect> entry : before.entrySet()) {
-                Log.d(TAG, entry.getKey().originalIndex + ":" + entry.getValue());
+                Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
             }
             Log.d(TAG, "after:");
             for (Map.Entry<Item, Rect> entry : after.entrySet()) {
-                Log.d(TAG, entry.getKey().originalIndex + ":" + entry.getValue());
+                Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
             }
         }
         assertEquals(message + ":\nitem counts should be equal", before.size()
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
index 96c9202..124658b 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -34,7 +34,7 @@
 
 public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest {
 
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
 
     private static final String TAG = "RecyclerViewAnimationsTest";
 
@@ -281,8 +281,7 @@
         setupBasic(10, 1, 7);
         mLayoutManager.expectLayouts(2);
         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
-        mTestAdapter.addAndNotify(0, 1);// add a new item 0 // invisible
-        mTestAdapter.addAndNotify(7, 1);// add a new item after 5th (old 5, new 6)
+        mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{7, 1});// add a new item 0 // invisible
         mLayoutManager.waitForLayout(2);
     }
 
@@ -290,8 +289,7 @@
         setupBasic(10, 1, 7);
         mLayoutManager.expectLayouts(1);
         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
-        mTestAdapter.addAndNotify(0, 1);// add a new item 0
-        mTestAdapter.addAndNotify(8, 1);// add a new item after 6th (old 6, new 7)
+        mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{8, 1});// add a new item 0
         mLayoutManager.waitForLayout(2);
     }
 
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
new file mode 100644
index 0000000..807bea8
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
@@ -0,0 +1,1116 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.support.v7.widget;
+
+
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static android.support.v7.widget.OrientationHelper.*;
+import static android.support.v7.widget.LayoutState.*;
+import static android.support.v7.widget.StaggeredGridLayoutManager.*;
+
+public class StaggeredGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
+
+    private static final boolean DEBUG = true;
+
+    private static final String TAG = "StaggeredGridLayoutManagerTest";
+
+    WrappedLayoutManager mLayoutManager;
+
+    GridTestAdapter mAdapter;
+
+    RecyclerView mRecyclerView;
+
+    final List<Config> mBaseVariations = new ArrayList<Config>();
+
+    @Override
+    protected void setUp() throws Exception {
+        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));
+                    }
+                }
+            }
+        }
+    }
+
+    void setupByConfig(Config config) throws Throwable {
+        mAdapter = new GridTestAdapter(config.mItemCount, config.mOrientation);
+        mRecyclerView = new RecyclerView(getActivity());
+        mRecyclerView.setAdapter(mAdapter);
+        mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
+                config.mOrientation);
+        mLayoutManager.setGapStrategy(config.mGapStrategy);
+        mLayoutManager.setReverseLayout(config.mReverseLayout);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+    }
+
+    LayoutParams getLp(View view) {
+        return (LayoutParams) view.getLayoutParams();
+    }
+
+    public void testInnerGapHandling() throws Throwable {
+        innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
+        innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
+    }
+
+    public void innerGapHandlingTest(int strategy) throws Throwable {
+        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});
+
+                // should be able to detect the gap
+                View gapView = mLayoutManager.hasGapsToFix(0, mLayoutManager.getChildCount());
+                assertSame("gap should be detected", mLayoutManager.findViewByPosition(101),
+                        gapView);
+                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 void testFullSizeSpans() throws Throwable {
+        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});
+    }
+
+    void assertSpans(String msg, int[]... childSpanTuples) {
+        for (int i = 0; i < childSpanTuples.length; i++) {
+            assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
+        }
+    }
+
+    void assertSpan(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);
+    }
+
+    public void testSpanReassignmentsOnItemChange() throws Throwable {
+        Config config = new Config().spanCount(5);
+        setupByConfig(config);
+        waitFirstLayout();
+        smoothScrollToPosition(mAdapter.getItemCount() / 2);
+        final int changePosition = mAdapter.getItemCount() / 4;
+        int[] prevAssignments = mLayoutManager.mLazySpanLookup.mData.clone();
+        mLayoutManager.expectLayouts(1);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mAdapter.notifyItemChanged(changePosition);
+            }
+        });
+        mLayoutManager.waitForLayout(2);
+        // item change should not affect span assignments
+        assertSpanAssignmentEquality("item change should not affect span assignments ",
+                prevAssignments, mLayoutManager.mLazySpanLookup.mData, 0, prevAssignments.length);
+
+        // 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);
+        // TODO move these bounds to edge case once animation changes are in.
+        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());
+    }
+
+    void assertSpanAssignmentEquality(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]);
+        }
+    }
+
+    void assertSpanAssignmentEquality(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]);
+        }
+    }
+
+    public void testViewSnapping() throws Throwable {
+        for (Config config : mBaseVariations) {
+            viewSnapTest(config.itemCount(config.mSpanCount + 1));
+            removeRecyclerView();
+        }
+    }
+
+    public void viewSnapTest(Config config) throws Throwable {
+        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;
+        }
+
+    }
+
+    public void testSpanCountChangeOnRestoreSavedState() throws Throwable {
+        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);
+        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 cound, 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 void testSavedState() throws Throwable {
+        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";
+                    }
+                },
+                new PostLayoutRunnable() {
+                    @Override
+                    public void run() throws Throwable {
+                        mLayoutManager.expectLayouts(1);
+                        scrollToPositionWithOffset(mAdapter.getItemCount() * 1 / 3,
+                                50);
+                        mLayoutManager.waitForLayout(2);
+                    }
+
+                    @Override
+                    public String describe() {
+                        return "scroll to position 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};
+        for (Config config : mBaseVariations) {
+            for (PostLayoutRunnable runnable : postLayoutOptions) {
+                for (boolean waitForLayout : waitForLayoutOptions) {
+                    savedStateTest(config, waitForLayout, runnable);
+                    removeRecyclerView();
+                }
+            }
+        }
+    }
+
+    public void savedStateTest(Config config, boolean waitForLayout,
+            PostLayoutRunnable postLayoutOperations)
+            throws Throwable {
+        if (DEBUG) {
+            Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config "
+                    + config + " post layout action " + postLayoutOperations.describe());
+        }
+        setupByConfig(config);
+        if (waitForLayout) {
+            waitFirstLayout();
+            postLayoutOperations.run();
+        }
+        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);
+        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());
+        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 void testScrollToPositionWithOffset() throws Throwable {
+        for (Config config : mBaseVariations) {
+            scrollToPositionWithOffsetTest(config);
+            removeRecyclerView();
+        }
+    }
+
+    public void scrollToPositionWithOffsetTest(Config config) throws Throwable {
+        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.getChildPosition(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 void testScrollToPosition() throws Throwable {
+        for (Config config : mBaseVariations) {
+            scrollToPositionTest(config);
+            removeRecyclerView();
+        }
+    }
+
+    public Rect getDecoratedRecyclerViewBounds() {
+        return new Rect(
+                mRecyclerView.getPaddingLeft(),
+                mRecyclerView.getPaddingTop(),
+                mRecyclerView.getPaddingLeft() + mRecyclerView.getWidth(),
+                mRecyclerView.getPaddingTop() + mRecyclerView.getHeight()
+        );
+    }
+
+    private TargetTuple findInvisibleTarget(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.getChildPosition(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 void scrollToPositionTest(Config config) throws Throwable {
+        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.getChildPosition(view);
+                LayoutParams layoutParams
+                        = (LayoutParams) (view.getLayoutParams());
+                TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder;
+                assertEquals("recycler view mPosition should match adapter mPosition", position,
+                        vh.mBindedItem.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.getChildPosition(view);
+                if (DEBUG) {
+                    Log.d(TAG,
+                            "child(" + position + ") not fully visible " + bounds + " not inside "
+                                    + layoutBounds
+                                    + mRecyclerView.getChildPosition(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 void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mLayoutManager.scrollToPositionWithOffset(position, offset);
+            }
+        });
+    }
+
+    public void testLayoutOrder() throws Throwable {
+        for (Config config : mBaseVariations) {
+            layoutOrderTest(config);
+            removeRecyclerView();
+        }
+    }
+
+    public void layoutOrderTest(Config config) throws Throwable {
+        setupByConfig(config);
+        assertViewPositions(config);
+    }
+
+    void assertViewPositions(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 void testScrollBy() throws Throwable {
+        for (Config config : mBaseVariations) {
+            scrollByTest(config);
+            removeRecyclerView();
+        }
+    }
+
+    void waitFirstLayout() throws Throwable {
+        mLayoutManager.expectLayouts(1);
+        setRecyclerView(mRecyclerView);
+        mLayoutManager.waitForLayout(2);
+    }
+
+    public void scrollByTest(Config config) throws Throwable {
+        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 void testConsistentRelayout() throws Throwable {
+        for (Config config : mBaseVariations) {
+            for (boolean firstChildMultiSpan : new boolean[]{false, true}) {
+                consistentRelayoutTest(config, firstChildMultiSpan);
+            }
+            removeRecyclerView();
+        }
+    }
+
+    public void consistentRelayoutTest(Config config, boolean firstChildMultiSpan)
+            throws Throwable {
+        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);
+    }
+
+    /**
+     * enqueues an empty runnable to main thread so that we can be assured it did run
+     *
+     * @param count Number of times to run
+     */
+    private void waitForMainThread(int count) throws Throwable {
+        final AtomicInteger i = new AtomicInteger(count);
+        while (i.get() > 0) {
+            runTestOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    i.decrementAndGet();
+                }
+            });
+        }
+    }
+
+    public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
+            Map<Item, Rect> after) {
+        Throwable throwable = null;
+        try {
+            assertRectSetsEqual("NOT " + message, before, after);
+        } catch (Throwable t) {
+            throwable = t;
+        }
+        assertNotNull(message + " two layout should be different", throwable);
+    }
+
+    public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
+        if (DEBUG) {
+            Log.d(TAG, "checking rectangle equality.");
+            Log.d(TAG, "before:");
+            for (Map.Entry<Item, Rect> entry : before.entrySet()) {
+                Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
+            }
+            Log.d(TAG, "after:");
+            for (Map.Entry<Item, Rect> entry : after.entrySet()) {
+                Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
+            }
+        }
+        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);
+        }
+    }
+
+    // test layout params assignment
+
+    class WrappedLayoutManager extends StaggeredGridLayoutManager {
+
+        CountDownLatch layoutLatch;
+
+        public void expectLayouts(int count) {
+            layoutLatch = new CountDownLatch(count);
+        }
+
+        public void waitForLayout(long timeout) throws InterruptedException {
+            waitForLayout(timeout * (DEBUG ? 1000 : 1), TimeUnit.SECONDS);
+        }
+
+        public void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
+            layoutLatch.await(timeout, timeUnit);
+            assertEquals("all expected layouts should be executed at the expected time",
+                    0, layoutLatch.getCount());
+        }
+
+        @Override
+        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+            super.onLayoutChildren(recycler, state);
+            layoutLatch.countDown();
+        }
+
+        public WrappedLayoutManager(int spanCount, int orientation) {
+            super(spanCount, orientation);
+        }
+
+        ArrayList<ArrayList<View>> collectChildrenBySpan() {
+            ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>();
+            for (int i = 0; i < getSpanCount(); i++) {
+                viewsBySpan.add(new ArrayList<View>());
+            }
+            for (int i = 0; i < getChildCount(); i++) {
+                View view = getChildAt(i);
+                LayoutParams lp
+                        = (LayoutParams) view
+                        .getLayoutParams();
+                viewsBySpan.get(lp.mSpan.mIndex).add(view);
+            }
+            return viewsBySpan;
+        }
+
+        Rect getViewBounds(View view) {
+            if (getOrientation() == HORIZONTAL) {
+                return new Rect(
+                        mPrimaryOrientation.getDecoratedStart(view),
+                        mSecondaryOrientation.getDecoratedStart(view),
+                        mPrimaryOrientation.getDecoratedEnd(view),
+                        mSecondaryOrientation.getDecoratedEnd(view));
+            } else {
+                return new Rect(
+                        mSecondaryOrientation.getDecoratedStart(view),
+                        mPrimaryOrientation.getDecoratedStart(view),
+                        mSecondaryOrientation.getDecoratedEnd(view),
+                        mPrimaryOrientation.getDecoratedEnd(view));
+            }
+
+        }
+
+        Map<Item, Rect> collectChildCoordinates() throws Throwable {
+            final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
+            runTestOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    final int childCount = getChildCount();
+                    for (int i = 0; i < childCount; i++) {
+                        View child = getChildAt(i);
+                        LayoutParams lp = (LayoutParams) child
+                                .getLayoutParams();
+                        TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
+                        items.put(vh.mBindedItem, getViewBounds(child));
+                    }
+                }
+            });
+            return items;
+        }
+    }
+
+    class GridTestAdapter extends TestAdapter {
+
+        int mOrientation;
+
+        // original ids of items that should be full span
+        HashSet<Integer> mFullSpanItems = new HashSet<Integer>();
+
+        private boolean mViewsHaveEqualSize = false; // size in the scrollable direction
+
+        GridTestAdapter(int count, int orientation) {
+            super(count);
+            mOrientation = orientation;
+        }
+
+        @Override
+        public void offsetOriginalIndices(int start, int offset) {
+            if (mFullSpanItems.size() > 0) {
+                HashSet<Integer> old = mFullSpanItems;
+                mFullSpanItems = new HashSet<Integer>();
+                for (Integer i : old) {
+                    if (i < start) {
+                        mFullSpanItems.add(i);
+                    } else if (offset > 0 || (start + Math.abs(offset)) <= i) {
+                        mFullSpanItems.add(i + offset);
+                    } else if (DEBUG) {
+                        Log.d(TAG, "removed full span item " + i);
+                    }
+                }
+            }
+            super.offsetOriginalIndices(start, offset);
+        }
+
+        @Override
+        public void onBindViewHolder(TestViewHolder holder,
+                int position) {
+            super.onBindViewHolder(holder, position);
+            Item item = mItems.get(position);
+            final int minSize = mViewsHaveEqualSize ? 200 : 200 + 20 * (position % 10);
+            if (mOrientation == OrientationHelper.HORIZONTAL) {
+                holder.itemView.setMinimumWidth(minSize);
+            } else {
+                holder.itemView.setMinimumHeight(minSize);
+            }
+            RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
+                    .getLayoutParams();
+            if (lp instanceof LayoutParams) {
+                ((LayoutParams) lp).setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
+            } else {
+                LayoutParams slp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT);
+                holder.itemView.setLayoutParams(slp);
+                slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
+                lp = slp;
+            }
+            lp.topMargin = 3;
+            lp.leftMargin = 5;
+            lp.rightMargin = 7;
+            lp.bottomMargin = 9;
+        }
+    }
+
+    static class Config {
+
+        private static final int DEFAULT_ITEM_COUNT = 300;
+
+        int mOrientation = OrientationHelper.VERTICAL;
+
+        boolean mReverseLayout = false;
+
+        int mSpanCount = 3;
+
+        int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
+
+        int mItemCount = DEFAULT_ITEM_COUNT;
+
+        OrientationHelper mOrientationHelper;
+
+        Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) {
+            mOrientation = orientation;
+            mReverseLayout = reverseLayout;
+            mSpanCount = spanCount;
+            mGapStrategy = gapStrategy;
+        }
+
+        public Config() {
+
+        }
+
+        Config orientation(int orientation) {
+            mOrientation = orientation;
+            return this;
+        }
+
+        Config reverseLayout(boolean reverseLayout) {
+            mReverseLayout = reverseLayout;
+            return this;
+        }
+
+        Config spanCount(int spanCount) {
+            mSpanCount = spanCount;
+            return this;
+        }
+
+        Config gapStrategy(int gapStrategy) {
+            mGapStrategy = gapStrategy;
+            return this;
+        }
+
+        public Config itemCount(int itemCount) {
+            mItemCount = itemCount;
+            return this;
+        }
+
+        @Override
+        public String toString() {
+            return "[CONFIG:" +
+                    " span:" + mSpanCount + "," +
+                    " orientation:" + (mOrientation == HORIZONTAL ? "horz," : "vert,") +
+                    " reverse:" + (mReverseLayout ? "T" : "F") +
+                    " gap strategy: " + gapStrategyName(mGapStrategy);
+        }
+
+        private static String gapStrategyName(int gapStrategy) {
+            switch (gapStrategy) {
+                case GAP_HANDLING_NONE:
+                    return "none";
+                case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
+                    return "move spans";
+                case GAP_HANDLING_LAZY:
+                    return "lazy";
+            }
+            return "gap strategy: unknown";
+        }
+    }
+
+    private static class TargetTuple {
+
+        final int mPosition;
+
+        final int mLayoutDirection;
+
+        TargetTuple(int position, int layoutDirection) {
+            this.mPosition = position;
+            this.mLayoutDirection = layoutDirection;
+        }
+    }
+
+    private interface PostLayoutRunnable {
+
+        void run() throws Throwable;
+
+        String describe();
+    }
+
+}
\ No newline at end of file