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      * 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      */
GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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      */
GridLayoutManager(Context context, int spanCount)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      */
GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout)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
setStackFromEnd(boolean stackFromEnd)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
getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)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
getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)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
onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)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
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)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
onLayoutCompleted(RecyclerView.State state)178     public void onLayoutCompleted(RecyclerView.State state) {
179         super.onLayoutCompleted(state);
180         mPendingSpanCountChange = false;
181     }
182 
clearPreLayoutSpanMappingCache()183     private void clearPreLayoutSpanMappingCache() {
184         mPreLayoutSpanSizeCache.clear();
185         mPreLayoutSpanIndexCache.clear();
186     }
187 
cachePreLayoutSpanMapping()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
onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)199     public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
200         mSpanSizeLookup.invalidateSpanIndexCache();
201     }
202 
203     @Override
onItemsChanged(RecyclerView recyclerView)204     public void onItemsChanged(RecyclerView recyclerView) {
205         mSpanSizeLookup.invalidateSpanIndexCache();
206     }
207 
208     @Override
onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)209     public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
210         mSpanSizeLookup.invalidateSpanIndexCache();
211     }
212 
213     @Override
onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, Object payload)214     public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
215             Object payload) {
216         mSpanSizeLookup.invalidateSpanIndexCache();
217     }
218 
219     @Override
onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount)220     public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
221         mSpanSizeLookup.invalidateSpanIndexCache();
222     }
223 
224     @Override
generateDefaultLayoutParams()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
generateLayoutParams(Context c, AttributeSet attrs)236     public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
237         return new LayoutParams(c, attrs);
238     }
239 
240     @Override
generateLayoutParams(ViewGroup.LayoutParams lp)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
checkLayoutParams(RecyclerView.LayoutParams lp)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      */
setSpanSizeLookup(SpanSizeLookup spanSizeLookup)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      */
getSpanSizeLookup()269     public SpanSizeLookup getSpanSizeLookup() {
270         return mSpanSizeLookup;
271     }
272 
updateMeasurements()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
setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)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      */
calculateItemBorders(int totalSpace)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      */
calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace)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
onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection)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 
ensureViewSet()353     private void ensureViewSet() {
354         if (mSet == null || mSet.length != mSpanCount) {
355             mSet = new View[mSpanCount];
356         }
357     }
358 
359     @Override
scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)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
scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)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 
ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection)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
findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, int start, int end, int itemCount)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 
getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int viewPosition)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 
getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)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 
getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)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
layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result)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      */
guessMeasurement(float maxSizeInOther, int currentOtherDirSize)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 
measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, boolean capBothSpecs, boolean alreadyMeasured)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 
updateSpecWithExtra(int spec, int startInset, int endInset)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 
assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, int consumedSpanCount, boolean layingOutInPrimaryDirection)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      */
getSpanCount()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      */
setSpanCount(int spanCount)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          */
getSpanSize(int position)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          */
setSpanIndexCacheEnabled(boolean cacheSpanIndices)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          */
invalidateSpanIndexCache()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          */
isSpanIndexCacheEnabled()854         public boolean isSpanIndexCacheEnabled() {
855             return mCacheSpanIndices;
856         }
857 
getCachedSpanIndex(int position, int spanCount)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          */
getSpanIndex(int position, int spanCount)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 
findReferenceIndexFromCache(int position)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          */
getSpanGroupIndex(int adapterPosition, int spanCount)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
onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state)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
supportsPredictiveItemAnimations()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
getSpanSize(int position)1061         public int getSpanSize(int position) {
1062             return 1;
1063         }
1064 
1065         @Override
getSpanIndex(int position, int spanCount)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 
LayoutParams(Context c, AttributeSet attrs)1089         public LayoutParams(Context c, AttributeSet attrs) {
1090             super(c, attrs);
1091         }
1092 
LayoutParams(int width, int height)1093         public LayoutParams(int width, int height) {
1094             super(width, height);
1095         }
1096 
LayoutParams(ViewGroup.MarginLayoutParams source)1097         public LayoutParams(ViewGroup.MarginLayoutParams source) {
1098             super(source);
1099         }
1100 
LayoutParams(ViewGroup.LayoutParams source)1101         public LayoutParams(ViewGroup.LayoutParams source) {
1102             super(source);
1103         }
1104 
LayoutParams(RecyclerView.LayoutParams source)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          */
getSpanIndex()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          */
getSpanSize()1132         public int getSpanSize() {
1133             return mSpanSize;
1134         }
1135     }
1136 
1137 }
1138