StaggeredGridLayoutManager.java revision 9bea36cf2e318e9b729ddc62d855cd0f93bc3866
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.widget;
18
19import android.content.Context;
20import android.graphics.PointF;
21import android.graphics.Rect;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.support.v4.view.ViewCompat;
25import android.util.AttributeSet;
26import android.util.Log;
27import android.view.View;
28import android.view.ViewGroup;
29
30
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.BitSet;
34
35
36import static android.support.v7.widget.LayoutState.LAYOUT_START;
37import static android.support.v7.widget.LayoutState.LAYOUT_END;
38import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD;
39import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL;
40/**
41 * A LayoutManager that lays out children in a staggered grid formation.
42 * It supports horizontal & vertical layout as well as an ability to layout children in reverse.
43 * <p>
44 * Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps,
45 * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can
46 * control this behavior via {@link #setGapStrategy(int)}.
47 */
48public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager {
49
50    public static final String TAG = "StaggeredGridLayoutManager";
51
52    private static final boolean DEBUG = false;
53
54    /**
55     * Does not do anything to hide gaps
56     */
57    public static final int GAP_HANDLING_NONE = 0;
58
59    public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
60
61    public static final int VERTICAL = OrientationHelper.VERTICAL;
62
63    /**
64     * Scroll the shorter span slower to avoid gaps in the UI.
65     * <p>
66     * For example, if LayoutManager ends up with the following layout:
67     * <code>
68     * BXC
69     * DEF
70     * </code>
71     * Where B has two spans height, if user scrolls down it will keep the positions of 2nd and 3rd
72     * columns,
73     * which will result in:
74     * <code>
75     * BXC
76     * BEF
77     * </code>
78     * instead of
79     * <code>
80     * B
81     * BEF
82     * </code>
83     */
84    public static final int GAP_HANDLING_LAZY = 1;
85
86    /**
87     * On scroll, LayoutManager checks for a view that is assigned to wrong span.
88     * When such a situation is detected, LayoutManager will wait until scroll is complete and then
89     * move children to their correct spans.
90     * <p>
91     * For example, if LayoutManager ends up with the following layout due to adapter changes:
92     * <code>
93     * AAA
94     * _BC
95     * DDD
96     * </code>
97     * It will animate to the following state:
98     * <code>
99     * AAA
100     * BC_
101     * DDD
102     * </code>
103     */
104    public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2;
105
106    private static final int INVALID_OFFSET = Integer.MIN_VALUE;
107
108    /**
109     * Number of spans
110     */
111    private int mSpanCount = -1;
112
113    private Span[] mSpans;
114
115    /**
116     * Primary orientation is the layout's orientation, secondary orientation is the orientation
117     * for spans. Having both makes code much cleaner for calculations.
118     */
119    OrientationHelper mPrimaryOrientation;
120    OrientationHelper mSecondaryOrientation;
121
122    private int mOrientation;
123
124    /**
125     * The width or height per span, depending on the orientation.
126     */
127    private int mSizePerSpan;
128
129    private LayoutState mLayoutState;
130
131    private boolean mReverseLayout = false;
132
133    /**
134     * Aggregated reverse layout value that takes RTL into account.
135     */
136    private boolean mShouldReverseLayout = false;
137
138    /**
139     * Temporary variable used during fill method to check which spans needs to be filled.
140     */
141    private BitSet mRemainingSpans;
142
143    /**
144     * When LayoutManager needs to scroll to a position, it sets this variable and requests a
145     * layout which will check this variable and re-layout accordingly.
146     */
147    private int mPendingScrollPosition = RecyclerView.NO_POSITION;
148
149    /**
150     * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is
151     * called.
152     */
153    private int mPendingScrollPositionOffset = INVALID_OFFSET;
154
155    /**
156     * Keeps the mapping between the adapter positions and spans. This is necessary to provide
157     * a consistent experience when user scrolls the list.
158     */
159    LazySpanLookup mLazySpanLookup = new LazySpanLookup();
160
161    /**
162     * how we handle gaps in UI.
163     */
164    private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
165
166    /**
167     * Saved state needs this information to properly layout on restore.
168     */
169    private boolean mLastLayoutFromEnd;
170
171    /**
172     * SavedState is not handled until a layout happens. This is where we keep it until next
173     * layout.
174     */
175    private SavedState mPendingSavedState;
176
177    /**
178     * If LayoutManager detects an unwanted gap in the layout, it sets this flag which will trigger
179     * a runnable after scrolling ends and will re-check. If invalid view state is still present,
180     * it will request a layout to fix it.
181     */
182    private boolean mHasGaps;
183
184    /**
185     * Creates a StaggeredGridLayoutManager with given parameters.
186     *
187     * @param spanCount   If orientation is vertical, spanCount is number of columns. If
188     *                    orientation is horizontal, spanCount is number of rows.
189     * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
190     */
191    public StaggeredGridLayoutManager(int spanCount, int orientation) {
192        mOrientation = orientation;
193        setSpanCount(spanCount);
194    }
195
196    @Override
197    public void onScrollStateChanged(int state) {
198        if (state == RecyclerView.SCROLL_STATE_IDLE && mHasGaps) {
199            // re-check for gaps
200            View gapView = hasGapsToFix(0, getChildCount());
201            if (gapView == null) {
202                mHasGaps = false; // yay, gap disappeared :)
203                // We should invalidate positions after the last visible child. No reason to
204                // re-layout.
205                final int lastVisiblePosition = mShouldReverseLayout ? getFirstChildPosition()
206                        : getLastChildPosition();
207                mLazySpanLookup.invalidateAfter(lastVisiblePosition + 1);
208            } else {
209                mLazySpanLookup.invalidateAfter(getPosition(gapView));
210                requestLayout(); // Trigger a re-layout which will fix the layout assignments.
211            }
212        }
213    }
214
215    /**
216     * Sets the number of spans for the layout. This will invalidate all of the span assignments
217     * for Views.
218     * <p>
219     * Calling this method will automatically result in a new layout request unless the spanCount
220     * parameter is equal to current span count.
221     *
222     * @param spanCount Number of spans to layout
223     */
224    public void setSpanCount(int spanCount) {
225        if (mPendingSavedState != null && mPendingSavedState.mSpanCount != spanCount) {
226            // invalidate span info in saved state
227            mPendingSavedState.invalidateSpanInfo();
228            mPendingSavedState.mSpanCount = spanCount;
229        }
230        if (spanCount != mSpanCount) {
231            invalidateSpanAssignments();
232            mSpanCount = spanCount;
233            mRemainingSpans = new BitSet(mSpanCount);
234            mSpans = new Span[mSpanCount];
235            for (int i = 0; i < mSpanCount; i++) {
236                mSpans[i] = new Span(i);
237            }
238            requestLayout();
239        }
240    }
241
242    /**
243     * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep
244     * scroll position.
245     *
246     * @param orientation {@link OrientationHelper#HORIZONTAL} or {@link OrientationHelper#VERTICAL}
247     */
248    public void setOrientation(int orientation) {
249        if (orientation != HORIZONTAL && orientation != VERTICAL) {
250            throw new IllegalArgumentException("invalid orientation.");
251        }
252        if (mPendingSavedState != null && mPendingSavedState.mOrientation != orientation) {
253            // override pending state
254            mPendingSavedState.mOrientation = orientation;
255        }
256        if (orientation == mOrientation) {
257            return;
258        }
259        mOrientation = orientation;
260        if (mPrimaryOrientation != null && mSecondaryOrientation != null) {
261            // swap
262            OrientationHelper tmp = mPrimaryOrientation;
263            mPrimaryOrientation = mSecondaryOrientation;
264            mSecondaryOrientation = tmp;
265        }
266        requestLayout();
267    }
268
269    /**
270     * Sets whether LayoutManager should start laying out items from the end of the UI. The order
271     * items are traversed is not affected by this call.
272     * <p>
273     * This behaves similar to the layout change for RTL views. When set to true, first item is
274     * laid out at the end of the ViewGroup, second item is laid out before it etc.
275     * <p>
276     * For horizontal layouts, it depends on the layout direction.
277     * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if
278     * {@link RecyclerView}} is RTL, it will layout from LTR.
279     *
280     * @param reverseLayout Whether layout should be in reverse or not
281     */
282    public void setReverseLayout(boolean reverseLayout) {
283        if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) {
284            mPendingSavedState.mReverseLayout = reverseLayout;
285        }
286        mReverseLayout = reverseLayout;
287        requestLayout();
288    }
289
290    /**
291     * Returns the current gap handling strategy for StaggeredGridLayoutManager.
292     * <p>
293     * Staggered grid may have gaps in the layout as items may have different sizes. To avoid gaps,
294     * StaggeredGridLayoutManager provides 3 options. Check {@link #GAP_HANDLING_NONE},
295     * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}, {@link #GAP_HANDLING_LAZY} for details.
296     * <p>
297     * By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}.
298     *
299     * @return Current gap handling strategy.
300     * @see #setGapStrategy(int)
301     * @see #GAP_HANDLING_NONE
302     * @see #GAP_HANDLING_LAZY
303     * @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
304     */
305    public int getGapStrategy() {
306        return mGapStrategy;
307    }
308
309    /**
310     * Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter
311     * is different than the current strategy, calling this method will trigger a layout request.
312     *
313     * @param gapStrategy The new gap handling strategy. Should be {@link #GAP_HANDLING_LAZY}
314     *                    , {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or
315     *                    {@link #GAP_HANDLING_NONE}
316     * @see #getGapStrategy()
317     */
318    public void setGapStrategy(int gapStrategy) {
319        if (mPendingSavedState != null && mPendingSavedState.mGapStrategy != gapStrategy) {
320            mPendingSavedState.mGapStrategy = gapStrategy;
321        }
322        if (gapStrategy == mGapStrategy) {
323            return;
324        }
325        if (gapStrategy != GAP_HANDLING_LAZY && gapStrategy != GAP_HANDLING_NONE &&
326                gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
327            throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE "
328                    + ", GAP_HANDLING_LAZY or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS");
329        }
330        mGapStrategy = gapStrategy;
331        requestLayout();
332    }
333
334    /**
335     * Returns the number of spans laid out by StaggeredGridLayoutManager.
336     *
337     * @return Number of spans in the layout
338     */
339    public int getSpanCount() {
340        return mSpanCount;
341    }
342
343    /**
344     * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items.
345     * <p>
346     * If you need to cancel current assignments, you can call this method which will clear all
347     * assignments and request a new layout.
348     */
349    public void invalidateSpanAssignments() {
350        mLazySpanLookup.clear();
351        requestLayout();
352    }
353
354    private void ensureOrientationHelper() {
355        if (mPrimaryOrientation == null) {
356            mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation);
357            mSecondaryOrientation = OrientationHelper
358                    .createOrientationHelper(this, 1 - mOrientation);
359            mLayoutState = new LayoutState();
360        }
361    }
362
363    /**
364     * Calculates the views' layout order. (e.g. from end to start or start to end)
365     * RTL layout support is applied automatically. So if layout is RTL and
366     * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
367     */
368    private void resolveShouldLayoutReverse() {
369        // A == B is the same result, but we rather keep it readable
370        if (mOrientation == VERTICAL || !isLayoutRTL()) {
371            mShouldReverseLayout = mReverseLayout;
372        } else {
373            mShouldReverseLayout = !mReverseLayout;
374        }
375    }
376
377    private boolean isLayoutRTL() {
378        return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
379    }
380
381    /**
382     * Returns whether views are laid out in reverse order or not.
383     * <p>
384     * Not that this value is not affected by RecyclerView's layout direction.
385     *
386     * @return True if layout is reversed, false otherwise
387     * @see #setReverseLayout(boolean)
388     */
389    public boolean getReverseLayout() {
390        return mReverseLayout;
391    }
392
393    @Override
394    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
395        ensureOrientationHelper();
396        // Update adapter size.
397        mLazySpanLookup.mAdapterSize = state.getItemCount();
398        int anchorItemPosition;
399        int anchorOffset;
400        // This value may change if we are jumping to a position.
401        boolean layoutFromEnd;
402
403        // If set to true, spans will clear their offsets and they'll be laid out from start
404        // depending on the layout direction. Invalidating span offsets is necessary to be able
405        // to jump to a position.
406        boolean invalidateSpanOffsets = false;
407
408        if (mPendingSavedState != null) {
409            if (DEBUG) {
410                Log.d(TAG, "found saved state: " + mPendingSavedState);
411            }
412            setOrientation(mPendingSavedState.mOrientation);
413            setSpanCount(mPendingSavedState.mSpanCount);
414            setGapStrategy(mPendingSavedState.mGapStrategy);
415            setReverseLayout(mPendingSavedState.mReverseLayout);
416            resolveShouldLayoutReverse();
417
418            if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) {
419                mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
420                layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
421            } else {
422                layoutFromEnd = mShouldReverseLayout;
423            }
424            if (mPendingSavedState.mHasSpanOffsets) {
425                for (int i = 0; i < mSpanCount; i++) {
426                    mSpans[i].clear();
427                    mSpans[i].setLine(mPendingSavedState.mSpanOffsets[i]);
428                }
429            }
430            if (mPendingSavedState.mSpanLookupSize > 1) {
431                mLazySpanLookup.mData = mPendingSavedState.mSpanLookup;
432            }
433
434        } else {
435            resolveShouldLayoutReverse();
436            layoutFromEnd = mShouldReverseLayout; // get updated value.
437        }
438
439        // Validate scroll position if exists.
440        if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
441            // Validate it.
442            if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
443                mPendingScrollPosition = RecyclerView.NO_POSITION;
444                mPendingScrollPositionOffset = INVALID_OFFSET;
445            }
446        }
447
448        if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
449            if (mPendingSavedState == null
450                    || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION
451                    || !mPendingSavedState.mHasSpanOffsets) {
452                // If item is visible, make it fully visible.
453                final View child = findViewByPosition(mPendingScrollPosition);
454                if (child != null) {
455                    if (mPendingScrollPositionOffset != INVALID_OFFSET) {
456                        // Use regular anchor position.
457                        anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
458                                : getFirstChildPosition();
459                        if (layoutFromEnd) {
460                            final int target = mPrimaryOrientation.getEndAfterPadding() -
461                                    mPendingScrollPositionOffset;
462                            anchorOffset = target - mPrimaryOrientation.getDecoratedEnd(child);
463                        } else {
464                            final int target = mPrimaryOrientation.getStartAfterPadding() +
465                                    mPendingScrollPositionOffset;
466                            anchorOffset = target - mPrimaryOrientation.getDecoratedStart(child);
467                        }
468                    } else {
469                        final int startGap = mPrimaryOrientation.getDecoratedStart(child)
470                                - mPrimaryOrientation.getStartAfterPadding();
471                        final int endGap = mPrimaryOrientation.getEndAfterPadding() -
472                                mPrimaryOrientation.getDecoratedEnd(child);
473                        final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child);
474                        // Use regular anchor item, just offset the layout.
475                        anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
476                                : getFirstChildPosition();
477                        if (childSize > mPrimaryOrientation.getTotalSpace()) {
478                            // Item does not fit. Fix depending on layout direction.
479                            anchorOffset = layoutFromEnd ? mPrimaryOrientation.getEndAfterPadding()
480                                    : mPrimaryOrientation.getStartAfterPadding();
481                        } else if (startGap < 0) {
482                            anchorOffset = -startGap;
483                        } else if (endGap < 0) {
484                            anchorOffset = endGap;
485                        } else {
486                            // Nothing to do, just layout normal.
487                            anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
488                                    : getFirstChildPosition();
489                            anchorOffset = INVALID_OFFSET;
490                        }
491                    }
492                } else {
493                    // Child is not visible. Set anchor coordinate depending on in which direction
494                    // child will be visible.
495                    anchorItemPosition = mPendingScrollPosition;
496                    if (mPendingScrollPositionOffset == INVALID_OFFSET) {
497                        final int position = calculateScrollDirectionForPosition(
498                                anchorItemPosition);
499                        if (position == LAYOUT_START) {
500                            anchorOffset = mPrimaryOrientation.getStartAfterPadding();
501                            layoutFromEnd = false;
502                        } else {
503                            anchorOffset = mPrimaryOrientation.getEndAfterPadding();
504                            layoutFromEnd = true;
505                        }
506                    } else {
507                        if (layoutFromEnd) {
508                            anchorOffset = mPrimaryOrientation.getEndAfterPadding()
509                                    - mPendingScrollPositionOffset;
510                        } else {
511                            anchorOffset = mPrimaryOrientation.getStartAfterPadding()
512                                    + mPendingScrollPositionOffset;
513                        }
514                    }
515                    invalidateSpanOffsets = true;
516                }
517            } else {
518                anchorOffset = INVALID_OFFSET;
519                anchorItemPosition = mPendingScrollPosition;
520            }
521
522        } else {
523            // We don't recycle views out of adapter order. This way, we can rely on the first or
524            // last child as the anchor position.
525            anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
526                    : getFirstChildPosition();
527            anchorOffset = INVALID_OFFSET;
528        }
529        if (getChildCount() > 0 && (mPendingSavedState == null
530                || !mPendingSavedState.mHasSpanOffsets)) {
531            if (invalidateSpanOffsets) {
532                for (int i = 0; i < mSpanCount; i++) {
533                    // Scroll to position is set, clear.
534                    mSpans[i].clear();
535                    if (anchorOffset != INVALID_OFFSET) {
536                        mSpans[i].setLine(anchorOffset);
537                    }
538                }
539            } else {
540                for (int i = 0; i < mSpanCount; i++) {
541                    mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout, anchorOffset);
542                }
543                if (DEBUG) {
544                    for (int i = 0; i < mSpanCount; i++) {
545                        Log.d(TAG, "cached start-end lines for " + i + ":" +
546                                mSpans[i].mCachedStart + ":" + mSpans[i].mCachedEnd);
547                    }
548                }
549            }
550        }
551        mSizePerSpan = mSecondaryOrientation.getTotalSpace() / mSpanCount;
552        detachAndScrapAttachedViews(recycler);
553        // Layout start.
554        updateLayoutStateToFillStart(anchorItemPosition, state);
555        if (!layoutFromEnd) {
556            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
557        }
558        fill(recycler, mLayoutState, state);
559
560        // Layout end.
561        updateLayoutStateToFillEnd(anchorItemPosition, state);
562        if (layoutFromEnd) {
563            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
564        }
565        fill(recycler, mLayoutState, state);
566
567        if (getChildCount() > 0) {
568            if (mShouldReverseLayout) {
569                fixEndGap(recycler, state, true);
570                fixStartGap(recycler, state, false);
571            } else {
572                fixStartGap(recycler, state, true);
573                fixEndGap(recycler, state, false);
574            }
575        }
576
577        mPendingScrollPosition = RecyclerView.NO_POSITION;
578        mPendingScrollPositionOffset = INVALID_OFFSET;
579        mLastLayoutFromEnd = layoutFromEnd;
580        mPendingSavedState = null; // we don't need this anymore
581    }
582
583    /**
584     * Checks if a child is assigned to the non-optimal span.
585     *
586     * @param startChildIndex Starts checking after this child, inclusive
587     * @param endChildIndex   Starts checking until this child, exclusive
588     * @return The first View that is assigned to the wrong span.
589     */
590    View hasGapsToFix(int startChildIndex, int endChildIndex) {
591        // quick reject
592        if (startChildIndex >= endChildIndex) {
593            return null;
594        }
595        final int firstChildIndex, childLimit;
596        final int nextSpanDiff = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1;
597
598        if (mShouldReverseLayout) {
599            firstChildIndex = endChildIndex - 1;
600            childLimit = startChildIndex - 1;
601        } else {
602            firstChildIndex = startChildIndex;
603            childLimit = endChildIndex;
604        }
605        final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1;
606        for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) {
607            View child = getChildAt(i);
608            final int start = mPrimaryOrientation.getDecoratedStart(child);
609            final int end = mPrimaryOrientation.getDecoratedEnd(child);
610            LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
611            if (layoutParams.mFullSpan) {
612                continue; // quick reject
613            }
614            int nextSpanIndex = layoutParams.getSpanIndex() + nextSpanDiff;
615            while (nextSpanIndex >= 0 && nextSpanIndex < mSpanCount) {
616                Span nextSpan = mSpans[nextSpanIndex];
617                if (nextSpan.isEmpty(start, end)) {
618                    return child;
619                }
620                nextSpanIndex += nextSpanDiff;
621            }
622        }
623        // everything looks good
624        return null;
625    }
626
627    @Override
628    public boolean supportsPredictiveItemAnimations() {
629        return true;
630    }
631
632    private void measureChildWithDecorationsAndMargin(View child, int widthSpec,
633            int heightSpec) {
634        final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
635        LayoutParams lp = (LayoutParams) child.getLayoutParams();
636        widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + insets.left,
637                lp.rightMargin + insets.right);
638        heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + insets.top,
639                lp.bottomMargin + insets.bottom);
640        child.measure(widthSpec, heightSpec);
641    }
642
643    private int updateSpecWithExtra(int spec, int startInset, int endInset) {
644        if (startInset == 0 && endInset == 0) {
645            return spec;
646        }
647        final int mode = View.MeasureSpec.getMode(spec);
648        if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
649            return View.MeasureSpec.makeMeasureSpec(
650                    View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
651        }
652        return spec;
653    }
654
655    @Override
656    public void onRestoreInstanceState(Parcelable state) {
657        if (state instanceof SavedState) {
658            mPendingSavedState = (SavedState) state;
659            requestLayout();
660        } else if (DEBUG) {
661            Log.d(TAG, "invalid saved state class");
662        }
663    }
664
665    @Override
666    public Parcelable onSaveInstanceState() {
667        if (mPendingSavedState != null) {
668            return new SavedState(mPendingSavedState);
669        }
670        SavedState state = new SavedState();
671        state.mOrientation = mOrientation;
672        state.mReverseLayout = mReverseLayout;
673        state.mSpanCount = mSpanCount;
674        state.mAnchorLayoutFromEnd = mLastLayoutFromEnd;
675        state.mGapStrategy = mGapStrategy;
676
677        if (mLazySpanLookup != null && mLazySpanLookup.mData != null) {
678            state.mSpanLookup = mLazySpanLookup.mData;
679            state.mSpanLookupSize = state.mSpanLookup.length;
680        } else {
681            state.mSpanLookupSize = 0;
682        }
683
684        if (getChildCount() > 0) {
685            state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition()
686                    : getFirstChildPosition();
687            state.mHasSpanOffsets = true;
688            state.mSpanOffsets = new int[mSpanCount];
689            for (int i = 0; i < mSpanCount; i++) {
690                state.mSpanOffsets[i] = mLastLayoutFromEnd ? mSpans[i].getEndLine()
691                        : mSpans[i].getStartLine();
692            }
693        } else {
694            state.mAnchorPosition = RecyclerView.NO_POSITION;
695            state.mHasSpanOffsets = false;
696        }
697        if (DEBUG) {
698            Log.d(TAG, "saved state:\n" + state);
699        }
700        return state;
701    }
702
703    private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state,
704            boolean canOffsetChildren) {
705        final int maxEndLine = getMaxEnd(mPrimaryOrientation.getEndAfterPadding());
706        int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine;
707        int fixOffset;
708        if (gap > 0) {
709            fixOffset = -scrollBy(-gap, recycler, state);
710        } else {
711            return; // nothing to fix
712        }
713        gap -= fixOffset;
714        if (canOffsetChildren && gap > 0) {
715            mPrimaryOrientation.offsetChildren(gap);
716        }
717    }
718
719    private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state,
720            boolean canOffsetChildren) {
721        final int minStartLine = getMinStart(mPrimaryOrientation.getStartAfterPadding());
722        int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding();
723        int fixOffset;
724        if (gap > 0) {
725            fixOffset = scrollBy(gap, recycler, state);
726        } else {
727            return; // nothing to fix
728        }
729        gap -= fixOffset;
730        if (canOffsetChildren && gap > 0) {
731            mPrimaryOrientation.offsetChildren(-gap);
732        }
733    }
734
735    private void updateLayoutStateToFillStart(int anchorPosition, RecyclerView.State state) {
736        mLayoutState.mAvailable = 0;
737        mLayoutState.mCurrentPosition = anchorPosition;
738        if (isSmoothScrolling()) {
739            final int targetPos = state.getTargetScrollPosition();
740            if (mShouldReverseLayout == targetPos < anchorPosition) {
741                mLayoutState.mExtra = 0;
742            } else {
743                mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace();
744            }
745        } else {
746            mLayoutState.mExtra = 0;
747        }
748        mLayoutState.mLayoutDirection = LAYOUT_START;
749        mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL
750                : ITEM_DIRECTION_HEAD;
751    }
752
753    private void updateLayoutStateToFillEnd(int anchorPosition, RecyclerView.State state) {
754        mLayoutState.mAvailable = 0;
755        mLayoutState.mCurrentPosition = anchorPosition;
756        if (isSmoothScrolling()) {
757            final int targetPos = state.getTargetScrollPosition();
758            if (mShouldReverseLayout == targetPos > anchorPosition) {
759                mLayoutState.mExtra = 0;
760            } else {
761                mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace();
762            }
763        } else {
764            mLayoutState.mExtra = 0;
765        }
766        mLayoutState.mLayoutDirection = LAYOUT_END;
767        mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD
768                : ITEM_DIRECTION_TAIL;
769    }
770
771    @Override
772    public void offsetChildrenHorizontal(int dx) {
773        super.offsetChildrenHorizontal(dx);
774        for (int i = 0; i < mSpanCount; i++) {
775            mSpans[i].onOffset(dx);
776        }
777    }
778
779    @Override
780    public void offsetChildrenVertical(int dy) {
781        super.offsetChildrenVertical(dy);
782        for (int i = 0; i < mSpanCount; i++) {
783            mSpans[i].onOffset(dy);
784        }
785    }
786
787    @Override
788    public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
789        if (!considerSpanInvalidate(positionStart, itemCount)) {
790            // If positions are not invalidated, move span offsets.
791            mLazySpanLookup.offsetForRemoval(positionStart, itemCount);
792        }
793    }
794
795    @Override
796    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
797        if (!considerSpanInvalidate(positionStart, itemCount)) {
798            // If positions are not invalidated, move span offsets.
799            mLazySpanLookup.offsetForAddition(positionStart, itemCount);
800        }
801    }
802
803    /**
804     * Checks whether it should invalidate span assignments in response to an adapter change.
805     */
806    private boolean considerSpanInvalidate(int positionStart, int itemCount) {
807        int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition();
808        if (positionStart + itemCount <= minPosition) {
809            return false;// nothing to update.
810        }
811        int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition();
812        mLazySpanLookup.invalidateAfter(positionStart);
813        if (positionStart <= maxPosition) {
814            requestLayout();
815        }
816        return true;
817    }
818
819    private int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
820            RecyclerView.State state) {
821        mRemainingSpans.set(0, mSpanCount, true);
822        // The target position we are trying to reach.
823        final int targetLine;
824
825        /*
826        * The line until which we can recycle, as long as we add views.
827        * Keep in mind, it is still the line in layout direction which means; to calculate the
828        * actual recycle line, we should subtract/add the size in orientation.
829        */
830        final int recycleLine;
831        // Line of the furthest row.
832        if (layoutState.mLayoutDirection == LAYOUT_END) {
833            recycleLine = mPrimaryOrientation.getEndAfterPadding() + mLayoutState.mAvailable;
834            targetLine = recycleLine + mLayoutState.mExtra;
835            final int defaultLine = mPrimaryOrientation.getStartAfterPadding();
836            for (int i = 0; i < mSpanCount; i++) {
837                final Span span = mSpans[i];
838                final int line = span.getEndLine(defaultLine);
839                if (line > targetLine) {
840                    mRemainingSpans.set(i, false);
841                }
842            }
843        } else { // LAYOUT_START
844            recycleLine = mPrimaryOrientation.getStartAfterPadding() - mLayoutState.mAvailable;
845            targetLine = recycleLine - mLayoutState.mExtra;
846            for (int i = 0; i < mSpanCount; i++) {
847                final Span span = mSpans[i];
848                final int defaultLine = mPrimaryOrientation.getEndAfterPadding();
849                final int line = span.getStartLine(defaultLine);
850                if (line < targetLine) {
851                    mRemainingSpans.set(i, false);
852                }
853            }
854        }
855
856        final int widthSpec, heightSpec;
857        if (mOrientation == VERTICAL) {
858            widthSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY);
859            heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
860        } else {
861            heightSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY);
862            widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
863        }
864
865        while (layoutState.hasMore(state) && !mRemainingSpans.isEmpty()) {
866            View view = layoutState.next(recycler);
867            LayoutParams lp = ((LayoutParams) view.getLayoutParams());
868            if (layoutState.mLayoutDirection == LAYOUT_END) {
869                addView(view);
870            } else {
871                addView(view, 0);
872            }
873            if (lp.mFullSpan) {
874                final int fullSizeSpec = View.MeasureSpec.makeMeasureSpec(
875                        mSecondaryOrientation.getTotalSpace(), View.MeasureSpec.EXACTLY);
876                if (mOrientation == VERTICAL) {
877                    measureChildWithDecorationsAndMargin(view, fullSizeSpec, heightSpec);
878                } else {
879                    measureChildWithDecorationsAndMargin(view, widthSpec, fullSizeSpec);
880                }
881            } else {
882                measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
883            }
884
885            final int position = getPosition(view);
886            final int spanIndex = mLazySpanLookup.getSpan(position);
887            Span currentSpan;
888            if (spanIndex == LayoutParams.INVALID_SPAN_ID) {
889                if (lp.mFullSpan) {
890                    // assign full span items to first span
891                    currentSpan = mSpans[0];
892                } else {
893                    currentSpan = getNextSpan(layoutState);
894                }
895                mLazySpanLookup.setSpan(position, currentSpan);
896            } else {
897                currentSpan = mSpans[spanIndex];
898            }
899            final int start;
900            final int end;
901            if (layoutState.mLayoutDirection == LAYOUT_END) {
902                final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding()
903                        : mPrimaryOrientation.getStartAfterPadding();
904                start = lp.mFullSpan ? getMaxEnd(def) : currentSpan.getEndLine(def);
905                end = start + mPrimaryOrientation.getDecoratedMeasurement(view);
906                if (lp.mFullSpan) {
907                    for (int i = 0; i < mSpanCount; i++) {
908                        mSpans[i].appendToSpan(view);
909                    }
910                } else {
911                    currentSpan.appendToSpan(view);
912                }
913            } else {
914                final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding()
915                        : mPrimaryOrientation.getStartAfterPadding();
916                end = lp.mFullSpan ? getMinStart(def) : currentSpan.getStartLine(def);
917                start = end - mPrimaryOrientation.getDecoratedMeasurement(view);
918                if (lp.mFullSpan) {
919                    for (int i = 0; i < mSpanCount; i++) {
920                        mSpans[i].prependToSpan(view);
921                    }
922                } else {
923                    currentSpan.prependToSpan(view);
924                }
925
926            }
927            lp.mSpan = currentSpan;
928
929            if (DEBUG) {
930                Log.d(TAG, "adding view item " + lp.getViewPosition() + " between " + start + ","
931                        + end);
932            }
933
934            final int otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding()
935                    : currentSpan.mIndex * mSizePerSpan + mSecondaryOrientation
936                            .getStartAfterPadding();
937            final int otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view);
938            if (mOrientation == VERTICAL) {
939                layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end);
940            } else {
941                layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd);
942            }
943            if (lp.mFullSpan) {
944                for (int i = 0; i < mSpanCount; i++) {
945                    updateRemainingSpans(mSpans[i], mLayoutState.mLayoutDirection, targetLine);
946                }
947            } else {
948                updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine);
949            }
950            if (mLayoutState.mLayoutDirection == LAYOUT_START) {
951                // calculate recycle line
952                int maxStart = getMaxStart(currentSpan.getStartLine());
953                recycleFromEnd(recycler, Math.max(recycleLine, maxStart)
954                        + mPrimaryOrientation.getTotalSpace());
955            } else {
956                // calculate recycle line
957                int minEnd = getMinEnd(currentSpan.getEndLine());
958                recycleFromStart(recycler, Math.min(recycleLine, minEnd)
959                        - mPrimaryOrientation.getTotalSpace());
960            }
961        }
962        if (DEBUG) {
963            Log.d(TAG, "fill, " + getChildCount());
964        }
965        if (mLayoutState.mLayoutDirection == LAYOUT_START) {
966            final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding());
967            return Math.max(0, mLayoutState.mAvailable + (targetLine - minStart));
968        } else {
969            final int max = getMaxEnd(mPrimaryOrientation.getEndAfterPadding());
970            return Math.max(0, mLayoutState.mAvailable + (max - targetLine));
971        }
972    }
973
974    private void layoutDecoratedWithMargins(View child, int left, int top, int right,
975            int bottom) {
976        LayoutParams lp = (LayoutParams) child.getLayoutParams();
977        layoutDecorated(child, left + lp.leftMargin, top + lp.topMargin, right - lp.rightMargin
978                , bottom - lp.bottomMargin);
979    }
980
981    private void updateRemainingSpans(Span span, int layoutDir, int targetLine) {
982        final int deletedSize = span.getDeletedSize();
983        if (layoutDir == LAYOUT_START) {
984            final int line = span.getStartLine();
985            if (line + deletedSize < targetLine) {
986                mRemainingSpans.set(span.mIndex, false);
987            }
988        } else {
989            final int line = span.getEndLine();
990            if (line - deletedSize > targetLine) {
991                mRemainingSpans.set(span.mIndex, false);
992            }
993        }
994    }
995
996    private int getMaxStart(int def) {
997        int maxStart = mSpans[0].getStartLine(def);
998        for (int i = 1; i < mSpanCount; i++) {
999            final int spanStart = mSpans[i].getStartLine(def);
1000            if (spanStart > maxStart) {
1001                maxStart = spanStart;
1002            }
1003        }
1004        return maxStart;
1005    }
1006
1007    private int getMinStart(int def) {
1008        int minStart = mSpans[0].getStartLine(def);
1009        for (int i = 1; i < mSpanCount; i++) {
1010            final int spanStart = mSpans[i].getStartLine(def);
1011            if (spanStart < minStart) {
1012                minStart = spanStart;
1013            }
1014        }
1015        return minStart;
1016    }
1017
1018    private int getMaxEnd(int def) {
1019        int maxEnd = mSpans[0].getEndLine(def);
1020        for (int i = 1; i < mSpanCount; i++) {
1021            final int spanEnd = mSpans[i].getEndLine(def);
1022            if (spanEnd > maxEnd) {
1023                maxEnd = spanEnd;
1024            }
1025        }
1026        return maxEnd;
1027    }
1028
1029    private int getMinEnd(int def) {
1030        int minEnd = mSpans[0].getEndLine(def);
1031        for (int i = 1; i < mSpanCount; i++) {
1032            final int spanEnd = mSpans[i].getEndLine(def);
1033            if (spanEnd < minEnd) {
1034                minEnd = spanEnd;
1035            }
1036        }
1037        return minEnd;
1038    }
1039
1040    private void recycleFromStart(RecyclerView.Recycler recycler, int line) {
1041        if (DEBUG) {
1042            Log.d(TAG, "recycling from start for line " + line);
1043        }
1044        while (getChildCount() > 0) {
1045            View child = getChildAt(0);
1046            if (mPrimaryOrientation.getDecoratedEnd(child) < line) {
1047                LayoutParams lp = (LayoutParams) child.getLayoutParams();
1048                if (lp.mFullSpan) {
1049                    for (int j = 0; j < mSpanCount; j++) {
1050                        mSpans[j].popStart();
1051                    }
1052                } else {
1053                    lp.mSpan.popStart();
1054                }
1055                removeAndRecycleView(child, recycler);
1056            } else {
1057                return;// done
1058            }
1059        }
1060    }
1061
1062    private void recycleFromEnd(RecyclerView.Recycler recycler, int line) {
1063        final int childCount = getChildCount();
1064        int i;
1065        for (i = childCount - 1; i >= 0; i--) {
1066            View child = getChildAt(i);
1067            if (mPrimaryOrientation.getDecoratedStart(child) > line) {
1068                LayoutParams lp = (LayoutParams) child.getLayoutParams();
1069                if (lp.mFullSpan) {
1070                    for (int j = 0; j < mSpanCount; j++) {
1071                        mSpans[j].popEnd();
1072                    }
1073                } else {
1074                    lp.mSpan.popEnd();
1075                }
1076                removeAndRecycleView(child, recycler);
1077            } else {
1078                return;// done
1079            }
1080        }
1081    }
1082
1083    /**
1084     * Finds the span for the next view.
1085     */
1086    private Span getNextSpan(LayoutState layoutState) {
1087        final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL();
1088        if (layoutState.mLayoutDirection == LAYOUT_END) {
1089            Span min = mSpans[0];
1090            int minLine = min.getEndLine(mPrimaryOrientation.getStartAfterPadding());
1091            final int defaultLine = mPrimaryOrientation.getStartAfterPadding();
1092            for (int i = 1; i < mSpanCount; i++) {
1093                final Span other = mSpans[i];
1094                final int otherLine = other.getEndLine(defaultLine);
1095                if (otherLine < minLine || (otherLine == minLine && preferLastSpan)) {
1096                    min = other;
1097                    minLine = otherLine;
1098                }
1099            }
1100            return min;
1101        } else {
1102            Span max = mSpans[0];
1103            int maxLine = max.getStartLine(mPrimaryOrientation.getEndAfterPadding());
1104            final int defaultLine = mPrimaryOrientation.getEndAfterPadding();
1105            for (int i = 1; i < mSpanCount; i++) {
1106                final Span other = mSpans[i];
1107                final int otherLine = other.getStartLine(defaultLine);
1108                if (otherLine > maxLine || (otherLine == maxLine && !preferLastSpan)) {
1109                    max = other;
1110                    maxLine = otherLine;
1111                }
1112            }
1113            return max;
1114        }
1115    }
1116
1117    @Override
1118    public boolean canScrollVertically() {
1119        return mOrientation == VERTICAL;
1120    }
1121
1122    @Override
1123    public boolean canScrollHorizontally() {
1124        return mOrientation == HORIZONTAL;
1125    }
1126
1127    @Override
1128    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
1129            RecyclerView.State state) {
1130        return scrollBy(dx, recycler, state);
1131    }
1132
1133    @Override
1134    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
1135            RecyclerView.State state) {
1136        return scrollBy(dy, recycler, state);
1137    }
1138
1139    private int calculateScrollDirectionForPosition(int position) {
1140        if (getChildCount() == 0) {
1141            return mShouldReverseLayout ? LAYOUT_END : LAYOUT_START;
1142        }
1143        final int firstChildPos = getFirstChildPosition();
1144        return position < firstChildPos != mShouldReverseLayout ? LAYOUT_START : LAYOUT_END;
1145    }
1146
1147    @Override
1148    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
1149            int position) {
1150        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
1151            @Override
1152            public PointF computeScrollVectorForPosition(int targetPosition) {
1153                final int direction = calculateScrollDirectionForPosition(targetPosition);
1154                if (direction == 0) {
1155                    return null;
1156                }
1157                if (mOrientation == HORIZONTAL) {
1158                    return new PointF(direction, 0);
1159                } else {
1160                    return new PointF(0, direction);
1161                }
1162            }
1163        };
1164        scroller.setTargetPosition(position);
1165        startSmoothScroll(scroller);
1166    }
1167
1168    @Override
1169    public void scrollToPosition(int position) {
1170        if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) {
1171            mPendingSavedState.invalidateAnchorPositionInfo();
1172        }
1173        mPendingScrollPosition = position;
1174        mPendingScrollPositionOffset = INVALID_OFFSET;
1175        requestLayout();
1176    }
1177
1178    /**
1179     * Scroll to the specified adapter position with the given offset from layout start.
1180     * <p>
1181     * Note that scroll position change will not be reflected until the next layout call.
1182     * <p>
1183     * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
1184     *
1185     * @param position Index (starting at 0) of the reference item.
1186     * @param offset   The distance (in pixels) between the start edge of the item view and
1187     *                 start edge of the RecyclerView.
1188     * @see #setReverseLayout(boolean)
1189     * @see #scrollToPosition(int)
1190     */
1191    public void scrollToPositionWithOffset(int position, int offset) {
1192        if (mPendingSavedState != null) {
1193            mPendingSavedState.invalidateAnchorPositionInfo();
1194        }
1195        mPendingScrollPosition = position;
1196        mPendingScrollPositionOffset = offset;
1197        requestLayout();
1198    }
1199
1200    private int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
1201        ensureOrientationHelper();
1202        final int referenceChildPosition;
1203        if (dt > 0) { // layout towards end
1204            mLayoutState.mLayoutDirection = LAYOUT_END;
1205            mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD
1206                    : ITEM_DIRECTION_TAIL;
1207            referenceChildPosition = getLastChildPosition();
1208        } else {
1209            mLayoutState.mLayoutDirection = LAYOUT_START;
1210            mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL
1211                    : ITEM_DIRECTION_HEAD;
1212            referenceChildPosition = getFirstChildPosition();
1213        }
1214        mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
1215        final int absDt = Math.abs(dt);
1216        mLayoutState.mAvailable = absDt;
1217        mLayoutState.mExtra = isSmoothScrolling() ? mPrimaryOrientation.getTotalSpace() : 0;
1218        int consumed = fill(recycler, mLayoutState, state);
1219        final int totalScroll;
1220        if (absDt < consumed) {
1221            totalScroll = dt;
1222        } else if (dt < 0) {
1223            totalScroll = -consumed;
1224        } else { // dt > 0
1225            totalScroll = consumed;
1226        }
1227        if (DEBUG) {
1228            Log.d(TAG, "asked " + dt + " scrolled" + totalScroll);
1229        }
1230
1231        if (mGapStrategy == GAP_HANDLING_LAZY
1232                && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD) {
1233            final int targetStart = mPrimaryOrientation.getStartAfterPadding();
1234            final int targetEnd = mPrimaryOrientation.getEndAfterPadding();
1235            lazyOffsetSpans(-totalScroll, targetStart, targetEnd);
1236        } else {
1237            mPrimaryOrientation.offsetChildren(-totalScroll);
1238        }
1239        // always reset this if we scroll for a proper save instance state
1240        mLastLayoutFromEnd = mShouldReverseLayout;
1241
1242        if (totalScroll != 0 && mGapStrategy != GAP_HANDLING_NONE
1243                && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD && !mHasGaps) {
1244            final int addedChildCount = Math.abs(mLayoutState.mCurrentPosition
1245                    - (referenceChildPosition + mLayoutState.mItemDirection));
1246            if (addedChildCount > 0) {
1247                // check if any child has been attached to wrong span. If so, trigger a re-layout
1248                // after scroll
1249                final View viewInWrongSpan;
1250                final View referenceView = findViewByPosition(referenceChildPosition);
1251                if (referenceView == null) {
1252                    viewInWrongSpan = hasGapsToFix(0, getChildCount());
1253                } else {
1254                    if (mLayoutState.mLayoutDirection == LAYOUT_START) {
1255                        viewInWrongSpan = hasGapsToFix(0, addedChildCount);
1256                    } else {
1257                        viewInWrongSpan = hasGapsToFix(getChildCount() - addedChildCount,
1258                                getChildCount());
1259                    }
1260                }
1261                mHasGaps = viewInWrongSpan != null;
1262            }
1263        }
1264        return totalScroll;
1265    }
1266
1267    /**
1268     * The actual method that implements {@link #GAP_HANDLING_LAZY}
1269     */
1270    private void lazyOffsetSpans(int offset, int targetStart, int targetEnd) {
1271        // For each span offset children one by one.
1272        // When a fullSpan item is reached, stop and wait for other spans to reach to that span.
1273        // When all reach, offset fullSpan to max of others and continue.
1274        int childrenToOffset = getChildCount();
1275        int[] indexPerSpan = new int[mSpanCount];
1276        int[] offsetPerSpan = new int[mSpanCount];
1277
1278        final int childOrder = offset > 0 ? ITEM_DIRECTION_TAIL : ITEM_DIRECTION_HEAD;
1279        if (offset > 0) {
1280            Arrays.fill(indexPerSpan, 0);
1281        } else {
1282            for (int i = 0; i < mSpanCount; i++) {
1283                indexPerSpan[i] = mSpans[i].mViews.size() - 1;
1284            }
1285        }
1286
1287        for (int i = 0; i < mSpanCount; i++) {
1288            offsetPerSpan[i] = mSpans[i].getNormalizedOffset(offset, targetStart, targetEnd);
1289        }
1290        if (DEBUG) {
1291            Log.d(TAG, "lazy offset start. normalized: " + Arrays.toString(offsetPerSpan));
1292        }
1293
1294        while (childrenToOffset > 0) {
1295            View fullSpanView = null;
1296            for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) {
1297                Span span = mSpans[spanIndex];
1298                int viewIndex;
1299                for (viewIndex = indexPerSpan[spanIndex];
1300                        viewIndex < span.mViews.size() && viewIndex >= 0; viewIndex += childOrder) {
1301                    View view = span.mViews.get(viewIndex);
1302                    if (DEBUG) {
1303                        Log.d(TAG, "span " + spanIndex + ", view:" + viewIndex + ", pos:"
1304                                + getPosition(view));
1305                    }
1306                    LayoutParams lp = (LayoutParams) view.getLayoutParams();
1307                    if (lp.mFullSpan) {
1308                        if (DEBUG) {
1309                            Log.d(TAG, "stopping on full span view on index " + viewIndex
1310                                    + " in span " + spanIndex);
1311                        }
1312                        fullSpanView = view;
1313                        viewIndex += childOrder;// move to next view
1314                        break;
1315                    }
1316                    // offset this child normally
1317                    mPrimaryOrientation.offsetChild(view, offsetPerSpan[spanIndex]);
1318                    final int nextChildIndex = viewIndex + childOrder;
1319                    if (nextChildIndex < span.mViews.size() && nextChildIndex >= 0) {
1320                        View nextView = span.mViews.get(nextChildIndex);
1321                        // find gap between, before offset
1322                        if (childOrder == ITEM_DIRECTION_HEAD) {// negative
1323                            offsetPerSpan[spanIndex] = Math
1324                                    .min(0, mPrimaryOrientation.getDecoratedStart(view)
1325                                            - mPrimaryOrientation.getDecoratedEnd(nextView));
1326                        } else {
1327                            offsetPerSpan[spanIndex] = Math
1328                                    .max(0, mPrimaryOrientation.getDecoratedEnd(view) -
1329                                            mPrimaryOrientation.getDecoratedStart(nextView));
1330                        }
1331                        if (DEBUG) {
1332                            Log.d(TAG, "offset diff:" + offsetPerSpan[spanIndex] + " between "
1333                                    + getPosition(nextView) + " and " + getPosition(view));
1334                        }
1335                    }
1336                    childrenToOffset--;
1337                }
1338                indexPerSpan[spanIndex] = viewIndex;
1339            }
1340            if (fullSpanView != null) {
1341                // we have to offset this view. We'll offset it as the biggest amount necessary
1342                int winnerSpan = 0;
1343                int winnerSpanOffset = Math.abs(offsetPerSpan[winnerSpan]);
1344                for (int i = 1; i < mSpanCount; i++) {
1345                    final int spanOffset = Math.abs(offsetPerSpan[i]);
1346                    if (spanOffset > winnerSpanOffset) {
1347                        winnerSpan = i;
1348                        winnerSpanOffset = spanOffset;
1349                    }
1350                }
1351                if (DEBUG) {
1352                    Log.d(TAG, "winner offset:" + offsetPerSpan[winnerSpan] + " of " + winnerSpan);
1353                }
1354                mPrimaryOrientation.offsetChild(fullSpanView, offsetPerSpan[winnerSpan]);
1355                childrenToOffset--;
1356
1357                for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) {
1358                    final int nextViewIndex = indexPerSpan[spanIndex];
1359                    final Span span = mSpans[spanIndex];
1360                    if (nextViewIndex < span.mViews.size() && nextViewIndex > 0) {
1361                        View nextView = span.mViews.get(nextViewIndex);
1362                        // find gap between, before offset
1363                        if (childOrder == ITEM_DIRECTION_HEAD) {// negative
1364                            offsetPerSpan[spanIndex] = Math
1365                                    .min(0, mPrimaryOrientation.getDecoratedStart(fullSpanView)
1366                                            - mPrimaryOrientation.getDecoratedEnd(nextView));
1367                        } else {
1368                            offsetPerSpan[spanIndex] = Math
1369                                    .max(0, mPrimaryOrientation.getDecoratedEnd(fullSpanView) -
1370                                            mPrimaryOrientation.getDecoratedStart(nextView));
1371                        }
1372                    }
1373                }
1374            }
1375        }
1376        for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) {
1377            mSpans[spanIndex].invalidateCache();
1378        }
1379    }
1380
1381    private int getLastChildPosition() {
1382        final int childCount = getChildCount();
1383        return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1));
1384    }
1385
1386    private int getFirstChildPosition() {
1387        final int childCount = getChildCount();
1388        return childCount == 0 ? 0 : getPosition(getChildAt(0));
1389    }
1390
1391    @Override
1392    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
1393        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
1394                ViewGroup.LayoutParams.WRAP_CONTENT);
1395    }
1396
1397    @Override
1398    public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
1399        return new LayoutParams(c, attrs);
1400    }
1401
1402    @Override
1403    public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
1404        if (lp instanceof ViewGroup.MarginLayoutParams) {
1405            return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
1406        } else {
1407            return new LayoutParams(lp);
1408        }
1409    }
1410
1411    @Override
1412    public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
1413        return lp instanceof LayoutParams;
1414    }
1415
1416    public int getOrientation() {
1417        return mOrientation;
1418    }
1419
1420
1421    /**
1422     * LayoutParams used by StaggeredGridLayoutManager.
1423     */
1424    public static class LayoutParams extends RecyclerView.LayoutParams {
1425
1426        /**
1427         * Span Id for Views that are not laid out yet.
1428         */
1429        public static final int INVALID_SPAN_ID = -1;
1430
1431        // Package scope to be able to access from tests.
1432        Span mSpan;
1433
1434        boolean mFullSpan;
1435
1436        public LayoutParams(Context c, AttributeSet attrs) {
1437            super(c, attrs);
1438        }
1439
1440        public LayoutParams(int width, int height) {
1441            super(width, height);
1442        }
1443
1444        public LayoutParams(ViewGroup.MarginLayoutParams source) {
1445            super(source);
1446        }
1447
1448        public LayoutParams(ViewGroup.LayoutParams source) {
1449            super(source);
1450        }
1451
1452        public LayoutParams(RecyclerView.LayoutParams source) {
1453            super(source);
1454        }
1455
1456        /**
1457         * When set to true, the item will layout using all span area. That means, if orientation
1458         * is vertical, the view will have full width; if orientation is horizontal, the view will
1459         * have full height.
1460         *
1461         * @param fullSpan True if this item should traverse all spans.
1462         */
1463        public void setFullSpan(boolean fullSpan) {
1464            mFullSpan = fullSpan;
1465        }
1466
1467        /**
1468         * Returns the Span index to which this View is assigned.
1469         *
1470         * @return The Span index of the View. If View is not yet assigned to any span, returns
1471         * {@link #INVALID_SPAN_ID}.
1472         */
1473        public final int getSpanIndex() {
1474            if (mSpan == null) {
1475                return INVALID_SPAN_ID;
1476            }
1477            return mSpan.mIndex;
1478        }
1479    }
1480
1481    // Package scoped to access from tests.
1482    class Span {
1483
1484        final int INVALID_LINE = Integer.MIN_VALUE;
1485
1486        private ArrayList<View> mViews = new ArrayList<View>();
1487
1488        int mCachedStart = INVALID_LINE;
1489
1490        int mCachedEnd = INVALID_LINE;
1491
1492        int mDeletedSize = 0;
1493
1494        final int mIndex;
1495
1496        private Span(int index) {
1497            mIndex = index;
1498        }
1499
1500        int getStartLine(int def) {
1501            if (mCachedStart != INVALID_LINE) {
1502                return mCachedStart;
1503            }
1504            if (mViews.size() == 0) {
1505                return def;
1506            }
1507            mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0));
1508            return mCachedStart;
1509        }
1510
1511        // Use this one when default value does not make sense and not having a value means a bug.
1512        int getStartLine() {
1513            if (mCachedStart != INVALID_LINE) {
1514                return mCachedStart;
1515            }
1516            mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0));
1517            return mCachedStart;
1518        }
1519
1520        int getEndLine(int def) {
1521            if (mCachedEnd != INVALID_LINE) {
1522                return mCachedEnd;
1523            }
1524            final int size = mViews.size();
1525            if (size == 0) {
1526                return def;
1527            }
1528            mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(size - 1));
1529            return mCachedEnd;
1530        }
1531
1532        // Use this one when default value does not make sense and not having a value means a bug.
1533        int getEndLine() {
1534            if (mCachedEnd != INVALID_LINE) {
1535                return mCachedEnd;
1536            }
1537            mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(mViews.size() - 1));
1538            return mCachedEnd;
1539        }
1540
1541        void prependToSpan(View view) {
1542            LayoutParams lp = getLayoutParams(view);
1543            lp.mSpan = this;
1544            mViews.add(0, view);
1545            mCachedStart = INVALID_LINE;
1546            if (mViews.size() == 1) {
1547                mCachedEnd = INVALID_LINE;
1548            }
1549            if (lp.isItemRemoved()) {
1550                mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
1551            }
1552        }
1553
1554        void appendToSpan(View view) {
1555            LayoutParams lp = getLayoutParams(view);
1556            lp.mSpan = this;
1557            mViews.add(view);
1558            mCachedEnd = INVALID_LINE;
1559            if (mViews.size() == 1) {
1560                mCachedStart = INVALID_LINE;
1561            }
1562            if (lp.isItemRemoved()) {
1563                mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
1564            }
1565        }
1566
1567        // Useful method to preserve positions on a re-layout.
1568        void cacheReferenceLineAndClear(boolean reverseLayout, int offset) {
1569            int reference;
1570            if (reverseLayout) {
1571                reference = getEndLine(INVALID_LINE);
1572            } else {
1573                reference = getStartLine(INVALID_LINE);
1574            }
1575            clear();
1576            if (reference == INVALID_LINE) {
1577                return;
1578            }
1579            if (offset != INVALID_OFFSET) {
1580                reference += offset;
1581            }
1582            mCachedStart = mCachedEnd = reference;
1583        }
1584
1585        void clear() {
1586            mViews.clear();
1587            invalidateCache();
1588            mDeletedSize = 0;
1589        }
1590
1591        void invalidateCache() {
1592            mCachedStart = INVALID_LINE;
1593            mCachedEnd = INVALID_LINE;
1594        }
1595
1596        void setLine(int line) {
1597            mCachedEnd = mCachedStart = line;
1598        }
1599
1600        void popEnd() {
1601            final int size = mViews.size();
1602            View end = mViews.remove(size - 1);
1603            final LayoutParams lp = getLayoutParams(end);
1604            lp.mSpan = null;
1605            if (lp.isItemRemoved()) {
1606                mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end);
1607            }
1608            if (size == 1) {
1609                mCachedStart = INVALID_LINE;
1610            }
1611            mCachedEnd = INVALID_LINE;
1612        }
1613
1614        void popStart() {
1615            View start = mViews.remove(0);
1616            final LayoutParams lp = getLayoutParams(start);
1617            lp.mSpan = null;
1618            if (mViews.size() == 0) {
1619                mCachedEnd = INVALID_LINE;
1620            }
1621            if (lp.isItemRemoved()) {
1622                mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start);
1623            }
1624            mCachedStart = INVALID_LINE;
1625        }
1626
1627        // TODO cache this.
1628        public int getDeletedSize() {
1629            return mDeletedSize;
1630        }
1631
1632        LayoutParams getLayoutParams(View view) {
1633            return (LayoutParams) view.getLayoutParams();
1634        }
1635
1636        void onOffset(int dt) {
1637            if (mCachedStart != INVALID_LINE) {
1638                mCachedStart += dt;
1639            }
1640            if (mCachedEnd != INVALID_LINE) {
1641                mCachedEnd += dt;
1642            }
1643        }
1644
1645        // normalized offset is how much this span can scroll
1646        int getNormalizedOffset(int dt, int targetStart, int targetEnd) {
1647            if (mViews.size() == 0) {
1648                return 0;
1649            }
1650            if (dt < 0) {
1651                final int endSpace = getEndLine() - targetEnd;
1652                if (endSpace <= 0) {
1653                    return 0;
1654                }
1655                return -dt > endSpace ? -endSpace : dt;
1656            } else {
1657                final int startSpace = targetStart - getStartLine();
1658                if (startSpace <= 0) {
1659                    return 0;
1660                }
1661                return startSpace < dt ? startSpace : dt;
1662            }
1663        }
1664
1665        /**
1666         * Returns if there is no child between start-end lines
1667         *
1668         * @param start The start line
1669         * @param end   The end line
1670         * @return true if a new child can be added between start and end
1671         */
1672        boolean isEmpty(int start, int end) {
1673            final int count = mViews.size();
1674            for (int i = 0; i < count; i++) {
1675                final View view = mViews.get(i);
1676                if (mPrimaryOrientation.getDecoratedStart(view) < end &&
1677                        mPrimaryOrientation.getDecoratedEnd(view) > start) {
1678                    return false;
1679                }
1680            }
1681            return true;
1682        }
1683    }
1684
1685    /**
1686     * An array of mappings from adapter position to span.
1687     * This only grows when a write happens and it grows up to the size of the adapter.
1688     */
1689    static class LazySpanLookup {
1690
1691        private static final int MIN_SIZE = 10;
1692
1693        int[] mData;
1694
1695        int mAdapterSize; // we don't want to grow beyond that, unless it grows
1696
1697        void invalidateAfter(int position) {
1698            if (mData == null) {
1699                return;
1700            }
1701            if (position >= mData.length) {
1702                return;
1703            }
1704            Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID);
1705        }
1706
1707        int getSpan(int position) {
1708            if (mData == null || position >= mData.length) {
1709                return LayoutParams.INVALID_SPAN_ID;
1710            } else {
1711                return mData[position];
1712            }
1713        }
1714
1715        void setSpan(int position, Span span) {
1716            ensureSize(position);
1717            mData[position] = span.mIndex;
1718        }
1719
1720        int sizeForPosition(int position) {
1721            int len = mData.length;
1722            while (len <= position) {
1723                len *= 2;
1724            }
1725            if (len > mAdapterSize) {
1726                len = mAdapterSize;
1727            }
1728            return len;
1729        }
1730
1731        void ensureSize(int position) {
1732            if (mData == null) {
1733                mData = new int[Math.max(position, MIN_SIZE) + 1];
1734                Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
1735            } else if (position >= mData.length) {
1736                int[] old = mData;
1737                mData = new int[sizeForPosition(position)];
1738                System.arraycopy(old, 0, mData, 0, old.length);
1739                Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID);
1740            }
1741        }
1742
1743        void clear() {
1744            if (mData != null) {
1745                Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
1746            }
1747        }
1748
1749        void offsetForRemoval(int positionStart, int itemCount) {
1750            ensureSize(positionStart + itemCount);
1751            System.arraycopy(mData, positionStart + itemCount, mData, positionStart,
1752                    mData.length - positionStart - itemCount);
1753            Arrays.fill(mData, mData.length - itemCount, mData.length,
1754                    LayoutParams.INVALID_SPAN_ID);
1755        }
1756
1757        void offsetForAddition(int positionStart, int itemCount) {
1758            ensureSize(positionStart + itemCount);
1759            System.arraycopy(mData, positionStart, mData, positionStart + itemCount,
1760                    mData.length - positionStart - itemCount);
1761            Arrays.fill(mData, positionStart, positionStart + itemCount,
1762                    LayoutParams.INVALID_SPAN_ID);
1763        }
1764    }
1765
1766    static class SavedState implements Parcelable {
1767
1768        int mOrientation;
1769
1770        int mSpanCount;
1771
1772        int mGapStrategy;
1773
1774        int mAnchorPosition;
1775
1776        int[] mSpanOffsets;
1777
1778        int mSpanLookupSize;
1779
1780        int[] mSpanLookup;
1781
1782        boolean mReverseLayout;
1783
1784        boolean mAnchorLayoutFromEnd;
1785
1786        boolean mHasSpanOffsets;
1787
1788        public SavedState() {
1789        }
1790
1791        SavedState(Parcel in) {
1792            mOrientation = in.readInt();
1793            mSpanCount = in.readInt();
1794            mGapStrategy = in.readInt();
1795            mAnchorPosition = in.readInt();
1796            mHasSpanOffsets = in.readInt() == 1;
1797            if (mHasSpanOffsets) {
1798                mSpanOffsets = new int[mSpanCount];
1799                in.readIntArray(mSpanOffsets);
1800            }
1801
1802            mSpanLookupSize = in.readInt();
1803            if (mSpanLookupSize > 0) {
1804                mSpanLookup = new int[mSpanLookupSize];
1805                in.readIntArray(mSpanLookup);
1806            }
1807            mReverseLayout = in.readInt() == 1;
1808            mAnchorLayoutFromEnd = in.readInt() == 1;
1809        }
1810
1811        public SavedState(SavedState other) {
1812            mOrientation = other.mOrientation;
1813            mSpanCount = other.mSpanCount;
1814            mGapStrategy = other.mGapStrategy;
1815            mAnchorPosition = other.mAnchorPosition;
1816            mHasSpanOffsets = other.mHasSpanOffsets;
1817            mSpanOffsets = other.mSpanOffsets;
1818            mSpanLookupSize = other.mSpanLookupSize;
1819            mSpanLookup = other.mSpanLookup;
1820            mReverseLayout = other.mReverseLayout;
1821            mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
1822        }
1823
1824        void invalidateSpanInfo() {
1825            mSpanOffsets = null;
1826            mHasSpanOffsets = false;
1827            mSpanCount = -1;
1828            mSpanLookupSize = 0;
1829            mSpanLookup = null;
1830        }
1831
1832        void invalidateAnchorPositionInfo() {
1833            mSpanOffsets = null;
1834            mHasSpanOffsets = false;
1835            mAnchorPosition = RecyclerView.NO_POSITION;
1836        }
1837
1838        @Override
1839        public int describeContents() {
1840            return 0;
1841        }
1842
1843        @Override
1844        public void writeToParcel(Parcel dest, int flags) {
1845            dest.writeInt(mOrientation);
1846            dest.writeInt(mSpanCount);
1847            dest.writeInt(mGapStrategy);
1848            dest.writeInt(mAnchorPosition);
1849            dest.writeInt(mHasSpanOffsets ? 1 : 0);
1850            if (mHasSpanOffsets) {
1851                dest.writeIntArray(mSpanOffsets);
1852            }
1853            dest.writeInt(mSpanLookupSize);
1854            if (mSpanLookupSize > 0) {
1855                dest.writeIntArray(mSpanLookup);
1856            }
1857            dest.writeInt(mReverseLayout ? 1 : 0);
1858            dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
1859        }
1860
1861        @Override
1862        public String toString() {
1863            return "SavedState{" +
1864                    "mOrientation=" + mOrientation +
1865                    ", mSpanCount=" + mSpanCount +
1866                    ", mGapStrategy=" + mGapStrategy +
1867                    ", mAnchorPosition=" + mAnchorPosition +
1868                    ", mSpanOffsets=" + Arrays.toString(mSpanOffsets) +
1869                    ", mSpanLookupSize=" + mSpanLookupSize +
1870                    ", mSpanLookup=" + Arrays.toString(mSpanLookup) +
1871                    ", mReverseLayout=" + mReverseLayout +
1872                    ", mAnchorLayoutFromEnd=" + mAnchorLayoutFromEnd +
1873                    ", mHasSpanOffsets=" + mHasSpanOffsets +
1874                    '}';
1875        }
1876
1877        public static final Parcelable.Creator<SavedState> CREATOR
1878                = new Parcelable.Creator<SavedState>() {
1879            @Override
1880            public SavedState createFromParcel(Parcel in) {
1881                return new SavedState(in);
1882            }
1883
1884            @Override
1885            public SavedState[] newArray(int size) {
1886                return new SavedState[size];
1887            }
1888        };
1889    }
1890}
1891