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 languag`e governing permissions and
14 * limitations under the License.
15 */
16package android.support.v7.widget;
17
18import android.content.Context;
19import android.graphics.Rect;
20import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
21import android.util.AttributeSet;
22import android.util.Log;
23import android.util.SparseIntArray;
24import android.view.View;
25import android.view.ViewGroup;
26
27import java.util.Arrays;
28
29/**
30 * A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
31 * <p>
32 * By default, each item occupies 1 span. You can change it by providing a custom
33 * {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
34 */
35public class GridLayoutManager extends LinearLayoutManager {
36
37    private static final boolean DEBUG = false;
38    private static final String TAG = "GridLayoutManager";
39    public static final int DEFAULT_SPAN_COUNT = -1;
40    /**
41     * Span size have been changed but we've not done a new layout calculation.
42     */
43    boolean mPendingSpanCountChange = false;
44    int mSpanCount = DEFAULT_SPAN_COUNT;
45    /**
46     * Right borders for each span.
47     * <p>For <b>i-th</b> item start is {@link #mCachedBorders}[i-1] + 1
48     * and end is {@link #mCachedBorders}[i].
49     */
50    int [] mCachedBorders;
51    /**
52     * Temporary array to keep views in layoutChunk method
53     */
54    View[] mSet;
55    final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray();
56    final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray();
57    SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
58    // re-used variable to acquire decor insets from RecyclerView
59    final Rect mDecorInsets = new Rect();
60
61
62    /**
63     * Constructor used when layout manager is set in XML by RecyclerView attribute
64     * "layoutManager". If spanCount is not specified in the XML, it defaults to a
65     * single column.
66     *
67     * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount
68     */
69    public GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
70                             int defStyleRes) {
71        super(context, attrs, defStyleAttr, defStyleRes);
72        Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
73        setSpanCount(properties.spanCount);
74    }
75
76    /**
77     * Creates a vertical GridLayoutManager
78     *
79     * @param context Current context, will be used to access resources.
80     * @param spanCount The number of columns in the grid
81     */
82    public GridLayoutManager(Context context, int spanCount) {
83        super(context);
84        setSpanCount(spanCount);
85    }
86
87    /**
88     * @param context Current context, will be used to access resources.
89     * @param spanCount The number of columns or rows in the grid
90     * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
91     *                      #VERTICAL}.
92     * @param reverseLayout When set to true, layouts from end to start.
93     */
94    public GridLayoutManager(Context context, int spanCount, int orientation,
95            boolean reverseLayout) {
96        super(context, orientation, reverseLayout);
97        setSpanCount(spanCount);
98    }
99
100    /**
101     * stackFromEnd is not supported by GridLayoutManager. Consider using
102     * {@link #setReverseLayout(boolean)}.
103     */
104    @Override
105    public void setStackFromEnd(boolean stackFromEnd) {
106        if (stackFromEnd) {
107            throw new UnsupportedOperationException(
108                    "GridLayoutManager does not support stack from end."
109                            + " Consider using reverse layout");
110        }
111        super.setStackFromEnd(false);
112    }
113
114    @Override
115    public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
116            RecyclerView.State state) {
117        if (mOrientation == HORIZONTAL) {
118            return mSpanCount;
119        }
120        if (state.getItemCount() < 1) {
121            return 0;
122        }
123
124        // Row count is one more than the last item's row index.
125        return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1;
126    }
127
128    @Override
129    public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
130            RecyclerView.State state) {
131        if (mOrientation == VERTICAL) {
132            return mSpanCount;
133        }
134        if (state.getItemCount() < 1) {
135            return 0;
136        }
137
138        // Column count is one more than the last item's column index.
139        return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1;
140    }
141
142    @Override
143    public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
144            RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
145        ViewGroup.LayoutParams lp = host.getLayoutParams();
146        if (!(lp instanceof LayoutParams)) {
147            super.onInitializeAccessibilityNodeInfoForItem(host, info);
148            return;
149        }
150        LayoutParams glp = (LayoutParams) lp;
151        int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition());
152        if (mOrientation == HORIZONTAL) {
153            info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
154                    glp.getSpanIndex(), glp.getSpanSize(),
155                    spanGroupIndex, 1,
156                    mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
157        } else { // VERTICAL
158            info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
159                    spanGroupIndex , 1,
160                    glp.getSpanIndex(), glp.getSpanSize(),
161                    mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
162        }
163    }
164
165    @Override
166    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
167        if (state.isPreLayout()) {
168            cachePreLayoutSpanMapping();
169        }
170        super.onLayoutChildren(recycler, state);
171        if (DEBUG) {
172            validateChildOrder();
173        }
174        clearPreLayoutSpanMappingCache();
175    }
176
177    @Override
178    public void onLayoutCompleted(RecyclerView.State state) {
179        super.onLayoutCompleted(state);
180        mPendingSpanCountChange = false;
181    }
182
183    private void clearPreLayoutSpanMappingCache() {
184        mPreLayoutSpanSizeCache.clear();
185        mPreLayoutSpanIndexCache.clear();
186    }
187
188    private void cachePreLayoutSpanMapping() {
189        final int childCount = getChildCount();
190        for (int i = 0; i < childCount; i++) {
191            final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
192            final int viewPosition = lp.getViewLayoutPosition();
193            mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize());
194            mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex());
195        }
196    }
197
198    @Override
199    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
200        mSpanSizeLookup.invalidateSpanIndexCache();
201    }
202
203    @Override
204    public void onItemsChanged(RecyclerView recyclerView) {
205        mSpanSizeLookup.invalidateSpanIndexCache();
206    }
207
208    @Override
209    public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
210        mSpanSizeLookup.invalidateSpanIndexCache();
211    }
212
213    @Override
214    public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
215            Object payload) {
216        mSpanSizeLookup.invalidateSpanIndexCache();
217    }
218
219    @Override
220    public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
221        mSpanSizeLookup.invalidateSpanIndexCache();
222    }
223
224    @Override
225    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
226        if (mOrientation == HORIZONTAL) {
227            return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
228                    ViewGroup.LayoutParams.FILL_PARENT);
229        } else {
230            return new LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
231                    ViewGroup.LayoutParams.WRAP_CONTENT);
232        }
233    }
234
235    @Override
236    public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
237        return new LayoutParams(c, attrs);
238    }
239
240    @Override
241    public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
242        if (lp instanceof ViewGroup.MarginLayoutParams) {
243            return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
244        } else {
245            return new LayoutParams(lp);
246        }
247    }
248
249    @Override
250    public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
251        return lp instanceof LayoutParams;
252    }
253
254    /**
255     * Sets the source to get the number of spans occupied by each item in the adapter.
256     *
257     * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
258     *                       occupied by each item
259     */
260    public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
261        mSpanSizeLookup = spanSizeLookup;
262    }
263
264    /**
265     * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
266     *
267     * @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
268     */
269    public SpanSizeLookup getSpanSizeLookup() {
270        return mSpanSizeLookup;
271    }
272
273    private void updateMeasurements() {
274        int totalSpace;
275        if (getOrientation() == VERTICAL) {
276            totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
277        } else {
278            totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
279        }
280        calculateItemBorders(totalSpace);
281    }
282
283    @Override
284    public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
285        if (mCachedBorders == null) {
286            super.setMeasuredDimension(childrenBounds, wSpec, hSpec);
287        }
288        final int width, height;
289        final int horizontalPadding = getPaddingLeft() + getPaddingRight();
290        final int verticalPadding = getPaddingTop() + getPaddingBottom();
291        if (mOrientation == VERTICAL) {
292            final int usedHeight = childrenBounds.height() + verticalPadding;
293            height = chooseSize(hSpec, usedHeight, getMinimumHeight());
294            width = chooseSize(wSpec, mCachedBorders[mCachedBorders.length - 1] + horizontalPadding,
295                    getMinimumWidth());
296        } else {
297            final int usedWidth = childrenBounds.width() + horizontalPadding;
298            width = chooseSize(wSpec, usedWidth, getMinimumWidth());
299            height = chooseSize(hSpec, mCachedBorders[mCachedBorders.length - 1] + verticalPadding,
300                    getMinimumHeight());
301        }
302        setMeasuredDimension(width, height);
303    }
304
305    /**
306     * @param totalSpace Total available space after padding is removed
307     */
308    private void calculateItemBorders(int totalSpace) {
309        mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace);
310    }
311
312    /**
313     * @param cachedBorders The out array
314     * @param spanCount number of spans
315     * @param totalSpace total available space after padding is removed
316     * @return The updated array. Might be the same instance as the provided array if its size
317     * has not changed.
318     */
319    static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) {
320        if (cachedBorders == null || cachedBorders.length != spanCount + 1
321                || cachedBorders[cachedBorders.length - 1] != totalSpace) {
322            cachedBorders = new int[spanCount + 1];
323        }
324        cachedBorders[0] = 0;
325        int sizePerSpan = totalSpace / spanCount;
326        int sizePerSpanRemainder = totalSpace % spanCount;
327        int consumedPixels = 0;
328        int additionalSize = 0;
329        for (int i = 1; i <= spanCount; i++) {
330            int itemSize = sizePerSpan;
331            additionalSize += sizePerSpanRemainder;
332            if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) {
333                itemSize += 1;
334                additionalSize -= spanCount;
335            }
336            consumedPixels += itemSize;
337            cachedBorders[i] = consumedPixels;
338        }
339        return cachedBorders;
340    }
341
342    @Override
343    void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state,
344                       AnchorInfo anchorInfo, int itemDirection) {
345        super.onAnchorReady(recycler, state, anchorInfo, itemDirection);
346        updateMeasurements();
347        if (state.getItemCount() > 0 && !state.isPreLayout()) {
348            ensureAnchorIsInCorrectSpan(recycler, state, anchorInfo, itemDirection);
349        }
350        ensureViewSet();
351    }
352
353    private void ensureViewSet() {
354        if (mSet == null || mSet.length != mSpanCount) {
355            mSet = new View[mSpanCount];
356        }
357    }
358
359    @Override
360    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
361            RecyclerView.State state) {
362        updateMeasurements();
363        ensureViewSet();
364        return super.scrollHorizontallyBy(dx, recycler, state);
365    }
366
367    @Override
368    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
369            RecyclerView.State state) {
370        updateMeasurements();
371        ensureViewSet();
372        return super.scrollVerticallyBy(dy, recycler, state);
373    }
374
375    private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler,
376            RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection) {
377        final boolean layingOutInPrimaryDirection =
378                itemDirection == LayoutState.ITEM_DIRECTION_TAIL;
379        int span = getSpanIndex(recycler, state, anchorInfo.mPosition);
380        if (layingOutInPrimaryDirection) {
381            // choose span 0
382            while (span > 0 && anchorInfo.mPosition > 0) {
383                anchorInfo.mPosition--;
384                span = getSpanIndex(recycler, state, anchorInfo.mPosition);
385            }
386        } else {
387            // choose the max span we can get. hopefully last one
388            final int indexLimit = state.getItemCount() - 1;
389            int pos = anchorInfo.mPosition;
390            int bestSpan = span;
391            while (pos < indexLimit) {
392                int next = getSpanIndex(recycler, state, pos + 1);
393                if (next > bestSpan) {
394                    pos += 1;
395                    bestSpan = next;
396                } else {
397                    break;
398                }
399            }
400            anchorInfo.mPosition = pos;
401        }
402    }
403
404    @Override
405    View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
406                            int start, int end, int itemCount) {
407        ensureLayoutState();
408        View invalidMatch = null;
409        View outOfBoundsMatch = null;
410        final int boundsStart = mOrientationHelper.getStartAfterPadding();
411        final int boundsEnd = mOrientationHelper.getEndAfterPadding();
412        final int diff = end > start ? 1 : -1;
413        for (int i = start; i != end; i += diff) {
414            final View view = getChildAt(i);
415            final int position = getPosition(view);
416            if (position >= 0 && position < itemCount) {
417                final int span = getSpanIndex(recycler, state, position);
418                if (span != 0) {
419                    continue;
420                }
421                if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) {
422                    if (invalidMatch == null) {
423                        invalidMatch = view; // removed item, least preferred
424                    }
425                } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd ||
426                        mOrientationHelper.getDecoratedEnd(view) < boundsStart) {
427                    if (outOfBoundsMatch == null) {
428                        outOfBoundsMatch = view; // item is not visible, less preferred
429                    }
430                } else {
431                    return view;
432                }
433            }
434        }
435        return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch;
436    }
437
438    private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state,
439            int viewPosition) {
440        if (!state.isPreLayout()) {
441            return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount);
442        }
443        final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition);
444        if (adapterPosition == -1) {
445            if (DEBUG) {
446                throw new RuntimeException("Cannot find span group index for position "
447                        + viewPosition);
448            }
449            Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition);
450            return 0;
451        }
452        return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount);
453    }
454
455    private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
456        if (!state.isPreLayout()) {
457            return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount);
458        }
459        final int cached = mPreLayoutSpanIndexCache.get(pos, -1);
460        if (cached != -1) {
461            return cached;
462        }
463        final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
464        if (adapterPosition == -1) {
465            if (DEBUG) {
466                throw new RuntimeException("Cannot find span index for pre layout position. It is"
467                        + " not cached, not in the adapter. Pos:" + pos);
468            }
469            Log.w(TAG, "Cannot find span size for pre layout position. It is"
470                    + " not cached, not in the adapter. Pos:" + pos);
471            return 0;
472        }
473        return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount);
474    }
475
476    private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
477        if (!state.isPreLayout()) {
478            return mSpanSizeLookup.getSpanSize(pos);
479        }
480        final int cached = mPreLayoutSpanSizeCache.get(pos, -1);
481        if (cached != -1) {
482            return cached;
483        }
484        final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
485        if (adapterPosition == -1) {
486            if (DEBUG) {
487                throw new RuntimeException("Cannot find span size for pre layout position. It is"
488                        + " not cached, not in the adapter. Pos:" + pos);
489            }
490            Log.w(TAG, "Cannot find span size for pre layout position. It is"
491                    + " not cached, not in the adapter. Pos:" + pos);
492            return 1;
493        }
494        return mSpanSizeLookup.getSpanSize(adapterPosition);
495    }
496
497    @Override
498    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
499            LayoutState layoutState, LayoutChunkResult result) {
500        final int otherDirSpecMode = mOrientationHelper.getModeInOther();
501        final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY;
502        final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0;
503        // if grid layout's dimensions are not specified, let the new row change the measurements
504        // This is not perfect since we not covering all rows but still solves an important case
505        // where they may have a header row which should be laid out according to children.
506        if (flexibleInOtherDir) {
507            updateMeasurements(); //  reset measurements
508        }
509        final boolean layingOutInPrimaryDirection =
510                layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
511        int count = 0;
512        int consumedSpanCount = 0;
513        int remainingSpan = mSpanCount;
514        if (!layingOutInPrimaryDirection) {
515            int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
516            int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
517            remainingSpan = itemSpanIndex + itemSpanSize;
518        }
519        while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
520            int pos = layoutState.mCurrentPosition;
521            final int spanSize = getSpanSize(recycler, state, pos);
522            if (spanSize > mSpanCount) {
523                throw new IllegalArgumentException("Item at position " + pos + " requires " +
524                        spanSize + " spans but GridLayoutManager has only " + mSpanCount
525                        + " spans.");
526            }
527            remainingSpan -= spanSize;
528            if (remainingSpan < 0) {
529                break; // item did not fit into this row or column
530            }
531            View view = layoutState.next(recycler);
532            if (view == null) {
533                break;
534            }
535            consumedSpanCount += spanSize;
536            mSet[count] = view;
537            count++;
538        }
539
540        if (count == 0) {
541            result.mFinished = true;
542            return;
543        }
544
545        int maxSize = 0;
546        float maxSizeInOther = 0; // use a float to get size per span
547
548        // we should assign spans before item decor offsets are calculated
549        assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
550        for (int i = 0; i < count; i++) {
551            View view = mSet[i];
552            if (layoutState.mScrapList == null) {
553                if (layingOutInPrimaryDirection) {
554                    addView(view);
555                } else {
556                    addView(view, 0);
557                }
558            } else {
559                if (layingOutInPrimaryDirection) {
560                    addDisappearingView(view);
561                } else {
562                    addDisappearingView(view, 0);
563                }
564            }
565
566            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
567            final int spec = getChildMeasureSpec(mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
568                    mCachedBorders[lp.mSpanIndex], otherDirSpecMode, 0,
569                    mOrientation == HORIZONTAL ? lp.height : lp.width,
570                    false);
571            final int mainSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(),
572                    mOrientationHelper.getMode(), 0,
573                    mOrientation == VERTICAL ? lp.height : lp.width, true);
574            // Unless the child has MATCH_PARENT, measure it from its specs before adding insets.
575            if (mOrientation == VERTICAL) {
576                @SuppressWarnings("deprecation")
577                final boolean applyInsets = lp.height == ViewGroup.LayoutParams.FILL_PARENT;
578                measureChildWithDecorationsAndMargin(view, spec, mainSpec, applyInsets, false);
579            } else {
580                //noinspection deprecation
581                final boolean applyInsets = lp.width == ViewGroup.LayoutParams.FILL_PARENT;
582                measureChildWithDecorationsAndMargin(view, mainSpec, spec, applyInsets, false);
583            }
584            final int size = mOrientationHelper.getDecoratedMeasurement(view);
585            if (size > maxSize) {
586                maxSize = size;
587            }
588            final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) /
589                    lp.mSpanSize;
590            if (otherSize > maxSizeInOther) {
591                maxSizeInOther = otherSize;
592            }
593        }
594        if (flexibleInOtherDir) {
595            // re-distribute columns
596            guessMeasurement(maxSizeInOther, currentOtherDirSize);
597            // now we should re-measure any item that was match parent.
598            maxSize = 0;
599            for (int i = 0; i < count; i++) {
600                View view = mSet[i];
601                final LayoutParams lp = (LayoutParams) view.getLayoutParams();
602                final int spec = getChildMeasureSpec(mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
603                                mCachedBorders[lp.mSpanIndex], View.MeasureSpec.EXACTLY, 0,
604                        mOrientation == HORIZONTAL ? lp.height : lp.width, false);
605                final int mainSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(),
606                        mOrientationHelper.getMode(), 0,
607                        mOrientation == VERTICAL ? lp.height : lp.width, true);
608                if (mOrientation == VERTICAL) {
609                    measureChildWithDecorationsAndMargin(view, spec, mainSpec, false, true);
610                } else {
611                    measureChildWithDecorationsAndMargin(view, mainSpec, spec, false, true);
612                }
613                final int size = mOrientationHelper.getDecoratedMeasurement(view);
614                if (size > maxSize) {
615                    maxSize = size;
616                }
617            }
618        }
619        // Views that did not measure the maxSize has to be re-measured
620        // We will stop doing this once we introduce Gravity in the GLM layout params
621        final int maxMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxSize,
622                View.MeasureSpec.EXACTLY);
623        for (int i = 0; i < count; i ++) {
624            final View view = mSet[i];
625            if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
626                final LayoutParams lp = (LayoutParams) view.getLayoutParams();
627                final int spec = getChildMeasureSpec(mCachedBorders[lp.mSpanIndex + lp.mSpanSize]
628                                - mCachedBorders[lp.mSpanIndex], View.MeasureSpec.EXACTLY, 0,
629                        mOrientation == HORIZONTAL ? lp.height : lp.width, false);
630                if (mOrientation == VERTICAL) {
631                    measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec, true, true);
632                } else {
633                    measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec, true, true);
634                }
635            }
636        }
637
638        result.mConsumed = maxSize;
639
640        int left = 0, right = 0, top = 0, bottom = 0;
641        if (mOrientation == VERTICAL) {
642            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
643                bottom = layoutState.mOffset;
644                top = bottom - maxSize;
645            } else {
646                top = layoutState.mOffset;
647                bottom = top + maxSize;
648            }
649        } else {
650            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
651                right = layoutState.mOffset;
652                left = right - maxSize;
653            } else {
654                left = layoutState.mOffset;
655                right = left + maxSize;
656            }
657        }
658        for (int i = 0; i < count; i++) {
659            View view = mSet[i];
660            LayoutParams params = (LayoutParams) view.getLayoutParams();
661            if (mOrientation == VERTICAL) {
662                if (isLayoutRTL()) {
663                    right = getPaddingLeft() + mCachedBorders[params.mSpanIndex + params.mSpanSize];
664                    left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
665                } else {
666                    left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
667                    right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
668                }
669            } else {
670                top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
671                bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
672            }
673            // We calculate everything with View's bounding box (which includes decor and margins)
674            // To calculate correct layout position, we subtract margins.
675            layoutDecoratedWithMargins(view, left, top, right, bottom);
676            if (DEBUG) {
677                Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
678                        + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
679                        + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
680                        + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
681            }
682            // Consume the available space if the view is not removed OR changed
683            if (params.isItemRemoved() || params.isItemChanged()) {
684                result.mIgnoreConsumed = true;
685            }
686            result.mFocusable |= view.isFocusable();
687        }
688        Arrays.fill(mSet, null);
689    }
690
691    /**
692     * This is called after laying out a row (if vertical) or a column (if horizontal) when the
693     * RecyclerView does not have exact measurement specs.
694     * <p>
695     * Here we try to assign a best guess width or height and re-do the layout to update other
696     * views that wanted to FILL_PARENT in the non-scroll orientation.
697     *
698     * @param maxSizeInOther The maximum size per span ratio from the measurement of the children.
699     * @param currentOtherDirSize The size before this layout chunk. There is no reason to go below.
700     */
701    private void guessMeasurement(float maxSizeInOther, int currentOtherDirSize) {
702        final int contentSize = Math.round(maxSizeInOther * mSpanCount);
703        // always re-calculate because borders were stretched during the fill
704        calculateItemBorders(Math.max(contentSize, currentOtherDirSize));
705    }
706
707    private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec,
708            boolean capBothSpecs, boolean alreadyMeasured) {
709        calculateItemDecorationsForChild(child, mDecorInsets);
710        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
711        if (capBothSpecs || mOrientation == VERTICAL) {
712            widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left,
713                    lp.rightMargin + mDecorInsets.right);
714        }
715        if (capBothSpecs || mOrientation == HORIZONTAL) {
716            heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
717                    lp.bottomMargin + mDecorInsets.bottom);
718        }
719        final boolean measure;
720        if (alreadyMeasured) {
721            measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp);
722        } else {
723            measure = shouldMeasureChild(child, widthSpec, heightSpec, lp);
724        }
725        if (measure) {
726            child.measure(widthSpec, heightSpec);
727        }
728
729    }
730
731    private int updateSpecWithExtra(int spec, int startInset, int endInset) {
732        if (startInset == 0 && endInset == 0) {
733            return spec;
734        }
735        final int mode = View.MeasureSpec.getMode(spec);
736        if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
737            return View.MeasureSpec.makeMeasureSpec(
738                    Math.max(0, View.MeasureSpec.getSize(spec) - startInset - endInset), mode);
739        }
740        return spec;
741    }
742
743    private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
744            int consumedSpanCount, boolean layingOutInPrimaryDirection) {
745        int span, spanDiff, start, end, diff;
746        // make sure we traverse from min position to max position
747        if (layingOutInPrimaryDirection) {
748            start = 0;
749            end = count;
750            diff = 1;
751        } else {
752            start = count - 1;
753            end = -1;
754            diff = -1;
755        }
756        if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span
757            span = mSpanCount - 1;
758            spanDiff = -1;
759        } else {
760            span = 0;
761            spanDiff = 1;
762        }
763        for (int i = start; i != end; i += diff) {
764            View view = mSet[i];
765            LayoutParams params = (LayoutParams) view.getLayoutParams();
766            params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
767            if (spanDiff == -1 && params.mSpanSize > 1) {
768                params.mSpanIndex = span - (params.mSpanSize - 1);
769            } else {
770                params.mSpanIndex = span;
771            }
772            span += spanDiff * params.mSpanSize;
773        }
774    }
775
776    /**
777     * Returns the number of spans laid out by this grid.
778     *
779     * @return The number of spans
780     * @see #setSpanCount(int)
781     */
782    public int getSpanCount() {
783        return mSpanCount;
784    }
785
786    /**
787     * Sets the number of spans to be laid out.
788     * <p>
789     * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
790     * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
791     *
792     * @param spanCount The total number of spans in the grid
793     * @see #getSpanCount()
794     */
795    public void setSpanCount(int spanCount) {
796        if (spanCount == mSpanCount) {
797            return;
798        }
799        mPendingSpanCountChange = true;
800        if (spanCount < 1) {
801            throw new IllegalArgumentException("Span count should be at least 1. Provided "
802                    + spanCount);
803        }
804        mSpanCount = spanCount;
805        mSpanSizeLookup.invalidateSpanIndexCache();
806        requestLayout();
807    }
808
809    /**
810     * A helper class to provide the number of spans each item occupies.
811     * <p>
812     * Default implementation sets each item to occupy exactly 1 span.
813     *
814     * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
815     */
816    public static abstract class SpanSizeLookup {
817
818        final SparseIntArray mSpanIndexCache = new SparseIntArray();
819
820        private boolean mCacheSpanIndices = false;
821
822        /**
823         * Returns the number of span occupied by the item at <code>position</code>.
824         *
825         * @param position The adapter position of the item
826         * @return The number of spans occupied by the item at the provided position
827         */
828        abstract public int getSpanSize(int position);
829
830        /**
831         * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or
832         * not. By default these values are not cached. If you are not overriding
833         * {@link #getSpanIndex(int, int)}, you should set this to true for better performance.
834         *
835         * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not.
836         */
837        public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) {
838            mCacheSpanIndices = cacheSpanIndices;
839        }
840
841        /**
842         * Clears the span index cache. GridLayoutManager automatically calls this method when
843         * adapter changes occur.
844         */
845        public void invalidateSpanIndexCache() {
846            mSpanIndexCache.clear();
847        }
848
849        /**
850         * Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not.
851         *
852         * @return True if results of {@link #getSpanIndex(int, int)} are cached.
853         */
854        public boolean isSpanIndexCacheEnabled() {
855            return mCacheSpanIndices;
856        }
857
858        int getCachedSpanIndex(int position, int spanCount) {
859            if (!mCacheSpanIndices) {
860                return getSpanIndex(position, spanCount);
861            }
862            final int existing = mSpanIndexCache.get(position, -1);
863            if (existing != -1) {
864                return existing;
865            }
866            final int value = getSpanIndex(position, spanCount);
867            mSpanIndexCache.put(position, value);
868            return value;
869        }
870
871        /**
872         * Returns the final span index of the provided position.
873         * <p>
874         * If you have a faster way to calculate span index for your items, you should override
875         * this method. Otherwise, you should enable span index cache
876         * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is
877         * disabled, default implementation traverses all items from 0 to
878         * <code>position</code>. When caching is enabled, it calculates from the closest cached
879         * value before the <code>position</code>.
880         * <p>
881         * If you override this method, you need to make sure it is consistent with
882         * {@link #getSpanSize(int)}. GridLayoutManager does not call this method for
883         * each item. It is called only for the reference item and rest of the items
884         * are assigned to spans based on the reference item. For example, you cannot assign a
885         * position to span 2 while span 1 is empty.
886         * <p>
887         * Note that span offsets always start with 0 and are not affected by RTL.
888         *
889         * @param position  The position of the item
890         * @param spanCount The total number of spans in the grid
891         * @return The final span position of the item. Should be between 0 (inclusive) and
892         * <code>spanCount</code>(exclusive)
893         */
894        public int getSpanIndex(int position, int spanCount) {
895            int positionSpanSize = getSpanSize(position);
896            if (positionSpanSize == spanCount) {
897                return 0; // quick return for full-span items
898            }
899            int span = 0;
900            int startPos = 0;
901            // If caching is enabled, try to jump
902            if (mCacheSpanIndices && mSpanIndexCache.size() > 0) {
903                int prevKey = findReferenceIndexFromCache(position);
904                if (prevKey >= 0) {
905                    span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey);
906                    startPos = prevKey + 1;
907                }
908            }
909            for (int i = startPos; i < position; i++) {
910                int size = getSpanSize(i);
911                span += size;
912                if (span == spanCount) {
913                    span = 0;
914                } else if (span > spanCount) {
915                    // did not fit, moving to next row / column
916                    span = size;
917                }
918            }
919            if (span + positionSpanSize <= spanCount) {
920                return span;
921            }
922            return 0;
923        }
924
925        int findReferenceIndexFromCache(int position) {
926            int lo = 0;
927            int hi = mSpanIndexCache.size() - 1;
928
929            while (lo <= hi) {
930                final int mid = (lo + hi) >>> 1;
931                final int midVal = mSpanIndexCache.keyAt(mid);
932                if (midVal < position) {
933                    lo = mid + 1;
934                } else {
935                    hi = mid - 1;
936                }
937            }
938            int index = lo - 1;
939            if (index >= 0 && index < mSpanIndexCache.size()) {
940                return mSpanIndexCache.keyAt(index);
941            }
942            return -1;
943        }
944
945        /**
946         * Returns the index of the group this position belongs.
947         * <p>
948         * For example, if grid has 3 columns and each item occupies 1 span, span group index
949         * for item 1 will be 0, item 5 will be 1.
950         *
951         * @param adapterPosition The position in adapter
952         * @param spanCount The total number of spans in the grid
953         * @return The index of the span group including the item at the given adapter position
954         */
955        public int getSpanGroupIndex(int adapterPosition, int spanCount) {
956            int span = 0;
957            int group = 0;
958            int positionSpanSize = getSpanSize(adapterPosition);
959            for (int i = 0; i < adapterPosition; i++) {
960                int size = getSpanSize(i);
961                span += size;
962                if (span == spanCount) {
963                    span = 0;
964                    group++;
965                } else if (span > spanCount) {
966                    // did not fit, moving to next row / column
967                    span = size;
968                    group++;
969                }
970            }
971            if (span + positionSpanSize > spanCount) {
972                group++;
973            }
974            return group;
975        }
976    }
977
978    @Override
979    public View onFocusSearchFailed(View focused, int focusDirection,
980            RecyclerView.Recycler recycler, RecyclerView.State state) {
981        View prevFocusedChild = findContainingItemView(focused);
982        if (prevFocusedChild == null) {
983            return null;
984        }
985        LayoutParams lp = (LayoutParams) prevFocusedChild.getLayoutParams();
986        final int prevSpanStart = lp.mSpanIndex;
987        final int prevSpanEnd = lp.mSpanIndex + lp.mSpanSize;
988        View view = super.onFocusSearchFailed(focused, focusDirection, recycler, state);
989        if (view == null) {
990            return null;
991        }
992        // LinearLayoutManager finds the last child. What we want is the child which has the same
993        // spanIndex.
994        final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection);
995        final boolean ascend = (layoutDir == LayoutState.LAYOUT_END) != mShouldReverseLayout;
996        final int start, inc, limit;
997        if (ascend) {
998            start = getChildCount() - 1;
999            inc = -1;
1000            limit = -1;
1001        } else {
1002            start = 0;
1003            inc = 1;
1004            limit = getChildCount();
1005        }
1006        final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL();
1007        View weakCandidate = null; // somewhat matches but not strong
1008        int weakCandidateSpanIndex = -1;
1009        int weakCandidateOverlap = 0; // how many spans overlap
1010
1011        for (int i = start; i != limit; i += inc) {
1012            View candidate = getChildAt(i);
1013            if (candidate == prevFocusedChild) {
1014                break;
1015            }
1016            if (!candidate.isFocusable()) {
1017                continue;
1018            }
1019            final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams();
1020            final int candidateStart = candidateLp.mSpanIndex;
1021            final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize;
1022            if (candidateStart == prevSpanStart && candidateEnd == prevSpanEnd) {
1023                return candidate; // perfect match
1024            }
1025            boolean assignAsWeek = false;
1026            if (weakCandidate == null) {
1027                assignAsWeek = true;
1028            } else {
1029                int maxStart = Math.max(candidateStart, prevSpanStart);
1030                int minEnd = Math.min(candidateEnd, prevSpanEnd);
1031                int overlap = minEnd - maxStart;
1032                if (overlap > weakCandidateOverlap) {
1033                    assignAsWeek = true;
1034                } else if (overlap == weakCandidateOverlap &&
1035                        preferLastSpan == (candidateStart > weakCandidateSpanIndex)) {
1036                    assignAsWeek = true;
1037                }
1038            }
1039
1040            if (assignAsWeek) {
1041                weakCandidate = candidate;
1042                weakCandidateSpanIndex = candidateLp.mSpanIndex;
1043                weakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) -
1044                        Math.max(candidateStart, prevSpanStart);
1045            }
1046        }
1047        return weakCandidate;
1048    }
1049
1050    @Override
1051    public boolean supportsPredictiveItemAnimations() {
1052        return mPendingSavedState == null && !mPendingSpanCountChange;
1053    }
1054
1055    /**
1056     * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
1057     */
1058    public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
1059
1060        @Override
1061        public int getSpanSize(int position) {
1062            return 1;
1063        }
1064
1065        @Override
1066        public int getSpanIndex(int position, int spanCount) {
1067            return position % spanCount;
1068        }
1069    }
1070
1071    /**
1072     * LayoutParams used by GridLayoutManager.
1073     * <p>
1074     * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the
1075     * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is
1076     * expected to fill all of the space given to it.
1077     */
1078    public static class LayoutParams extends RecyclerView.LayoutParams {
1079
1080        /**
1081         * Span Id for Views that are not laid out yet.
1082         */
1083        public static final int INVALID_SPAN_ID = -1;
1084
1085        private int mSpanIndex = INVALID_SPAN_ID;
1086
1087        private int mSpanSize = 0;
1088
1089        public LayoutParams(Context c, AttributeSet attrs) {
1090            super(c, attrs);
1091        }
1092
1093        public LayoutParams(int width, int height) {
1094            super(width, height);
1095        }
1096
1097        public LayoutParams(ViewGroup.MarginLayoutParams source) {
1098            super(source);
1099        }
1100
1101        public LayoutParams(ViewGroup.LayoutParams source) {
1102            super(source);
1103        }
1104
1105        public LayoutParams(RecyclerView.LayoutParams source) {
1106            super(source);
1107        }
1108
1109        /**
1110         * Returns the current span index of this View. If the View is not laid out yet, the return
1111         * value is <code>undefined</code>.
1112         * <p>
1113         * Note that span index may change by whether the RecyclerView is RTL or not. For
1114         * example, if the number of spans is 3 and layout is RTL, the rightmost item will have
1115         * span index of 2. If the layout changes back to LTR, span index for this view will be 0.
1116         * If the item was occupying 2 spans, span indices would be 1 and 0 respectively.
1117         * <p>
1118         * If the View occupies multiple spans, span with the minimum index is returned.
1119         *
1120         * @return The span index of the View.
1121         */
1122        public int getSpanIndex() {
1123            return mSpanIndex;
1124        }
1125
1126        /**
1127         * Returns the number of spans occupied by this View. If the View not laid out yet, the
1128         * return value is <code>undefined</code>.
1129         *
1130         * @return The number of spans occupied by this View.
1131         */
1132        public int getSpanSize() {
1133            return mSpanSize;
1134        }
1135    }
1136
1137}
1138