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