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  */
16 package android.support.v7.widget;
17 
18 import android.content.Context;
19 import android.graphics.Rect;
20 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
21 import android.util.AttributeSet;
22 import android.util.Log;
23 import android.util.SparseIntArray;
24 import android.view.View;
25 import android.view.ViewGroup;
26 
27 import 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  */
35 public 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      * The measure spec for the scroll direction.
42      */
43     static final int MAIN_DIR_SPEC =
44             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
45 
46     int mSpanCount = DEFAULT_SPAN_COUNT;
47     /**
48      * The size of each span
49      */
50     int mSizePerSpan;
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      * Creates a vertical GridLayoutManager
63      *
64      * @param context Current context, will be used to access resources.
65      * @param spanCount The number of columns in the grid
66      */
GridLayoutManager(Context context, int spanCount)67     public GridLayoutManager(Context context, int spanCount) {
68         super(context);
69         setSpanCount(spanCount);
70     }
71 
72     /**
73      * @param context Current context, will be used to access resources.
74      * @param spanCount The number of columns or rows in the grid
75      * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
76      *                      #VERTICAL}.
77      * @param reverseLayout When set to true, layouts from end to start.
78      */
GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout)79     public GridLayoutManager(Context context, int spanCount, int orientation,
80             boolean reverseLayout) {
81         super(context, orientation, reverseLayout);
82         setSpanCount(spanCount);
83     }
84 
85     /**
86      * stackFromEnd is not supported by GridLayoutManager. Consider using
87      * {@link #setReverseLayout(boolean)}.
88      */
89     @Override
setStackFromEnd(boolean stackFromEnd)90     public void setStackFromEnd(boolean stackFromEnd) {
91         if (stackFromEnd) {
92             throw new UnsupportedOperationException(
93                     "GridLayoutManager does not support stack from end."
94                             + " Consider using reverse layout");
95         }
96         super.setStackFromEnd(false);
97     }
98 
99     @Override
getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)100     public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
101             RecyclerView.State state) {
102         if (mOrientation == HORIZONTAL) {
103             return mSpanCount;
104         }
105         if (state.getItemCount() < 1) {
106             return 0;
107         }
108         return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
109     }
110 
111     @Override
getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)112     public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
113             RecyclerView.State state) {
114         if (mOrientation == VERTICAL) {
115             return mSpanCount;
116         }
117         if (state.getItemCount() < 1) {
118             return 0;
119         }
120         return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
121     }
122 
123     @Override
onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)124     public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
125             RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
126         ViewGroup.LayoutParams lp = host.getLayoutParams();
127         if (!(lp instanceof LayoutParams)) {
128             super.onInitializeAccessibilityNodeInfoForItem(host, info);
129             return;
130         }
131         LayoutParams glp = (LayoutParams) lp;
132         int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition());
133         if (mOrientation == HORIZONTAL) {
134             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
135                     glp.getSpanIndex(), glp.getSpanSize(),
136                     spanGroupIndex, 1,
137                     mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
138         } else { // VERTICAL
139             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
140                     spanGroupIndex , 1,
141                     glp.getSpanIndex(), glp.getSpanSize(),
142                     mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
143         }
144     }
145 
146     @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)147     public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
148         if (state.isPreLayout()) {
149             cachePreLayoutSpanMapping();
150         }
151         super.onLayoutChildren(recycler, state);
152         if (DEBUG) {
153             validateChildOrder();
154         }
155         clearPreLayoutSpanMappingCache();
156     }
157 
clearPreLayoutSpanMappingCache()158     private void clearPreLayoutSpanMappingCache() {
159         mPreLayoutSpanSizeCache.clear();
160         mPreLayoutSpanIndexCache.clear();
161     }
162 
cachePreLayoutSpanMapping()163     private void cachePreLayoutSpanMapping() {
164         final int childCount = getChildCount();
165         for (int i = 0; i < childCount; i++) {
166             final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
167             final int viewPosition = lp.getViewLayoutPosition();
168             mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize());
169             mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex());
170         }
171     }
172 
173     @Override
onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)174     public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
175         mSpanSizeLookup.invalidateSpanIndexCache();
176     }
177 
178     @Override
onItemsChanged(RecyclerView recyclerView)179     public void onItemsChanged(RecyclerView recyclerView) {
180         mSpanSizeLookup.invalidateSpanIndexCache();
181     }
182 
183     @Override
onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)184     public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
185         mSpanSizeLookup.invalidateSpanIndexCache();
186     }
187 
188     @Override
onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount)189     public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
190         mSpanSizeLookup.invalidateSpanIndexCache();
191     }
192 
193     @Override
onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount)194     public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
195         mSpanSizeLookup.invalidateSpanIndexCache();
196     }
197 
198     @Override
generateDefaultLayoutParams()199     public RecyclerView.LayoutParams generateDefaultLayoutParams() {
200         return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
201                 ViewGroup.LayoutParams.WRAP_CONTENT);
202     }
203 
204     @Override
generateLayoutParams(Context c, AttributeSet attrs)205     public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
206         return new LayoutParams(c, attrs);
207     }
208 
209     @Override
generateLayoutParams(ViewGroup.LayoutParams lp)210     public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
211         if (lp instanceof ViewGroup.MarginLayoutParams) {
212             return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
213         } else {
214             return new LayoutParams(lp);
215         }
216     }
217 
218     @Override
checkLayoutParams(RecyclerView.LayoutParams lp)219     public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
220         return lp instanceof LayoutParams;
221     }
222 
223     /**
224      * Sets the source to get the number of spans occupied by each item in the adapter.
225      *
226      * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
227      *                       occupied by each item
228      */
setSpanSizeLookup(SpanSizeLookup spanSizeLookup)229     public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
230         mSpanSizeLookup = spanSizeLookup;
231     }
232 
233     /**
234      * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
235      *
236      * @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
237      */
getSpanSizeLookup()238     public SpanSizeLookup getSpanSizeLookup() {
239         return mSpanSizeLookup;
240     }
241 
updateMeasurements()242     private void updateMeasurements() {
243         int totalSpace;
244         if (getOrientation() == VERTICAL) {
245             totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
246         } else {
247             totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
248         }
249         mSizePerSpan = totalSpace / mSpanCount;
250     }
251 
252     @Override
onAnchorReady(RecyclerView.State state, AnchorInfo anchorInfo)253     void onAnchorReady(RecyclerView.State state, AnchorInfo anchorInfo) {
254         super.onAnchorReady(state, anchorInfo);
255         updateMeasurements();
256         if (state.getItemCount() > 0 && !state.isPreLayout()) {
257             ensureAnchorIsInFirstSpan(anchorInfo);
258         }
259         if (mSet == null || mSet.length != mSpanCount) {
260             mSet = new View[mSpanCount];
261         }
262     }
263 
ensureAnchorIsInFirstSpan(AnchorInfo anchorInfo)264     private void ensureAnchorIsInFirstSpan(AnchorInfo anchorInfo) {
265         int span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
266         while (span > 0 && anchorInfo.mPosition > 0) {
267             anchorInfo.mPosition--;
268             span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
269         }
270     }
271 
getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int viewPosition)272     private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state,
273             int viewPosition) {
274         if (!state.isPreLayout()) {
275             return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount);
276         }
277         final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition);
278         if (adapterPosition == -1) {
279             if (DEBUG) {
280                 throw new RuntimeException("Cannot find span group index for position "
281                         + viewPosition);
282             }
283             Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition);
284             return 0;
285         }
286         return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount);
287     }
288 
getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)289     private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
290         if (!state.isPreLayout()) {
291             return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount);
292         }
293         final int cached = mPreLayoutSpanIndexCache.get(pos, -1);
294         if (cached != -1) {
295             return cached;
296         }
297         final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
298         if (adapterPosition == -1) {
299             if (DEBUG) {
300                 throw new RuntimeException("Cannot find span index for pre layout position. It is"
301                         + " not cached, not in the adapter. Pos:" + pos);
302             }
303             Log.w(TAG, "Cannot find span size for pre layout position. It is"
304                     + " not cached, not in the adapter. Pos:" + pos);
305             return 0;
306         }
307         return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount);
308     }
309 
getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)310     private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
311         if (!state.isPreLayout()) {
312             return mSpanSizeLookup.getSpanSize(pos);
313         }
314         final int cached = mPreLayoutSpanSizeCache.get(pos, -1);
315         if (cached != -1) {
316             return cached;
317         }
318         final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
319         if (adapterPosition == -1) {
320             if (DEBUG) {
321                 throw new RuntimeException("Cannot find span size for pre layout position. It is"
322                         + " not cached, not in the adapter. Pos:" + pos);
323             }
324             Log.w(TAG, "Cannot find span size for pre layout position. It is"
325                     + " not cached, not in the adapter. Pos:" + pos);
326             return 1;
327         }
328         return mSpanSizeLookup.getSpanSize(adapterPosition);
329     }
330 
331     @Override
layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result)332     void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
333             LayoutState layoutState, LayoutChunkResult result) {
334         final boolean layingOutInPrimaryDirection =
335                 layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
336         int count = 0;
337         int consumedSpanCount = 0;
338         int remainingSpan = mSpanCount;
339         if (!layingOutInPrimaryDirection) {
340             int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
341             int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
342             remainingSpan = itemSpanIndex + itemSpanSize;
343         }
344         while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
345             int pos = layoutState.mCurrentPosition;
346             final int spanSize = getSpanSize(recycler, state, pos);
347             if (spanSize > mSpanCount) {
348                 throw new IllegalArgumentException("Item at position " + pos + " requires " +
349                         spanSize + " spans but GridLayoutManager has only " + mSpanCount
350                         + " spans.");
351             }
352             remainingSpan -= spanSize;
353             if (remainingSpan < 0) {
354                 break; // item did not fit into this row or column
355             }
356             View view = layoutState.next(recycler);
357             if (view == null) {
358                 break;
359             }
360             consumedSpanCount += spanSize;
361             mSet[count] = view;
362             count++;
363         }
364 
365         if (count == 0) {
366             result.mFinished = true;
367             return;
368         }
369 
370         int maxSize = 0;
371 
372         // we should assign spans before item decor offsets are calculated
373         assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
374         for (int i = 0; i < count; i++) {
375             View view = mSet[i];
376             if (layoutState.mScrapList == null) {
377                 if (layingOutInPrimaryDirection) {
378                     addView(view);
379                 } else {
380                     addView(view, 0);
381                 }
382             } else {
383                 if (layingOutInPrimaryDirection) {
384                     addDisappearingView(view);
385                 } else {
386                     addDisappearingView(view, 0);
387                 }
388             }
389 
390             int spanSize = getSpanSize(recycler, state, getPosition(view));
391             final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize,
392                     View.MeasureSpec.EXACTLY);
393             final LayoutParams lp = (LayoutParams) view.getLayoutParams();
394             if (mOrientation == VERTICAL) {
395                 measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height));
396             } else {
397                 measureChildWithDecorationsAndMargin(view, getMainDirSpec(lp.width), spec);
398             }
399             final int size = mOrientationHelper.getDecoratedMeasurement(view);
400             if (size > maxSize) {
401                 maxSize = size;
402             }
403         }
404 
405         // views that did not measure the maxSize has to be re-measured
406         final int maxMeasureSpec = getMainDirSpec(maxSize);
407         for (int i = 0; i < count; i ++) {
408             final View view = mSet[i];
409             if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
410                 int spanSize = getSpanSize(recycler, state, getPosition(view));
411                 final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize,
412                         View.MeasureSpec.EXACTLY);
413                 if (mOrientation == VERTICAL) {
414                     measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec);
415                 } else {
416                     measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec);
417                 }
418             }
419         }
420 
421         result.mConsumed = maxSize;
422 
423         int left = 0, right = 0, top = 0, bottom = 0;
424         if (mOrientation == VERTICAL) {
425             if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
426                 bottom = layoutState.mOffset;
427                 top = bottom - maxSize;
428             } else {
429                 top = layoutState.mOffset;
430                 bottom = top + maxSize;
431             }
432         } else {
433             if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
434                 right = layoutState.mOffset;
435                 left = right - maxSize;
436             } else {
437                 left = layoutState.mOffset;
438                 right = left + maxSize;
439             }
440         }
441         for (int i = 0; i < count; i++) {
442             View view = mSet[i];
443             LayoutParams params = (LayoutParams) view.getLayoutParams();
444             if (mOrientation == VERTICAL) {
445                 left = getPaddingLeft() + mSizePerSpan * params.mSpanIndex;
446                 right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
447             } else {
448                 top = getPaddingTop() + mSizePerSpan * params.mSpanIndex;
449                 bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
450             }
451             // We calculate everything with View's bounding box (which includes decor and margins)
452             // To calculate correct layout position, we subtract margins.
453             layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
454                     right - params.rightMargin, bottom - params.bottomMargin);
455             if (DEBUG) {
456                 Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
457                         + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
458                         + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
459                         + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
460             }
461             // Consume the available space if the view is not removed OR changed
462             if (params.isItemRemoved() || params.isItemChanged()) {
463                 result.mIgnoreConsumed = true;
464             }
465             result.mFocusable |= view.isFocusable();
466         }
467         Arrays.fill(mSet, null);
468     }
469 
getMainDirSpec(int dim)470     private int getMainDirSpec(int dim) {
471         if (dim < 0) {
472             return MAIN_DIR_SPEC;
473         } else {
474             return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY);
475         }
476     }
477 
measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec)478     private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
479         calculateItemDecorationsForChild(child, mDecorInsets);
480         RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
481         widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left,
482                 lp.rightMargin + mDecorInsets.right);
483         heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
484                 lp.bottomMargin + mDecorInsets.bottom);
485         child.measure(widthSpec, heightSpec);
486     }
487 
updateSpecWithExtra(int spec, int startInset, int endInset)488     private int updateSpecWithExtra(int spec, int startInset, int endInset) {
489         if (startInset == 0 && endInset == 0) {
490             return spec;
491         }
492         final int mode = View.MeasureSpec.getMode(spec);
493         if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
494             return View.MeasureSpec.makeMeasureSpec(
495                     View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
496         }
497         return spec;
498     }
499 
assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, int consumedSpanCount, boolean layingOutInPrimaryDirection)500     private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
501             int consumedSpanCount, boolean layingOutInPrimaryDirection) {
502         int span, spanDiff, start, end, diff;
503         // make sure we traverse from min position to max position
504         if (layingOutInPrimaryDirection) {
505             start = 0;
506             end = count;
507             diff = 1;
508         } else {
509             start = count - 1;
510             end = -1;
511             diff = -1;
512         }
513         if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span
514             span = consumedSpanCount - 1;
515             spanDiff = -1;
516         } else {
517             span = 0;
518             spanDiff = 1;
519         }
520         for (int i = start; i != end; i += diff) {
521             View view = mSet[i];
522             LayoutParams params = (LayoutParams) view.getLayoutParams();
523             params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
524             if (spanDiff == -1 && params.mSpanSize > 1) {
525                 params.mSpanIndex = span - (params.mSpanSize - 1);
526             } else {
527                 params.mSpanIndex = span;
528             }
529             span += spanDiff * params.mSpanSize;
530         }
531     }
532 
533     /**
534      * Returns the number of spans laid out by this grid.
535      *
536      * @return The number of spans
537      * @see #setSpanCount(int)
538      */
getSpanCount()539     public int getSpanCount() {
540         return mSpanCount;
541     }
542 
543     /**
544      * Sets the number of spans to be laid out.
545      * <p>
546      * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
547      * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
548      *
549      * @param spanCount The total number of spans in the grid
550      * @see #getSpanCount()
551      */
setSpanCount(int spanCount)552     public void setSpanCount(int spanCount) {
553         if (spanCount == mSpanCount) {
554             return;
555         }
556         if (spanCount < 1) {
557             throw new IllegalArgumentException("Span count should be at least 1. Provided "
558                     + spanCount);
559         }
560         mSpanCount = spanCount;
561         mSpanSizeLookup.invalidateSpanIndexCache();
562     }
563 
564     /**
565      * A helper class to provide the number of spans each item occupies.
566      * <p>
567      * Default implementation sets each item to occupy exactly 1 span.
568      *
569      * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
570      */
571     public static abstract class SpanSizeLookup {
572 
573         final SparseIntArray mSpanIndexCache = new SparseIntArray();
574 
575         private boolean mCacheSpanIndices = false;
576 
577         /**
578          * Returns the number of span occupied by the item at <code>position</code>.
579          *
580          * @param position The adapter position of the item
581          * @return The number of spans occupied by the item at the provided position
582          */
getSpanSize(int position)583         abstract public int getSpanSize(int position);
584 
585         /**
586          * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or
587          * not. By default these values are not cached. If you are not overriding
588          * {@link #getSpanIndex(int, int)}, you should set this to true for better performance.
589          *
590          * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not.
591          */
setSpanIndexCacheEnabled(boolean cacheSpanIndices)592         public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) {
593             mCacheSpanIndices = cacheSpanIndices;
594         }
595 
596         /**
597          * Clears the span index cache. GridLayoutManager automatically calls this method when
598          * adapter changes occur.
599          */
invalidateSpanIndexCache()600         public void invalidateSpanIndexCache() {
601             mSpanIndexCache.clear();
602         }
603 
604         /**
605          * Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not.
606          *
607          * @return True if results of {@link #getSpanIndex(int, int)} are cached.
608          */
isSpanIndexCacheEnabled()609         public boolean isSpanIndexCacheEnabled() {
610             return mCacheSpanIndices;
611         }
612 
getCachedSpanIndex(int position, int spanCount)613         int getCachedSpanIndex(int position, int spanCount) {
614             if (!mCacheSpanIndices) {
615                 return getSpanIndex(position, spanCount);
616             }
617             final int existing = mSpanIndexCache.get(position, -1);
618             if (existing != -1) {
619                 return existing;
620             }
621             final int value = getSpanIndex(position, spanCount);
622             mSpanIndexCache.put(position, value);
623             return value;
624         }
625 
626         /**
627          * Returns the final span index of the provided position.
628          * <p>
629          * If you have a faster way to calculate span index for your items, you should override
630          * this method. Otherwise, you should enable span index cache
631          * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is
632          * disabled, default implementation traverses all items from 0 to
633          * <code>position</code>. When caching is enabled, it calculates from the closest cached
634          * value before the <code>position</code>.
635          * <p>
636          * If you override this method, you need to make sure it is consistent with
637          * {@link #getSpanSize(int)}. GridLayoutManager does not call this method for
638          * each item. It is called only for the reference item and rest of the items
639          * are assigned to spans based on the reference item. For example, you cannot assign a
640          * position to span 2 while span 1 is empty.
641          * <p>
642          * Note that span offsets always start with 0 and are not affected by RTL.
643          *
644          * @param position  The position of the item
645          * @param spanCount The total number of spans in the grid
646          * @return The final span position of the item. Should be between 0 (inclusive) and
647          * <code>spanCount</code>(exclusive)
648          */
getSpanIndex(int position, int spanCount)649         public int getSpanIndex(int position, int spanCount) {
650             int positionSpanSize = getSpanSize(position);
651             if (positionSpanSize == spanCount) {
652                 return 0; // quick return for full-span items
653             }
654             int span = 0;
655             int startPos = 0;
656             // If caching is enabled, try to jump
657             if (mCacheSpanIndices && mSpanIndexCache.size() > 0) {
658                 int prevKey = findReferenceIndexFromCache(position);
659                 if (prevKey >= 0) {
660                     span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey);
661                     startPos = prevKey + 1;
662                 }
663             }
664             for (int i = startPos; i < position; i++) {
665                 int size = getSpanSize(i);
666                 span += size;
667                 if (span == spanCount) {
668                     span = 0;
669                 } else if (span > spanCount) {
670                     // did not fit, moving to next row / column
671                     span = size;
672                 }
673             }
674             if (span + positionSpanSize <= spanCount) {
675                 return span;
676             }
677             return 0;
678         }
679 
findReferenceIndexFromCache(int position)680         int findReferenceIndexFromCache(int position) {
681             int lo = 0;
682             int hi = mSpanIndexCache.size() - 1;
683 
684             while (lo <= hi) {
685                 final int mid = (lo + hi) >>> 1;
686                 final int midVal = mSpanIndexCache.keyAt(mid);
687                 if (midVal < position) {
688                     lo = mid + 1;
689                 } else {
690                     hi = mid - 1;
691                 }
692             }
693             int index = lo - 1;
694             if (index >= 0 && index < mSpanIndexCache.size()) {
695                 return mSpanIndexCache.keyAt(index);
696             }
697             return -1;
698         }
699 
700         /**
701          * Returns the index of the group this position belongs.
702          * <p>
703          * For example, if grid has 3 columns and each item occupies 1 span, span group index
704          * for item 1 will be 0, item 5 will be 1.
705          *
706          * @param adapterPosition The position in adapter
707          * @param spanCount The total number of spans in the grid
708          * @return The index of the span group including the item at the given adapter position
709          */
getSpanGroupIndex(int adapterPosition, int spanCount)710         public int getSpanGroupIndex(int adapterPosition, int spanCount) {
711             int span = 0;
712             int group = 0;
713             int positionSpanSize = getSpanSize(adapterPosition);
714             for (int i = 0; i < adapterPosition; i++) {
715                 int size = getSpanSize(i);
716                 span += size;
717                 if (span == spanCount) {
718                     span = 0;
719                     group++;
720                 } else if (span > spanCount) {
721                     // did not fit, moving to next row / column
722                     span = size;
723                     group++;
724                 }
725             }
726             if (span + positionSpanSize > spanCount) {
727                 group++;
728             }
729             return group;
730         }
731     }
732 
733     @Override
supportsPredictiveItemAnimations()734     public boolean supportsPredictiveItemAnimations() {
735         return mPendingSavedState == null;
736     }
737 
738     /**
739      * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
740      */
741     public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
742 
743         @Override
getSpanSize(int position)744         public int getSpanSize(int position) {
745             return 1;
746         }
747 
748         @Override
getSpanIndex(int position, int spanCount)749         public int getSpanIndex(int position, int spanCount) {
750             return position % spanCount;
751         }
752     }
753 
754     /**
755      * LayoutParams used by GridLayoutManager.
756      * <p>
757      * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the
758      * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is
759      * expected to fill all of the space given to it.
760      */
761     public static class LayoutParams extends RecyclerView.LayoutParams {
762 
763         /**
764          * Span Id for Views that are not laid out yet.
765          */
766         public static final int INVALID_SPAN_ID = -1;
767 
768         private int mSpanIndex = INVALID_SPAN_ID;
769 
770         private int mSpanSize = 0;
771 
LayoutParams(Context c, AttributeSet attrs)772         public LayoutParams(Context c, AttributeSet attrs) {
773             super(c, attrs);
774         }
775 
LayoutParams(int width, int height)776         public LayoutParams(int width, int height) {
777             super(width, height);
778         }
779 
LayoutParams(ViewGroup.MarginLayoutParams source)780         public LayoutParams(ViewGroup.MarginLayoutParams source) {
781             super(source);
782         }
783 
LayoutParams(ViewGroup.LayoutParams source)784         public LayoutParams(ViewGroup.LayoutParams source) {
785             super(source);
786         }
787 
LayoutParams(RecyclerView.LayoutParams source)788         public LayoutParams(RecyclerView.LayoutParams source) {
789             super(source);
790         }
791 
792         /**
793          * Returns the current span index of this View. If the View is not laid out yet, the return
794          * value is <code>undefined</code>.
795          * <p>
796          * Note that span index may change by whether the RecyclerView is RTL or not. For
797          * example, if the number of spans is 3 and layout is RTL, the rightmost item will have
798          * span index of 2. If the layout changes back to LTR, span index for this view will be 0.
799          * If the item was occupying 2 spans, span indices would be 1 and 0 respectively.
800          * <p>
801          * If the View occupies multiple spans, span with the minimum index is returned.
802          *
803          * @return The span index of the View.
804          */
getSpanIndex()805         public int getSpanIndex() {
806             return mSpanIndex;
807         }
808 
809         /**
810          * Returns the number of spans occupied by this View. If the View not laid out yet, the
811          * return value is <code>undefined</code>.
812          *
813          * @return The number of spans occupied by this View.
814          */
getSpanSize()815         public int getSpanSize() {
816             return mSpanSize;
817         }
818     }
819 
820 }
821