1 /*
2  * Copyright 2018 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 package androidx.recyclerview.widget;
17 
18 import android.content.Context;
19 import android.graphics.Rect;
20 import android.os.Build;
21 import android.os.Bundle;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.util.SparseIntArray;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.view.accessibility.AccessibilityEvent;
28 import android.view.accessibility.AccessibilityNodeInfo;
29 import android.widget.GridView;
30 
31 import androidx.annotation.RequiresApi;
32 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
33 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
34 
35 import org.jspecify.annotations.NonNull;
36 import org.jspecify.annotations.Nullable;
37 
38 import java.util.Arrays;
39 import java.util.Collections;
40 import java.util.HashSet;
41 import java.util.Map;
42 import java.util.Objects;
43 import java.util.Set;
44 import java.util.TreeMap;
45 
46 /**
47  * A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
48  * <p>
49  * By default, each item occupies 1 span. You can change it by providing a custom
50  * {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
51  */
52 public class GridLayoutManager extends LinearLayoutManager {
53 
54     private static final boolean DEBUG = false;
55     private static final String TAG = "GridLayoutManager";
56     public static final int DEFAULT_SPAN_COUNT = -1;
57     private static final int INVALID_POSITION = -1;
58 
59     private static final Set<Integer> sSupportedDirectionsForActionScrollInDirection =
60             Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
61                     View.FOCUS_LEFT,
62                     View.FOCUS_RIGHT,
63                     View.FOCUS_UP,
64                     View.FOCUS_DOWN)));
65 
66     /**
67      * Span size have been changed but we've not done a new layout calculation.
68      */
69     boolean mPendingSpanCountChange = false;
70     int mSpanCount = DEFAULT_SPAN_COUNT;
71     /**
72      * Right borders for each span.
73      * <p>For <b>i-th</b> item start is {@link #mCachedBorders}[i-1] + 1
74      * and end is {@link #mCachedBorders}[i].
75      */
76     int [] mCachedBorders;
77     /**
78      * Temporary array to keep views in layoutChunk method
79      */
80     View[] mSet;
81     final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray();
82     final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray();
83     SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
84     // re-used variable to acquire decor insets from RecyclerView
85     final Rect mDecorInsets = new Rect();
86 
87     private boolean mUsingSpansToEstimateScrollBarDimensions;
88 
89     /**
90      * Used to track the position of the target node brought on screen by
91      * {@code ACTIONS_SCROLL_IN_DIRECTION} so that a {@code TYPE_VIEW_TARGETED_BY_SCROLL} event can
92      * be emitted.
93      */
94     private int mPositionTargetedByScrollInDirection = INVALID_POSITION;
95 
96     /**
97      * Stores the index of the row with accessibility focus for use with
98      * {@link  AccessibilityNodeInfoCompat.AccessibilityActionCompat#ACTION_SCROLL_IN_DIRECTION}.
99      * This may include a position that is spanned by a grid child. For example, in the following
100      * grid...
101      * 0  3  4
102      * 1  3  5
103      * 2  3  6
104      * ...the child at adapter position 3 (which spans three rows) could have a row index of either
105      * 0, 1, or 2, and the choice may depend on which row of the grid previously had
106      * accessibility focus. Note that for single span cells, the row index stored here should be
107      * the same as the value returned by {@code getRowIndex()}.
108      */
109     int mRowWithAccessibilityFocus = INVALID_POSITION;
110 
111     /**
112      * Stores the index of the column with accessibility focus for use with
113      * {@link  AccessibilityNodeInfoCompat.AccessibilityActionCompat#ACTION_SCROLL_IN_DIRECTION}.
114      * This may include a position that is spanned by a grid child. For example, in the following
115      * grid...
116      * 0  1  2
117      * 3  3  3
118      * 4  5  6
119      * ... the child at adapter position 3 (which spans three columns) could have a column index
120      * of either 0, 1, or 2, and the choice may depend on which column of the grid previously had
121      * accessibility focus. Note that for single span cells, the column index stored here should be
122      * the same as the value returned by {@code getColumnIndex()}.
123      */
124     int mColumnWithAccessibilityFocus = INVALID_POSITION;
125 
126     /**
127      * Constructor used when layout manager is set in XML by RecyclerView attribute
128      * "layoutManager". If spanCount is not specified in the XML, it defaults to a
129      * single column.
130      *
131      * {@link androidx.recyclerview.R.attr#spanCount}
132      */
GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)133     public GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
134                              int defStyleRes) {
135         super(context, attrs, defStyleAttr, defStyleRes);
136         Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
137         setSpanCount(properties.spanCount);
138     }
139 
140     /**
141      * Creates a vertical GridLayoutManager
142      *
143      * @param context Current context, will be used to access resources.
144      * @param spanCount The number of columns in the grid
145      */
GridLayoutManager(Context context, int spanCount)146     public GridLayoutManager(Context context, int spanCount) {
147         super(context);
148         setSpanCount(spanCount);
149     }
150 
151     /**
152      * @param context Current context, will be used to access resources.
153      * @param spanCount The number of columns or rows in the grid
154      * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
155      *                      #VERTICAL}.
156      * @param reverseLayout When set to true, layouts from end to start.
157      */
GridLayoutManager(Context context, int spanCount, @RecyclerView.Orientation int orientation, boolean reverseLayout)158     public GridLayoutManager(Context context, int spanCount,
159             @RecyclerView.Orientation int orientation, boolean reverseLayout) {
160         super(context, orientation, reverseLayout);
161         setSpanCount(spanCount);
162     }
163 
164     /**
165      * stackFromEnd is not supported by GridLayoutManager. Consider using
166      * {@link #setReverseLayout(boolean)}.
167      */
168     @Override
setStackFromEnd(boolean stackFromEnd)169     public void setStackFromEnd(boolean stackFromEnd) {
170         if (stackFromEnd) {
171             throw new UnsupportedOperationException(
172                     "GridLayoutManager does not support stack from end."
173                             + " Consider using reverse layout");
174         }
175         super.setStackFromEnd(false);
176     }
177 
178     @Override
getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)179     public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
180             RecyclerView.State state) {
181         if (mOrientation == HORIZONTAL) {
182             return Math.min(mSpanCount, getItemCount());
183         }
184         if (state.getItemCount() < 1) {
185             return 0;
186         }
187 
188         // Row count is one more than the last item's row index.
189         return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1;
190     }
191 
192     @Override
getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)193     public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
194             RecyclerView.State state) {
195         if (mOrientation == VERTICAL) {
196             return Math.min(mSpanCount, getItemCount());
197         }
198         if (state.getItemCount() < 1) {
199             return 0;
200         }
201 
202         // Column count is one more than the last item's column index.
203         return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1;
204     }
205 
206     @Override
onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)207     public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
208             RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
209         ViewGroup.LayoutParams lp = host.getLayoutParams();
210         if (!(lp instanceof LayoutParams)) {
211             super.onInitializeAccessibilityNodeInfoForItem(host, info);
212             return;
213         }
214         LayoutParams glp = (LayoutParams) lp;
215         int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition());
216         if (mOrientation == HORIZONTAL) {
217             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
218                     glp.getSpanIndex(), glp.getSpanSize(),
219                     spanGroupIndex, 1, false, false));
220         } else { // VERTICAL
221             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
222                     spanGroupIndex , 1,
223                     glp.getSpanIndex(), glp.getSpanSize(), false, false));
224         }
225     }
226 
227     @Override
onInitializeAccessibilityNodeInfo(RecyclerView.@onNull Recycler recycler, RecyclerView.@NonNull State state, @NonNull AccessibilityNodeInfoCompat info)228     public void onInitializeAccessibilityNodeInfo(RecyclerView.@NonNull Recycler recycler,
229             RecyclerView.@NonNull State state, @NonNull AccessibilityNodeInfoCompat info) {
230         super.onInitializeAccessibilityNodeInfo(recycler, state, info);
231         // Set the class name so this is treated as a grid. A11y services should identify grids
232         // and list via CollectionInfos, but an almost empty grid may be incorrectly identified
233         // as a list.
234         info.setClassName(GridView.class.getName());
235 
236         if (mRecyclerView.mAdapter != null && mRecyclerView.mAdapter.getItemCount() > 1) {
237             info.addAction(AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION);
238         }
239     }
240 
241     @Override
performAccessibilityAction(int action, @Nullable Bundle args)242     boolean performAccessibilityAction(int action, @Nullable Bundle args) {
243         // TODO (267511848): when U constants are finalized:
244         //  - convert if/else blocks to switch statement
245         //  - remove SDK check
246         //  - remove the -1 check (this check makes accessibilityActionScrollInDirection
247         //  no-op for < 34; see action definition in AccessibilityNodeInfoCompat.java).
248         if (action == AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION.getId()
249                 && action != -1) {
250             final View viewWithAccessibilityFocus = findChildWithAccessibilityFocus();
251             if (viewWithAccessibilityFocus == null) {
252                 // TODO(b/268487724#comment2): handle rare cases when the requesting service does
253                 //  not place accessibility focus on a child. Consider scrolling forward/backward?
254                 return false;
255             }
256 
257             // Direction must be specified.
258             if (args == null) {
259                 return false;
260             }
261 
262             final int direction = args.getInt(
263                     AccessibilityNodeInfo.ACTION_ARGUMENT_DIRECTION_INT, INVALID_POSITION);
264 
265             if (!sSupportedDirectionsForActionScrollInDirection.contains(direction)) {
266                 if (DEBUG) {
267                     Log.w(TAG, "Direction equals " + direction
268                             + "which is unsupported when using ACTION_SCROLL_IN_DIRECTION");
269                 }
270                 return false;
271             }
272 
273             RecyclerView.ViewHolder vh =
274                     mRecyclerView.getChildViewHolder(viewWithAccessibilityFocus);
275             if (vh == null) {
276                 if (DEBUG) {
277                     throw new RuntimeException(
278                             "viewHolder is null for " + viewWithAccessibilityFocus);
279                 }
280                 return false;
281             }
282 
283             int startingAdapterPosition = vh.getAbsoluteAdapterPosition();
284             int startingRow = getRowIndex(startingAdapterPosition);
285             int startingColumn = getColumnIndex(startingAdapterPosition);
286 
287             if (startingRow < 0 || startingColumn < 0) {
288                 if (DEBUG) {
289                     throw new RuntimeException("startingRow equals " + startingRow + ", and "
290                             + "startingColumn equals " + startingColumn + ", and neither can be "
291                             + "less than 0.");
292                 }
293                 return false;
294             }
295 
296             if (hasAccessibilityFocusChanged(startingAdapterPosition)) {
297                 mRowWithAccessibilityFocus = startingRow;
298                 mColumnWithAccessibilityFocus = startingColumn;
299             }
300 
301             int scrollTargetPosition;
302 
303             int row = (mRowWithAccessibilityFocus == INVALID_POSITION) ? startingRow
304                     : mRowWithAccessibilityFocus;
305             int column = (mColumnWithAccessibilityFocus == INVALID_POSITION)
306                     ? startingColumn : mColumnWithAccessibilityFocus;
307 
308             switch (direction) {
309                 case View.FOCUS_LEFT:
310                     scrollTargetPosition = findScrollTargetPositionOnTheLeft(row, column,
311                             startingAdapterPosition);
312                     break;
313                 case View.FOCUS_RIGHT:
314                     scrollTargetPosition =
315                             findScrollTargetPositionOnTheRight(row, column,
316                                     startingAdapterPosition);
317                     break;
318                 case View.FOCUS_UP:
319                     scrollTargetPosition = findScrollTargetPositionAbove(row, column,
320                             startingAdapterPosition);
321                     break;
322                 case View.FOCUS_DOWN:
323                     scrollTargetPosition = findScrollTargetPositionBelow(row, column,
324                             startingAdapterPosition);
325                     break;
326                 default:
327                     return false;
328             }
329 
330             if (scrollTargetPosition == INVALID_POSITION
331                     && mOrientation == RecyclerView.HORIZONTAL) {
332                 // TODO (b/268487724): handle RTL.
333                 // Handle case in grids with horizontal orientation where the scroll target is on
334                 // a different row.
335                 if (direction == View.FOCUS_LEFT) {
336                     scrollTargetPosition = findPositionOfLastItemOnARowAboveForHorizontalGrid(
337                             startingRow);
338                 } else if (direction == View.FOCUS_RIGHT) {
339                     scrollTargetPosition = findPositionOfFirstItemOnARowBelowForHorizontalGrid(
340                             startingRow);
341                 }
342             }
343 
344             if (scrollTargetPosition != INVALID_POSITION) {
345                 scrollToPosition(scrollTargetPosition);
346                 mPositionTargetedByScrollInDirection = scrollTargetPosition;
347                 return true;
348             }
349 
350             return false;
351         } else if (action == android.R.id.accessibilityActionScrollToPosition) {
352             final int noRow = -1;
353             final int noColumn = -1;
354             if (args != null) {
355                 int rowArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT,
356                         noRow);
357                 int columnArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT,
358                         noColumn);
359 
360                 if (rowArg == noRow || columnArg == noColumn) {
361                     return false;
362                 }
363 
364                 int itemCount = mRecyclerView.mAdapter.getItemCount();
365 
366                 int position = -1;
367                 for (int i = 0; i < itemCount; i++) {
368                     // Corresponds to a column value if the orientation is VERTICAL and a row value
369                     // if the orientation is HORIZONTAL
370                     int spanIndex = getSpanIndex(mRecyclerView.mRecycler, mRecyclerView.mState, i);
371 
372                     // Corresponds to a row value if the orientation is VERTICAL and a column value
373                     // if the orientation is HORIZONTAL
374                     int spanGroupIndex = getSpanGroupIndex(mRecyclerView.mRecycler,
375                             mRecyclerView.mState, i);
376 
377                     if (mOrientation == VERTICAL) {
378                         if (spanIndex == columnArg && spanGroupIndex == rowArg) {
379                             position = i;
380                             break;
381                         }
382                     } else { // horizontal
383                         if (spanIndex == rowArg && spanGroupIndex == columnArg) {
384                             position = i;
385                             break;
386                         }
387                     }
388                 }
389 
390                 if (position > -1) {
391                     scrollToPositionWithOffset(position, 0);
392                     return true;
393                 }
394                 return false;
395             }
396         }
397         return super.performAccessibilityAction(action, args);
398     }
399 
findScrollTargetPositionOnTheRight(int startingRow, int startingColumn, int startingAdapterPosition)400     private int findScrollTargetPositionOnTheRight(int startingRow, int startingColumn,
401             int startingAdapterPosition) {
402         int scrollTargetPosition = INVALID_POSITION;
403         for (int i = startingAdapterPosition + 1; i < getItemCount(); i++) {
404             int currentRow = getRowIndex(i);
405             int currentColumn = getColumnIndex(i);
406 
407             if (currentRow < 0 || currentColumn < 0) {
408                 if (DEBUG) {
409                     throw new RuntimeException("currentRow equals " + currentRow + ", and "
410                             + "currentColumn equals " + currentColumn + ", and neither can be "
411                             + "less than 0.");
412                 }
413                 return INVALID_POSITION;
414             }
415 
416             if (mOrientation == VERTICAL) {
417                 /*
418                  * For grids with vertical orientation...
419                  * 1   2   3
420                  * 4   5   5
421                  * 6   7
422                  * ... the scroll target may lie on the same or a following row.
423                  */
424                 // TODO (b/268487724): handle RTL.
425                 if ((currentRow == startingRow && currentColumn > startingColumn)
426                         || (currentRow > startingRow)) {
427                     mRowWithAccessibilityFocus = currentRow;
428                     mColumnWithAccessibilityFocus = currentColumn;
429                     return i;
430                 }
431             } else { // HORIZONTAL
432                 /*
433                  * For grids with horizontal orientation, the scroll target may span multiple
434                  * rows. For example, in this grid...
435                  * 1   4   6
436                  * 2   5   7
437                  * 3   5   8
438                  * ... moving from 3 to 5 is considered staying on the "same row" because 5 spans
439                  *  multiple rows and the row indices for 5 include 3's row.
440                  */
441                 if (currentColumn > startingColumn && getRowIndices(i).contains(startingRow)) {
442                     // Note: mRowWithAccessibilityFocus not updated since the scroll target is on
443                     // the same row.
444                     mColumnWithAccessibilityFocus = currentColumn;
445                     return i;
446                 }
447             }
448         }
449 
450         return scrollTargetPosition;
451     }
452 
findScrollTargetPositionOnTheLeft(int startingRow, int startingColumn, int startingAdapterPosition)453     private int findScrollTargetPositionOnTheLeft(int startingRow, int startingColumn,
454             int startingAdapterPosition) {
455         int scrollTargetPosition = INVALID_POSITION;
456         for (int i = startingAdapterPosition - 1; i >= 0; i--) {
457             int currentRow = getRowIndex(i);
458             int currentColumn = getColumnIndex(i);
459 
460             if (currentRow < 0 || currentColumn < 0) {
461                 if (DEBUG) {
462                     throw new RuntimeException("currentRow equals " + currentRow + ", and "
463                             + "currentColumn equals " + currentColumn + ", and neither can be "
464                             + "less than 0.");
465                 }
466                 return INVALID_POSITION;
467             }
468 
469             if (mOrientation == VERTICAL) {
470                 /*
471                  * For grids with vertical orientation...
472                  * 1   2   3
473                  * 4   5   5
474                  * 6   7
475                  * ... the scroll target may lie on the same or a preceding row.
476                  */
477                 // TODO (b/268487724): handle RTL.
478                 if ((currentRow == startingRow && currentColumn < startingColumn)
479                         || (currentRow < startingRow)) {
480                     scrollTargetPosition = i;
481                     mRowWithAccessibilityFocus = currentRow;
482                     mColumnWithAccessibilityFocus = currentColumn;
483                     break;
484                 }
485             } else { // HORIZONTAL
486                 /*
487                  * For grids with horizontal orientation, the scroll target may span multiple
488                  * rows. For example, in this grid...
489                  * 1   4   6
490                  * 2   5   7
491                  * 3   5   8
492                  * ... moving from 8 to 5 or from 7 to 5 is considered staying on the "same row"
493                  * because the row indices for 5 include 8's and 7's row.
494                  */
495                 if (getRowIndices(i).contains(startingRow) && currentColumn < startingColumn) {
496                     // Note: mRowWithAccessibilityFocus not updated since the scroll target is on
497                     // the same row.
498                     mColumnWithAccessibilityFocus = currentColumn;
499                     return i;
500                 }
501             }
502         }
503         return scrollTargetPosition;
504     }
505 
findScrollTargetPositionAbove(int startingRow, int startingColumn, int startingAdapterPosition)506     private int findScrollTargetPositionAbove(int startingRow, int startingColumn,
507             int startingAdapterPosition) {
508         int scrollTargetPosition = INVALID_POSITION;
509         for (int i = startingAdapterPosition - 1; i >= 0; i--) {
510             int currentRow = getRowIndex(i);
511             int currentColumn = getColumnIndex(i);
512 
513             if (currentRow < 0 || currentColumn < 0) {
514                 if (DEBUG) {
515                     throw new RuntimeException("currentRow equals " + currentRow + ", and "
516                             + "currentColumn equals " + currentColumn + ", and neither can be "
517                             + "less than 0.");
518                 }
519                 return INVALID_POSITION;
520             }
521 
522             if (mOrientation == VERTICAL) {
523                 /*
524                  * The scroll target may span multiple columns. For example, in this grid...
525                  * 1   2   3
526                  * 4   4   5
527                  * 6   7
528                  * ... moving from 7 to 4 interprets as staying in second column, and moving from
529                  * 6 to 4 interprets as staying in the first column.
530                  */
531                 if (currentRow < startingRow && getColumnIndices(i).contains(startingColumn)) {
532                     scrollTargetPosition = i;
533                     mRowWithAccessibilityFocus = currentRow;
534                     // Note: mColumnWithAccessibilityFocus not updated since the scroll target is on
535                     // the same column.
536                     break;
537                 }
538             } else { // HORIZONTAL
539                 /*
540                  * The scroll target may span multiple rows. In this grid...
541                  * 1   4
542                  * 2   5
543                  * 2
544                  * 3
545                  * ... 2 spans two rows and moving up from 3 to 2 interprets moving to the third
546                  * row.
547                  */
548                 if (currentRow < startingRow && currentColumn == startingColumn) {
549                     Set<Integer> rowIndices = getRowIndices(i);
550                     scrollTargetPosition = i;
551                     mRowWithAccessibilityFocus = Collections.max(rowIndices);
552                     // Note: mColumnWithAccessibilityFocus not updated since the scroll target is on
553                     // the same column.
554                     break;
555                 }
556             }
557         }
558         return scrollTargetPosition;
559     }
560 
findScrollTargetPositionBelow(int startingRow, int startingColumn, int startingAdapterPosition)561     private int findScrollTargetPositionBelow(int startingRow, int startingColumn,
562             int startingAdapterPosition) {
563         int scrollTargetPosition = INVALID_POSITION;
564         for (int i = startingAdapterPosition + 1; i < getItemCount(); i++) {
565             int currentRow = getRowIndex(i);
566             int currentColumn = getColumnIndex(i);
567 
568             if (currentRow < 0 || currentColumn < 0) {
569                 if (DEBUG) {
570                     throw new RuntimeException("currentRow equals " + currentRow + ", and "
571                             + "currentColumn equals " + currentColumn + ", and neither can be "
572                             + "less than 0.");
573                 }
574                 return INVALID_POSITION;
575             }
576 
577             if (mOrientation == VERTICAL) {
578                 /*
579                  * The scroll target may span multiple columns. For example, in this grid...
580                  * 1   2   3
581                  * 4   4   5
582                  * 6   7
583                  * ... moving from 2 to 4 interprets as staying in second column, and moving from
584                  * 1 to 4 interprets as staying in the first column.
585                  */
586                 if ((currentRow > startingRow) && (currentColumn == startingColumn
587                         || getColumnIndices(i).contains(startingColumn))) {
588                     scrollTargetPosition = i;
589                     mRowWithAccessibilityFocus = currentRow;
590                     break;
591                 }
592             } else { // HORIZONTAL
593                 /*
594                  * The scroll target may span multiple rows. In this grid...
595                  * 1   4
596                  * 2   5
597                  * 2
598                  * 3
599                  * ... 2 spans two rows and moving down from 1 to 2 interprets moving to the second
600                  * row.
601                  */
602                 if (currentRow > startingRow && currentColumn == startingColumn) {
603                     scrollTargetPosition = i;
604                     mRowWithAccessibilityFocus = getRowIndex(i);
605                     break;
606                 }
607             }
608         }
609         return scrollTargetPosition;
610     }
611 
612     @SuppressWarnings("ConstantConditions") // For the spurious NPE warning related to getting a
613         // value from a map using one of the map keys.
findPositionOfLastItemOnARowAboveForHorizontalGrid(int startingRow)614     int findPositionOfLastItemOnARowAboveForHorizontalGrid(int startingRow) {
615         if (startingRow < 0) {
616             if (DEBUG) {
617                 throw new RuntimeException(
618                         "startingRow equals " + startingRow + ". It cannot be less than zero");
619             }
620             return INVALID_POSITION;
621         }
622 
623         if (mOrientation == VERTICAL) {
624             // This only handles cases of grids with horizontal orientation.
625             if (DEBUG) {
626                 Log.w(TAG, "You should not "
627                         + "use findPositionOfLastItemOnARowAboveForHorizontalGrid(...) with grids "
628                         + "with VERTICAL orientation");
629             }
630             return INVALID_POSITION;
631         }
632 
633         // Map where the keys are row numbers and values are the adapter positions of the last
634         // item in each row. This map is used to locate a scroll target on a previous row in grids
635         // with horizontal orientation. In this example...
636         // 1   4   7
637         // 2   5   8
638         // 3   6
639         // ... the generated map - {2 -> 5, 1 -> 7, 0 -> 6} - can be used to scroll from,
640         // say, "2" (adapter position 1) in the second row to "7" (adapter position 6) in the
641         // preceding row.
642         //
643         // Sometimes cells span multiple rows. In this example:
644         // 1   4   7
645         // 2   5   7
646         // 3   6   8
647         // ... the generated map - {0 -> 6, 1 -> 6, 2 -> 7} - can be used to scroll left from,
648         // say, "3" (adapter position 2) in the third row to "7" (adapter position 6) on the
649         // second row, and then to "5" (adapter position 4).
650         Map<Integer, Integer> rowToLastItemPositionMap = new TreeMap<>(Collections.reverseOrder());
651         for (int position = 0; position < getItemCount(); position++) {
652             Set<Integer> rows = getRowIndices(position);
653             for (int row: rows) {
654                 if (row < 0) {
655                     if (DEBUG) {
656                         throw new RuntimeException(
657                                 "row equals " + row + ". It cannot be less than zero");
658                     }
659                     return INVALID_POSITION;
660                 }
661                 rowToLastItemPositionMap.put(row, position);
662             }
663         }
664 
665         for (int row : rowToLastItemPositionMap.keySet()) {
666             if (row < startingRow) {
667                 int scrollTargetPosition = rowToLastItemPositionMap.get(row);
668                 mRowWithAccessibilityFocus = row;
669                 mColumnWithAccessibilityFocus = getColumnIndex(scrollTargetPosition);
670                 return scrollTargetPosition;
671             }
672         }
673         return INVALID_POSITION;
674     }
675 
676     @SuppressWarnings("ConstantConditions") // For the spurious NPE warning related to getting a
677         // value from a map using one of the map keys.
findPositionOfFirstItemOnARowBelowForHorizontalGrid(int startingRow)678     int findPositionOfFirstItemOnARowBelowForHorizontalGrid(int startingRow) {
679         if (startingRow < 0) {
680             if (DEBUG) {
681                 throw new RuntimeException(
682                         "startingRow equals " + startingRow + ". It cannot be less than zero");
683             }
684             return INVALID_POSITION;
685         }
686 
687         if (mOrientation == VERTICAL) {
688             // This only handles cases of grids with horizontal orientation.
689             if (DEBUG) {
690                 Log.w(TAG, "You should not "
691                         + "use findPositionOfFirstItemOnARowBelowForHorizontalGrid(...) with grids "
692                         + "with VERTICAL orientation");
693             }
694             return INVALID_POSITION;
695         }
696 
697         // Map where the keys are row numbers and values are the adapter positions of the first
698         // item in each row. This map is used to locate a scroll target on a following row in grids
699         // with horizontal orientation. In this example:
700         // 1   4   7
701         // 2   5   8
702         // 3   6
703         // ... the generated map - {0 -> 0, 1 -> 1, 2 -> 2} - can be used to scroll from, say,
704         // "7" (adapter position 6) in the first row to "2" (adapter position 1) in the next row.
705         // Sometimes cells span multiple rows. In this example:
706         // 1   3   6
707         // 1   4   7
708         // 2   5   8
709         // ... the generated map - {0 -> 0, 1 -> 0, 2 -> 1} - can be used to scroll right from,
710         // say, "6" (adapter position 5) in the first row to "1" (adapter position 0) on the
711         // second row, and then to "4" (adapter position 3).
712         Map<Integer, Integer> rowToFirstItemPositionMap = new TreeMap<>();
713         for (int position = 0; position < getItemCount(); position++) {
714             Set<Integer> rows = getRowIndices(position);
715             for (int row : rows) {
716                 if (row < 0) {
717                     if (DEBUG) {
718                         throw new RuntimeException(
719                                 "row equals " + row + ". It cannot be less than zero");
720                     }
721                     return INVALID_POSITION;
722                 }
723                 // We only care about the first item on each row.
724                 if (!rowToFirstItemPositionMap.containsKey(row)) {
725                     rowToFirstItemPositionMap.put(row, position);
726                 }
727             }
728         }
729 
730         for (int row : rowToFirstItemPositionMap.keySet()) {
731             if (row > startingRow) {
732                 int scrollTargetPosition = rowToFirstItemPositionMap.get(row);
733                 mRowWithAccessibilityFocus = row;
734                 mColumnWithAccessibilityFocus = 0;
735                 return scrollTargetPosition;
736             }
737         }
738         return INVALID_POSITION;
739     }
740 
741     /**
742      * Returns the row index associated with a position. If the item at this position spans multiple
743      * rows, it returns the first row index. To get all row indices for a position, use
744      * {@link #getRowIndices(int)}.
745      */
getRowIndex(int position)746     private int getRowIndex(int position) {
747         return mOrientation == VERTICAL ? getSpanGroupIndex(mRecyclerView.mRecycler,
748                 mRecyclerView.mState, position) : getSpanIndex(mRecyclerView.mRecycler,
749                 mRecyclerView.mState, position);
750     }
751 
752     /**
753      * Returns the column index associated with a position. If the item at this position spans
754      * multiple columns, it returns the first column index. To get all column indices, use
755      * {@link #getColumnIndices(int)}.
756      */
getColumnIndex(int position)757     private int getColumnIndex(int position) {
758         return mOrientation == HORIZONTAL ? getSpanGroupIndex(mRecyclerView.mRecycler,
759                 mRecyclerView.mState, position) : getSpanIndex(mRecyclerView.mRecycler,
760                 mRecyclerView.mState, position);
761     }
762 
763     /**
764      * Returns the row indices for a cell associated with {@code position}. For example, in this
765      * grid...
766      * 0   2   3
767      * 1   2   4
768      * ... the rows for the view at position 2 will be [0, 1] and the rows for position 3 will be
769      * [0].
770      */
getRowIndices(int position)771     private Set<Integer> getRowIndices(int position) {
772         return getRowOrColumnIndices(getRowIndex(position), position);
773     }
774 
775     /**
776      * Returns the column indices for a cell associated with {@code position}. For example, in this
777      * grid...
778      * 0   1
779      * 2   2
780      * 3   4
781      * ... the columns for the view at position 2 will be [0, 1] and the columns for position 3
782      * will be [0].
783      */
getColumnIndices(int position)784     private Set<Integer> getColumnIndices(int position) {
785         return getRowOrColumnIndices(getColumnIndex(position), position);
786     }
787 
getRowOrColumnIndices(int rowOrColumnIndex, int position)788     private Set<Integer> getRowOrColumnIndices(int rowOrColumnIndex, int position) {
789         Set<Integer> indices = new HashSet<>();
790         int spanSize = getSpanSize(mRecyclerView.mRecycler, mRecyclerView.mState, position);
791         for (int i = rowOrColumnIndex;  i <  rowOrColumnIndex + spanSize; i++) {
792             indices.add(i);
793         }
794         return indices;
795     }
796 
findChildWithAccessibilityFocus()797     private @Nullable View findChildWithAccessibilityFocus() {
798         View child = null;
799         // SDK check needed for View#isAccessibilityFocused()
800         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
801             boolean childFound = false;
802             int i;
803             for (i = 0; i < getChildCount(); i++) {
804                 if (Api21Impl.isAccessibilityFocused(Objects.requireNonNull(getChildAt(i)))) {
805                     childFound = true;
806                     break;
807                 }
808             }
809             if (childFound) {
810                 child = getChildAt(i);
811             }
812         }
813         return child;
814     }
815 
816     /**
817      * Returns true if the values stored in {@link #mRowWithAccessibilityFocus} and
818      * {@link #mColumnWithAccessibilityFocus} are not correct for the view at
819      * {@code adapterPosition}.
820      *
821      * Note that for cells that span multiple rows or multiple columns, {@link
822      * #mRowWithAccessibilityFocus} and {@link #mColumnWithAccessibilityFocus} can be set to more
823      * than one of several values. Accessibility focus is considered unchanged if any of the
824      * possible row values for a cell are the same as {@link #mRowWithAccessibilityFocus} and any
825      * of the possible column values are the same as {@link #mColumnWithAccessibilityFocus}.
826      */
hasAccessibilityFocusChanged(int adapterPosition)827     private boolean hasAccessibilityFocusChanged(int adapterPosition) {
828         return !getRowIndices(adapterPosition).contains(mRowWithAccessibilityFocus)
829                 || !getColumnIndices(adapterPosition).contains(mColumnWithAccessibilityFocus);
830     }
831 
832     @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)833     public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
834         if (state.isPreLayout()) {
835             cachePreLayoutSpanMapping();
836         }
837         super.onLayoutChildren(recycler, state);
838         if (DEBUG) {
839             validateChildOrder();
840         }
841         clearPreLayoutSpanMappingCache();
842     }
843 
844     @Override
onLayoutCompleted(RecyclerView.State state)845     public void onLayoutCompleted(RecyclerView.State state) {
846         super.onLayoutCompleted(state);
847         mPendingSpanCountChange = false;
848         if (mPositionTargetedByScrollInDirection != INVALID_POSITION) {
849             View viewTargetedByScrollInDirection = findViewByPosition(
850                     mPositionTargetedByScrollInDirection);
851             if (viewTargetedByScrollInDirection != null) {
852                 // Send event after the scroll associated with ACTION_SCROLL_IN_DIRECTION (see
853                 // performAccessibilityAction()) concludes and layout completes. Accessibility
854                 // services can listen for this event and change UI state as needed.
855                 viewTargetedByScrollInDirection.sendAccessibilityEvent(
856                         AccessibilityEvent.TYPE_VIEW_TARGETED_BY_SCROLL);
857                 mPositionTargetedByScrollInDirection = INVALID_POSITION;
858             }
859         }
860     }
861 
clearPreLayoutSpanMappingCache()862     private void clearPreLayoutSpanMappingCache() {
863         mPreLayoutSpanSizeCache.clear();
864         mPreLayoutSpanIndexCache.clear();
865     }
866 
cachePreLayoutSpanMapping()867     private void cachePreLayoutSpanMapping() {
868         final int childCount = getChildCount();
869         for (int i = 0; i < childCount; i++) {
870             final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
871             final int viewPosition = lp.getViewLayoutPosition();
872             mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize());
873             mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex());
874         }
875     }
876 
877     @Override
onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)878     public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
879         mSpanSizeLookup.invalidateSpanIndexCache();
880         mSpanSizeLookup.invalidateSpanGroupIndexCache();
881     }
882 
883     @Override
onItemsChanged(RecyclerView recyclerView)884     public void onItemsChanged(RecyclerView recyclerView) {
885         mSpanSizeLookup.invalidateSpanIndexCache();
886         mSpanSizeLookup.invalidateSpanGroupIndexCache();
887     }
888 
889     @Override
onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)890     public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
891         mSpanSizeLookup.invalidateSpanIndexCache();
892         mSpanSizeLookup.invalidateSpanGroupIndexCache();
893     }
894 
895     @Override
onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, Object payload)896     public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
897             Object payload) {
898         mSpanSizeLookup.invalidateSpanIndexCache();
899         mSpanSizeLookup.invalidateSpanGroupIndexCache();
900     }
901 
902     @Override
onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount)903     public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
904         mSpanSizeLookup.invalidateSpanIndexCache();
905         mSpanSizeLookup.invalidateSpanGroupIndexCache();
906     }
907 
908     @Override
generateDefaultLayoutParams()909     public RecyclerView.LayoutParams generateDefaultLayoutParams() {
910         if (mOrientation == HORIZONTAL) {
911             return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
912                     ViewGroup.LayoutParams.MATCH_PARENT);
913         } else {
914             return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
915                     ViewGroup.LayoutParams.WRAP_CONTENT);
916         }
917     }
918 
919     @Override
generateLayoutParams(Context c, AttributeSet attrs)920     public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
921         return new LayoutParams(c, attrs);
922     }
923 
924     @Override
generateLayoutParams(ViewGroup.LayoutParams lp)925     public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
926         if (lp instanceof ViewGroup.MarginLayoutParams) {
927             return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
928         } else {
929             return new LayoutParams(lp);
930         }
931     }
932 
933     @Override
checkLayoutParams(RecyclerView.LayoutParams lp)934     public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
935         return lp instanceof LayoutParams;
936     }
937 
938     /**
939      * Sets the source to get the number of spans occupied by each item in the adapter.
940      *
941      * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
942      *                       occupied by each item
943      */
setSpanSizeLookup(SpanSizeLookup spanSizeLookup)944     public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
945         mSpanSizeLookup = spanSizeLookup;
946     }
947 
948     /**
949      * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
950      *
951      * @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
952      */
getSpanSizeLookup()953     public SpanSizeLookup getSpanSizeLookup() {
954         return mSpanSizeLookup;
955     }
956 
updateMeasurements()957     private void updateMeasurements() {
958         int totalSpace;
959         if (getOrientation() == VERTICAL) {
960             totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
961         } else {
962             totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
963         }
964         calculateItemBorders(totalSpace);
965     }
966 
967     @Override
setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)968     public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
969         if (mCachedBorders == null) {
970             super.setMeasuredDimension(childrenBounds, wSpec, hSpec);
971         }
972         final int width, height;
973         final int horizontalPadding = getPaddingLeft() + getPaddingRight();
974         final int verticalPadding = getPaddingTop() + getPaddingBottom();
975         if (mOrientation == VERTICAL) {
976             final int usedHeight = childrenBounds.height() + verticalPadding;
977             height = chooseSize(hSpec, usedHeight, getMinimumHeight());
978             width = chooseSize(wSpec, mCachedBorders[mCachedBorders.length - 1] + horizontalPadding,
979                     getMinimumWidth());
980         } else {
981             final int usedWidth = childrenBounds.width() + horizontalPadding;
982             width = chooseSize(wSpec, usedWidth, getMinimumWidth());
983             height = chooseSize(hSpec, mCachedBorders[mCachedBorders.length - 1] + verticalPadding,
984                     getMinimumHeight());
985         }
986         setMeasuredDimension(width, height);
987     }
988 
989     /**
990      * @param totalSpace Total available space after padding is removed
991      */
calculateItemBorders(int totalSpace)992     private void calculateItemBorders(int totalSpace) {
993         mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace);
994     }
995 
996     /**
997      * @param cachedBorders The out array
998      * @param spanCount number of spans
999      * @param totalSpace total available space after padding is removed
1000      * @return The updated array. Might be the same instance as the provided array if its size
1001      * has not changed.
1002      */
calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace)1003     static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) {
1004         if (cachedBorders == null || cachedBorders.length != spanCount + 1
1005                 || cachedBorders[cachedBorders.length - 1] != totalSpace) {
1006             cachedBorders = new int[spanCount + 1];
1007         }
1008         cachedBorders[0] = 0;
1009         int sizePerSpan = totalSpace / spanCount;
1010         int sizePerSpanRemainder = totalSpace % spanCount;
1011         int consumedPixels = 0;
1012         int additionalSize = 0;
1013         for (int i = 1; i <= spanCount; i++) {
1014             int itemSize = sizePerSpan;
1015             additionalSize += sizePerSpanRemainder;
1016             if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) {
1017                 itemSize += 1;
1018                 additionalSize -= spanCount;
1019             }
1020             consumedPixels += itemSize;
1021             cachedBorders[i] = consumedPixels;
1022         }
1023         return cachedBorders;
1024     }
1025 
getSpaceForSpanRange(int startSpan, int spanSize)1026     int getSpaceForSpanRange(int startSpan, int spanSize) {
1027         if (mOrientation == VERTICAL && isLayoutRTL()) {
1028             return mCachedBorders[mSpanCount - startSpan]
1029                     - mCachedBorders[mSpanCount - startSpan - spanSize];
1030         } else {
1031             return mCachedBorders[startSpan + spanSize] - mCachedBorders[startSpan];
1032         }
1033     }
1034 
1035     @Override
onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection)1036     void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state,
1037                        AnchorInfo anchorInfo, int itemDirection) {
1038         super.onAnchorReady(recycler, state, anchorInfo, itemDirection);
1039         updateMeasurements();
1040         if (state.getItemCount() > 0 && !state.isPreLayout()) {
1041             ensureAnchorIsInCorrectSpan(recycler, state, anchorInfo, itemDirection);
1042         }
1043         ensureViewSet();
1044     }
1045 
ensureViewSet()1046     private void ensureViewSet() {
1047         if (mSet == null || mSet.length != mSpanCount) {
1048             mSet = new View[mSpanCount];
1049         }
1050     }
1051 
1052     @Override
scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)1053     public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
1054             RecyclerView.State state) {
1055         updateMeasurements();
1056         ensureViewSet();
1057         return super.scrollHorizontallyBy(dx, recycler, state);
1058     }
1059 
1060     @Override
scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)1061     public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
1062             RecyclerView.State state) {
1063         updateMeasurements();
1064         ensureViewSet();
1065         return super.scrollVerticallyBy(dy, recycler, state);
1066     }
1067 
ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection)1068     private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler,
1069             RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection) {
1070         final boolean layingOutInPrimaryDirection =
1071                 itemDirection == LayoutState.ITEM_DIRECTION_TAIL;
1072         int span = getSpanIndex(recycler, state, anchorInfo.mPosition);
1073         if (layingOutInPrimaryDirection) {
1074             // choose span 0
1075             while (span > 0 && anchorInfo.mPosition > 0) {
1076                 anchorInfo.mPosition--;
1077                 span = getSpanIndex(recycler, state, anchorInfo.mPosition);
1078             }
1079         } else {
1080             // choose the max span we can get. hopefully last one
1081             final int indexLimit = state.getItemCount() - 1;
1082             int pos = anchorInfo.mPosition;
1083             int bestSpan = span;
1084             while (pos < indexLimit) {
1085                 int next = getSpanIndex(recycler, state, pos + 1);
1086                 if (next > bestSpan) {
1087                     pos += 1;
1088                     bestSpan = next;
1089                 } else {
1090                     break;
1091                 }
1092             }
1093             anchorInfo.mPosition = pos;
1094         }
1095     }
1096 
1097     @Override
findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, boolean layoutFromEnd, boolean traverseChildrenInReverseOrder)1098     View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
1099             boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) {
1100 
1101         int start = 0;
1102         int end = getChildCount();
1103         int diff = 1;
1104         if (traverseChildrenInReverseOrder) {
1105             start = getChildCount() - 1;
1106             end = -1;
1107             diff = -1;
1108         }
1109 
1110         int itemCount = state.getItemCount();
1111 
1112         ensureLayoutState();
1113         View invalidMatch = null;
1114         View outOfBoundsMatch = null;
1115 
1116         final int boundsStart = mOrientationHelper.getStartAfterPadding();
1117         final int boundsEnd = mOrientationHelper.getEndAfterPadding();
1118 
1119         for (int i = start; i != end; i += diff) {
1120             final View view = getChildAt(i);
1121             final int position = getPosition(view);
1122             if (position >= 0 && position < itemCount) {
1123                 final int span = getSpanIndex(recycler, state, position);
1124                 if (span != 0) {
1125                     continue;
1126                 }
1127                 if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) {
1128                     if (invalidMatch == null) {
1129                         invalidMatch = view; // removed item, least preferred
1130                     }
1131                 } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd
1132                         || mOrientationHelper.getDecoratedEnd(view) < boundsStart) {
1133                     if (outOfBoundsMatch == null) {
1134                         outOfBoundsMatch = view; // item is not visible, less preferred
1135                     }
1136                 } else {
1137                     return view;
1138                 }
1139             }
1140         }
1141         return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch;
1142     }
1143 
getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int viewPosition)1144     private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state,
1145             int viewPosition) {
1146         if (!state.isPreLayout()) {
1147             return mSpanSizeLookup.getCachedSpanGroupIndex(viewPosition, mSpanCount);
1148         }
1149         final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition);
1150         if (adapterPosition == -1) {
1151             if (DEBUG) {
1152                 throw new RuntimeException("Cannot find span group index for position "
1153                         + viewPosition);
1154             }
1155             Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition);
1156             return 0;
1157         }
1158         return mSpanSizeLookup.getCachedSpanGroupIndex(adapterPosition, mSpanCount);
1159     }
1160 
getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)1161     private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
1162         if (!state.isPreLayout()) {
1163             return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount);
1164         }
1165         final int cached = mPreLayoutSpanIndexCache.get(pos, -1);
1166         if (cached != -1) {
1167             return cached;
1168         }
1169         final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
1170         if (adapterPosition == -1) {
1171             if (DEBUG) {
1172                 throw new RuntimeException("Cannot find span index for pre layout position. It is"
1173                         + " not cached, not in the adapter. Pos:" + pos);
1174             }
1175             Log.w(TAG, "Cannot find span size for pre layout position. It is"
1176                     + " not cached, not in the adapter. Pos:" + pos);
1177             return 0;
1178         }
1179         return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount);
1180     }
1181 
getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)1182     private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
1183         if (!state.isPreLayout()) {
1184             return mSpanSizeLookup.getSpanSize(pos);
1185         }
1186         final int cached = mPreLayoutSpanSizeCache.get(pos, -1);
1187         if (cached != -1) {
1188             return cached;
1189         }
1190         final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
1191         if (adapterPosition == -1) {
1192             if (DEBUG) {
1193                 throw new RuntimeException("Cannot find span size for pre layout position. It is"
1194                         + " not cached, not in the adapter. Pos:" + pos);
1195             }
1196             Log.w(TAG, "Cannot find span size for pre layout position. It is"
1197                     + " not cached, not in the adapter. Pos:" + pos);
1198             return 1;
1199         }
1200         return mSpanSizeLookup.getSpanSize(adapterPosition);
1201     }
1202 
1203     @Override
collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, LayoutPrefetchRegistry layoutPrefetchRegistry)1204     void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
1205             LayoutPrefetchRegistry layoutPrefetchRegistry) {
1206         int remainingSpan = mSpanCount;
1207         int count = 0;
1208         while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
1209             final int pos = layoutState.mCurrentPosition;
1210             layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
1211             final int spanSize = mSpanSizeLookup.getSpanSize(pos);
1212             remainingSpan -= spanSize;
1213             layoutState.mCurrentPosition += layoutState.mItemDirection;
1214             count++;
1215         }
1216     }
1217 
1218     @Override
layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result)1219     void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
1220             LayoutState layoutState, LayoutChunkResult result) {
1221         final int otherDirSpecMode = mOrientationHelper.getModeInOther();
1222         final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY;
1223         final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0;
1224         // if grid layout's dimensions are not specified, let the new row change the measurements
1225         // This is not perfect since we not covering all rows but still solves an important case
1226         // where they may have a header row which should be laid out according to children.
1227         if (flexibleInOtherDir) {
1228             updateMeasurements(); //  reset measurements
1229         }
1230         final boolean layingOutInPrimaryDirection =
1231                 layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
1232         int count = 0;
1233         int remainingSpan = mSpanCount;
1234         if (!layingOutInPrimaryDirection) {
1235             int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
1236             int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
1237             remainingSpan = itemSpanIndex + itemSpanSize;
1238         }
1239         while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
1240             int pos = layoutState.mCurrentPosition;
1241             final int spanSize = getSpanSize(recycler, state, pos);
1242             if (spanSize > mSpanCount) {
1243                 throw new IllegalArgumentException("Item at position " + pos + " requires "
1244                         + spanSize + " spans but GridLayoutManager has only " + mSpanCount
1245                         + " spans.");
1246             }
1247             remainingSpan -= spanSize;
1248             if (remainingSpan < 0) {
1249                 break; // item did not fit into this row or column
1250             }
1251             View view = layoutState.next(recycler);
1252             if (view == null) {
1253                 break;
1254             }
1255             mSet[count] = view;
1256             count++;
1257         }
1258 
1259         if (count == 0) {
1260             result.mFinished = true;
1261             return;
1262         }
1263 
1264         int maxSize = 0;
1265         float maxSizeInOther = 0; // use a float to get size per span
1266 
1267         // we should assign spans before item decor offsets are calculated
1268         assignSpans(recycler, state, count, layingOutInPrimaryDirection);
1269         for (int i = 0; i < count; i++) {
1270             View view = mSet[i];
1271             if (layoutState.mScrapList == null) {
1272                 if (layingOutInPrimaryDirection) {
1273                     addView(view);
1274                 } else {
1275                     addView(view, 0);
1276                 }
1277             } else {
1278                 if (layingOutInPrimaryDirection) {
1279                     addDisappearingView(view);
1280                 } else {
1281                     addDisappearingView(view, 0);
1282                 }
1283             }
1284             calculateItemDecorationsForChild(view, mDecorInsets);
1285 
1286             measureChild(view, otherDirSpecMode, false);
1287             final int size = mOrientationHelper.getDecoratedMeasurement(view);
1288             if (size > maxSize) {
1289                 maxSize = size;
1290             }
1291             final LayoutParams lp = (LayoutParams) view.getLayoutParams();
1292             final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view)
1293                     / lp.mSpanSize;
1294             if (otherSize > maxSizeInOther) {
1295                 maxSizeInOther = otherSize;
1296             }
1297         }
1298         if (flexibleInOtherDir) {
1299             // re-distribute columns
1300             guessMeasurement(maxSizeInOther, currentOtherDirSize);
1301             // now we should re-measure any item that was match parent.
1302             maxSize = 0;
1303             for (int i = 0; i < count; i++) {
1304                 View view = mSet[i];
1305                 measureChild(view, View.MeasureSpec.EXACTLY, true);
1306                 final int size = mOrientationHelper.getDecoratedMeasurement(view);
1307                 if (size > maxSize) {
1308                     maxSize = size;
1309                 }
1310             }
1311         }
1312 
1313         // Views that did not measure the maxSize has to be re-measured
1314         // We will stop doing this once we introduce Gravity in the GLM layout params
1315         for (int i = 0; i < count; i++) {
1316             final View view = mSet[i];
1317             if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
1318                 final LayoutParams lp = (LayoutParams) view.getLayoutParams();
1319                 final Rect decorInsets = lp.mDecorInsets;
1320                 final int verticalInsets = decorInsets.top + decorInsets.bottom
1321                         + lp.topMargin + lp.bottomMargin;
1322                 final int horizontalInsets = decorInsets.left + decorInsets.right
1323                         + lp.leftMargin + lp.rightMargin;
1324                 final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
1325                 final int wSpec;
1326                 final int hSpec;
1327                 if (mOrientation == VERTICAL) {
1328                     wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
1329                             horizontalInsets, lp.width, false);
1330                     hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets,
1331                             View.MeasureSpec.EXACTLY);
1332                 } else {
1333                     wSpec = View.MeasureSpec.makeMeasureSpec(maxSize - horizontalInsets,
1334                             View.MeasureSpec.EXACTLY);
1335                     hSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
1336                             verticalInsets, lp.height, false);
1337                 }
1338                 measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true);
1339             }
1340         }
1341 
1342         result.mConsumed = maxSize;
1343 
1344         int left = 0, right = 0, top = 0, bottom = 0;
1345         if (mOrientation == VERTICAL) {
1346             if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
1347                 bottom = layoutState.mOffset;
1348                 top = bottom - maxSize;
1349             } else {
1350                 top = layoutState.mOffset;
1351                 bottom = top + maxSize;
1352             }
1353         } else {
1354             if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
1355                 right = layoutState.mOffset;
1356                 left = right - maxSize;
1357             } else {
1358                 left = layoutState.mOffset;
1359                 right = left + maxSize;
1360             }
1361         }
1362         for (int i = 0; i < count; i++) {
1363             View view = mSet[i];
1364             LayoutParams params = (LayoutParams) view.getLayoutParams();
1365             if (mOrientation == VERTICAL) {
1366                 if (isLayoutRTL()) {
1367                     right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex];
1368                     left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
1369                 } else {
1370                     left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
1371                     right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
1372                 }
1373             } else {
1374                 top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
1375                 bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
1376             }
1377             // We calculate everything with View's bounding box (which includes decor and margins)
1378             // To calculate correct layout position, we subtract margins.
1379             layoutDecoratedWithMargins(view, left, top, right, bottom);
1380             if (DEBUG) {
1381                 Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
1382                         + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
1383                         + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
1384                         + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
1385             }
1386             // Consume the available space if the view is not removed OR changed
1387             if (params.isItemRemoved() || params.isItemChanged()) {
1388                 result.mIgnoreConsumed = true;
1389             }
1390             result.mFocusable |= view.hasFocusable();
1391         }
1392         Arrays.fill(mSet, null);
1393     }
1394 
1395     /**
1396      * Measures a child with currently known information. This is not necessarily the child's final
1397      * measurement. (see fillChunk for details).
1398      *
1399      * @param view The child view to be measured
1400      * @param otherDirParentSpecMode The RV measure spec that should be used in the secondary
1401      *                               orientation
1402      * @param alreadyMeasured True if we've already measured this view once
1403      */
measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured)1404     private void measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured) {
1405         final LayoutParams lp = (LayoutParams) view.getLayoutParams();
1406         final Rect decorInsets = lp.mDecorInsets;
1407         final int verticalInsets = decorInsets.top + decorInsets.bottom
1408                 + lp.topMargin + lp.bottomMargin;
1409         final int horizontalInsets = decorInsets.left + decorInsets.right
1410                 + lp.leftMargin + lp.rightMargin;
1411         final int availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
1412         final int wSpec;
1413         final int hSpec;
1414         if (mOrientation == VERTICAL) {
1415             wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
1416                     horizontalInsets, lp.width, false);
1417             hSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(),
1418                     verticalInsets, lp.height, true);
1419         } else {
1420             hSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
1421                     verticalInsets, lp.height, false);
1422             wSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getWidthMode(),
1423                     horizontalInsets, lp.width, true);
1424         }
1425         measureChildWithDecorationsAndMargin(view, wSpec, hSpec, alreadyMeasured);
1426     }
1427 
1428     /**
1429      * This is called after laying out a row (if vertical) or a column (if horizontal) when the
1430      * RecyclerView does not have exact measurement specs.
1431      * <p>
1432      * Here we try to assign a best guess width or height and re-do the layout to update other
1433      * views that wanted to MATCH_PARENT in the non-scroll orientation.
1434      *
1435      * @param maxSizeInOther The maximum size per span ratio from the measurement of the children.
1436      * @param currentOtherDirSize The size before this layout chunk. There is no reason to go below.
1437      */
guessMeasurement(float maxSizeInOther, int currentOtherDirSize)1438     private void guessMeasurement(float maxSizeInOther, int currentOtherDirSize) {
1439         final int contentSize = Math.round(maxSizeInOther * mSpanCount);
1440         // always re-calculate because borders were stretched during the fill
1441         calculateItemBorders(Math.max(contentSize, currentOtherDirSize));
1442     }
1443 
measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, boolean alreadyMeasured)1444     private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec,
1445             boolean alreadyMeasured) {
1446         RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
1447         final boolean measure;
1448         if (alreadyMeasured) {
1449             measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp);
1450         } else {
1451             measure = shouldMeasureChild(child, widthSpec, heightSpec, lp);
1452         }
1453         if (measure) {
1454             child.measure(widthSpec, heightSpec);
1455         }
1456     }
1457 
assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, boolean layingOutInPrimaryDirection)1458     private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
1459             boolean layingOutInPrimaryDirection) {
1460         // spans are always assigned from 0 to N no matter if it is RTL or not.
1461         // RTL is used only when positioning the view.
1462         int span, start, end, diff;
1463         // make sure we traverse from min position to max position
1464         if (layingOutInPrimaryDirection) {
1465             start = 0;
1466             end = count;
1467             diff = 1;
1468         } else {
1469             start = count - 1;
1470             end = -1;
1471             diff = -1;
1472         }
1473         span = 0;
1474         for (int i = start; i != end; i += diff) {
1475             View view = mSet[i];
1476             LayoutParams params = (LayoutParams) view.getLayoutParams();
1477             params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
1478             params.mSpanIndex = span;
1479             span += params.mSpanSize;
1480         }
1481     }
1482 
1483     /**
1484      * Returns the number of spans laid out by this grid.
1485      *
1486      * @return The number of spans
1487      * @see #setSpanCount(int)
1488      */
getSpanCount()1489     public int getSpanCount() {
1490         return mSpanCount;
1491     }
1492 
1493     /**
1494      * Sets the number of spans to be laid out.
1495      * <p>
1496      * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
1497      * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
1498      *
1499      * @param spanCount The total number of spans in the grid
1500      * @see #getSpanCount()
1501      */
setSpanCount(int spanCount)1502     public void setSpanCount(int spanCount) {
1503         if (spanCount == mSpanCount) {
1504             return;
1505         }
1506         mPendingSpanCountChange = true;
1507         if (spanCount < 1) {
1508             throw new IllegalArgumentException("Span count should be at least 1. Provided "
1509                     + spanCount);
1510         }
1511         mSpanCount = spanCount;
1512         mSpanSizeLookup.invalidateSpanIndexCache();
1513         requestLayout();
1514     }
1515 
1516     /**
1517      * A helper class to provide the number of spans each item occupies.
1518      * <p>
1519      * Default implementation sets each item to occupy exactly 1 span.
1520      *
1521      * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
1522      */
1523     public abstract static class SpanSizeLookup {
1524 
1525         final SparseIntArray mSpanIndexCache = new SparseIntArray();
1526         final SparseIntArray mSpanGroupIndexCache = new SparseIntArray();
1527 
1528         private boolean mCacheSpanIndices = false;
1529         private boolean mCacheSpanGroupIndices = false;
1530 
1531         /**
1532          * Returns the number of span occupied by the item at <code>position</code>.
1533          *
1534          * @param position The adapter position of the item
1535          * @return The number of spans occupied by the item at the provided position
1536          */
getSpanSize(int position)1537         public abstract int getSpanSize(int position);
1538 
1539         /**
1540          * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or
1541          * not. By default these values are not cached. If you are not overriding
1542          * {@link #getSpanIndex(int, int)} with something highly performant, you should set this
1543          * to true for better performance.
1544          *
1545          * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not.
1546          */
setSpanIndexCacheEnabled(boolean cacheSpanIndices)1547         public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) {
1548             if (!cacheSpanIndices) {
1549                 mSpanGroupIndexCache.clear();
1550             }
1551             mCacheSpanIndices = cacheSpanIndices;
1552         }
1553 
1554         /**
1555          * Sets whether the results of {@link #getSpanGroupIndex(int, int)} method should be cached
1556          * or not. By default these values are not cached. If you are not overriding
1557          * {@link #getSpanGroupIndex(int, int)} with something highly performant, and you are using
1558          * spans to calculate scrollbar offset and range, you should set this to true for better
1559          * performance.
1560          *
1561          * @param cacheSpanGroupIndices Whether results of getGroupSpanIndex should be cached or
1562          *                              not.
1563          */
setSpanGroupIndexCacheEnabled(boolean cacheSpanGroupIndices)1564         public void setSpanGroupIndexCacheEnabled(boolean cacheSpanGroupIndices)  {
1565             if (!cacheSpanGroupIndices) {
1566                 mSpanGroupIndexCache.clear();
1567             }
1568             mCacheSpanGroupIndices = cacheSpanGroupIndices;
1569         }
1570 
1571         /**
1572          * Clears the span index cache. GridLayoutManager automatically calls this method when
1573          * adapter changes occur.
1574          */
invalidateSpanIndexCache()1575         public void invalidateSpanIndexCache() {
1576             mSpanIndexCache.clear();
1577         }
1578 
1579         /**
1580          * Clears the span group index cache. GridLayoutManager automatically calls this method
1581          * when adapter changes occur.
1582          */
invalidateSpanGroupIndexCache()1583         public void invalidateSpanGroupIndexCache() {
1584             mSpanGroupIndexCache.clear();
1585         }
1586 
1587         /**
1588          * Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not.
1589          *
1590          * @return True if results of {@link #getSpanIndex(int, int)} are cached.
1591          */
isSpanIndexCacheEnabled()1592         public boolean isSpanIndexCacheEnabled() {
1593             return mCacheSpanIndices;
1594         }
1595 
1596         /**
1597          * Returns whether results of {@link #getSpanGroupIndex(int, int)} method are cached or not.
1598          *
1599          * @return True if results of {@link #getSpanGroupIndex(int, int)} are cached.
1600          */
isSpanGroupIndexCacheEnabled()1601         public boolean isSpanGroupIndexCacheEnabled() {
1602             return mCacheSpanGroupIndices;
1603         }
1604 
getCachedSpanIndex(int position, int spanCount)1605         int getCachedSpanIndex(int position, int spanCount) {
1606             if (!mCacheSpanIndices) {
1607                 return getSpanIndex(position, spanCount);
1608             }
1609             final int existing = mSpanIndexCache.get(position, -1);
1610             if (existing != -1) {
1611                 return existing;
1612             }
1613             final int value = getSpanIndex(position, spanCount);
1614             mSpanIndexCache.put(position, value);
1615             return value;
1616         }
1617 
getCachedSpanGroupIndex(int position, int spanCount)1618         int getCachedSpanGroupIndex(int position, int spanCount) {
1619             if (!mCacheSpanGroupIndices) {
1620                 return getSpanGroupIndex(position, spanCount);
1621             }
1622             final int existing = mSpanGroupIndexCache.get(position, -1);
1623             if (existing != -1) {
1624                 return existing;
1625             }
1626             final int value = getSpanGroupIndex(position, spanCount);
1627             mSpanGroupIndexCache.put(position, value);
1628             return value;
1629         }
1630 
1631         /**
1632          * Returns the final span index of the provided position.
1633          * <p>
1634          * If {@link #getOrientation()} is {@link #VERTICAL}, this is a column value.
1635          * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is a row value.
1636          * <p>
1637          * If you have a faster way to calculate span index for your items, you should override
1638          * this method. Otherwise, you should enable span index cache
1639          * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is
1640          * disabled, default implementation traverses all items from 0 to
1641          * <code>position</code>. When caching is enabled, it calculates from the closest cached
1642          * value before the <code>position</code>.
1643          * <p>
1644          * If you override this method, you need to make sure it is consistent with
1645          * {@link #getSpanSize(int)}. GridLayoutManager does not call this method for
1646          * each item. It is called only for the reference item and rest of the items
1647          * are assigned to spans based on the reference item. For example, you cannot assign a
1648          * position to span 2 while span 1 is empty.
1649          * <p>
1650          * Note that span offsets always start with 0 and are not affected by RTL.
1651          *
1652          * @param position  The position of the item
1653          * @param spanCount The total number of spans in the grid
1654          * @return The final span position of the item. Should be between 0 (inclusive) and
1655          * <code>spanCount</code>(exclusive)
1656          */
getSpanIndex(int position, int spanCount)1657         public int getSpanIndex(int position, int spanCount) {
1658             int positionSpanSize = getSpanSize(position);
1659             if (positionSpanSize == spanCount) {
1660                 return 0; // quick return for full-span items
1661             }
1662             int span = 0;
1663             int startPos = 0;
1664             // If caching is enabled, try to jump
1665             if (mCacheSpanIndices) {
1666                 int prevKey = findFirstKeyLessThan(mSpanIndexCache, position);
1667                 if (prevKey >= 0) {
1668                     span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey);
1669                     startPos = prevKey + 1;
1670                 }
1671             }
1672             for (int i = startPos; i < position; i++) {
1673                 int size = getSpanSize(i);
1674                 span += size;
1675                 if (span == spanCount) {
1676                     span = 0;
1677                 } else if (span > spanCount) {
1678                     // did not fit, moving to next row / column
1679                     span = size;
1680                 }
1681             }
1682             if (span + positionSpanSize <= spanCount) {
1683                 return span;
1684             }
1685             return 0;
1686         }
1687 
findFirstKeyLessThan(SparseIntArray cache, int position)1688         static int findFirstKeyLessThan(SparseIntArray cache, int position) {
1689             int lo = 0;
1690             int hi = cache.size() - 1;
1691 
1692             while (lo <= hi) {
1693                 // Using unsigned shift here to divide by two because it is guaranteed to not
1694                 // overflow.
1695                 final int mid = (lo + hi) >>> 1;
1696                 final int midVal = cache.keyAt(mid);
1697                 if (midVal < position) {
1698                     lo = mid + 1;
1699                 } else {
1700                     hi = mid - 1;
1701                 }
1702             }
1703             int index = lo - 1;
1704             if (index >= 0 && index < cache.size()) {
1705                 return cache.keyAt(index);
1706             }
1707             return -1;
1708         }
1709 
1710         /**
1711          * Returns the index of the group this position belongs.
1712          * <p>
1713          * If {@link #getOrientation()} is {@link #VERTICAL}, this is a row value.
1714          * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is a column value.
1715          * <p>
1716          * For example, if grid has 3 columns and each item occupies 1 span, span group index
1717          * for item 1 will be 0, item 5 will be 1.
1718          *
1719          * @param adapterPosition The position in adapter
1720          * @param spanCount The total number of spans in the grid
1721          * @return The index of the span group including the item at the given adapter position
1722          */
getSpanGroupIndex(int adapterPosition, int spanCount)1723         public int getSpanGroupIndex(int adapterPosition, int spanCount) {
1724             int span = 0;
1725             int group = 0;
1726             int start = 0;
1727             if (mCacheSpanGroupIndices) {
1728                 // This finds the first non empty cached group cache key.
1729                 int prevKey = findFirstKeyLessThan(mSpanGroupIndexCache, adapterPosition);
1730                 if (prevKey != -1) {
1731                     group = mSpanGroupIndexCache.get(prevKey);
1732                     start = prevKey + 1;
1733                     span = getCachedSpanIndex(prevKey, spanCount) + getSpanSize(prevKey);
1734                     if (span == spanCount) {
1735                         span = 0;
1736                         group++;
1737                     }
1738                 }
1739             }
1740             int positionSpanSize = getSpanSize(adapterPosition);
1741             for (int i = start; i < adapterPosition; i++) {
1742                 int size = getSpanSize(i);
1743                 span += size;
1744                 if (span == spanCount) {
1745                     span = 0;
1746                     group++;
1747                 } else if (span > spanCount) {
1748                     // did not fit, moving to next row / column
1749                     span = size;
1750                     group++;
1751                 }
1752             }
1753             if (span + positionSpanSize > spanCount) {
1754                 group++;
1755             }
1756             return group;
1757         }
1758     }
1759 
1760     @Override
onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state)1761     public View onFocusSearchFailed(View focused, int direction,
1762             RecyclerView.Recycler recycler, RecyclerView.State state) {
1763         View prevFocusedChild = findContainingItemView(focused);
1764         if (prevFocusedChild == null) {
1765             return null;
1766         }
1767         LayoutParams lp = (LayoutParams) prevFocusedChild.getLayoutParams();
1768         final int prevSpanStart = lp.mSpanIndex;
1769         final int prevSpanEnd = lp.mSpanIndex + lp.mSpanSize;
1770         View view = super.onFocusSearchFailed(focused, direction, recycler, state);
1771         if (view == null) {
1772             return null;
1773         }
1774         // LinearLayoutManager finds the last child. What we want is the child which has the same
1775         // spanIndex.
1776         final int layoutDir = convertFocusDirectionToLayoutDirection(direction);
1777         final boolean ascend = (layoutDir == LayoutState.LAYOUT_END) != mShouldReverseLayout;
1778         final int start, inc, limit;
1779         if (ascend) {
1780             start = getChildCount() - 1;
1781             inc = -1;
1782             limit = -1;
1783         } else {
1784             start = 0;
1785             inc = 1;
1786             limit = getChildCount();
1787         }
1788         final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL();
1789 
1790         // The focusable candidate to be picked if no perfect focusable candidate is found.
1791         // The best focusable candidate is the one with the highest amount of span overlap with
1792         // the currently focused view.
1793         View focusableWeakCandidate = null; // somewhat matches but not strong
1794         int focusableWeakCandidateSpanIndex = -1;
1795         int focusableWeakCandidateOverlap = 0; // how many spans overlap
1796 
1797         // The unfocusable candidate to become visible on the screen next, if no perfect or
1798         // weak focusable candidates are found to receive focus next.
1799         // We are only interested in partially visible unfocusable views. These are views that are
1800         // not fully visible, that is either partially overlapping, or out-of-bounds and right below
1801         // or above RV's padded bounded area. The best unfocusable candidate is the one with the
1802         // highest amount of span overlap with the currently focused view.
1803         View unfocusableWeakCandidate = null; // somewhat matches but not strong
1804         int unfocusableWeakCandidateSpanIndex = -1;
1805         int unfocusableWeakCandidateOverlap = 0; // how many spans overlap
1806 
1807         // The span group index of the start child. This indicates the span group index of the
1808         // next focusable item to receive focus, if a focusable item within the same span group
1809         // exists. Any focusable item beyond this group index are not relevant since they
1810         // were already stored in the layout before onFocusSearchFailed call and were not picked
1811         // by the focusSearch algorithm.
1812         int focusableSpanGroupIndex = getSpanGroupIndex(recycler, state, start);
1813         for (int i = start; i != limit; i += inc) {
1814             int spanGroupIndex = getSpanGroupIndex(recycler, state, i);
1815             View candidate = getChildAt(i);
1816             if (candidate == prevFocusedChild) {
1817                 break;
1818             }
1819 
1820             if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) {
1821                 // We are past the allowable span group index for the next focusable item.
1822                 // The search only continues if no focusable weak candidates have been found up
1823                 // until this point, in order to find the best unfocusable candidate to become
1824                 // visible on the screen next.
1825                 if (focusableWeakCandidate != null) {
1826                     break;
1827                 }
1828                 continue;
1829             }
1830 
1831             final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams();
1832             final int candidateStart = candidateLp.mSpanIndex;
1833             final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize;
1834             if (candidate.hasFocusable() && candidateStart == prevSpanStart
1835                     && candidateEnd == prevSpanEnd) {
1836                 return candidate; // perfect match
1837             }
1838             boolean assignAsWeek = false;
1839             if ((candidate.hasFocusable() && focusableWeakCandidate == null)
1840                     || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) {
1841                 assignAsWeek = true;
1842             } else {
1843                 int maxStart = Math.max(candidateStart, prevSpanStart);
1844                 int minEnd = Math.min(candidateEnd, prevSpanEnd);
1845                 int overlap = minEnd - maxStart;
1846                 if (candidate.hasFocusable()) {
1847                     if (overlap > focusableWeakCandidateOverlap) {
1848                         assignAsWeek = true;
1849                     } else if (overlap == focusableWeakCandidateOverlap
1850                             && preferLastSpan == (candidateStart
1851                             > focusableWeakCandidateSpanIndex)) {
1852                         assignAsWeek = true;
1853                     }
1854                 } else if (focusableWeakCandidate == null
1855                         && isViewPartiallyVisible(candidate, false, true)) {
1856                     if (overlap > unfocusableWeakCandidateOverlap) {
1857                         assignAsWeek = true;
1858                     } else if (overlap == unfocusableWeakCandidateOverlap
1859                             && preferLastSpan == (candidateStart
1860                                     > unfocusableWeakCandidateSpanIndex)) {
1861                         assignAsWeek = true;
1862                     }
1863                 }
1864             }
1865 
1866             if (assignAsWeek) {
1867                 if (candidate.hasFocusable()) {
1868                     focusableWeakCandidate = candidate;
1869                     focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex;
1870                     focusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd)
1871                             - Math.max(candidateStart, prevSpanStart);
1872                 } else {
1873                     unfocusableWeakCandidate = candidate;
1874                     unfocusableWeakCandidateSpanIndex = candidateLp.mSpanIndex;
1875                     unfocusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd)
1876                             - Math.max(candidateStart, prevSpanStart);
1877                 }
1878             }
1879         }
1880         return (focusableWeakCandidate != null) ? focusableWeakCandidate : unfocusableWeakCandidate;
1881     }
1882 
1883     @Override
supportsPredictiveItemAnimations()1884     public boolean supportsPredictiveItemAnimations() {
1885         return mPendingSavedState == null && !mPendingSpanCountChange;
1886     }
1887 
1888     @Override
computeHorizontalScrollRange(RecyclerView.State state)1889     public int computeHorizontalScrollRange(RecyclerView.State state) {
1890         if (mUsingSpansToEstimateScrollBarDimensions) {
1891             return computeScrollRangeWithSpanInfo(state);
1892         } else {
1893             return super.computeHorizontalScrollRange(state);
1894         }
1895     }
1896 
1897     @Override
computeVerticalScrollRange(RecyclerView.State state)1898     public int computeVerticalScrollRange(RecyclerView.State state) {
1899         if (mUsingSpansToEstimateScrollBarDimensions) {
1900             return computeScrollRangeWithSpanInfo(state);
1901         } else {
1902             return super.computeVerticalScrollRange(state);
1903         }
1904     }
1905 
1906     @Override
computeHorizontalScrollOffset(RecyclerView.State state)1907     public int computeHorizontalScrollOffset(RecyclerView.State state) {
1908         if (mUsingSpansToEstimateScrollBarDimensions) {
1909             return computeScrollOffsetWithSpanInfo(state);
1910         } else {
1911             return super.computeHorizontalScrollOffset(state);
1912         }
1913     }
1914 
1915     @Override
computeVerticalScrollOffset(RecyclerView.State state)1916     public int computeVerticalScrollOffset(RecyclerView.State state) {
1917         if (mUsingSpansToEstimateScrollBarDimensions) {
1918             return computeScrollOffsetWithSpanInfo(state);
1919         } else {
1920             return super.computeVerticalScrollOffset(state);
1921         }
1922     }
1923 
1924     /**
1925      * When this flag is set, the scroll offset and scroll range calculations will take account
1926      * of span information.
1927      *
1928      * <p>This is will increase the accuracy of the scroll bar's size and offset but will require
1929      * more calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)}".
1930      *
1931      * <p>This additional accuracy may or may not be needed, depending on the characteristics of
1932      * your layout.  You will likely benefit from this accuracy when:
1933      *
1934      * <ul>
1935      *   <li>The variation in item span sizes is large.
1936      *   <li>The size of your data set is small (if your data set is large, the scrollbar will
1937      *   likely be very small anyway, and thus the increased accuracy has less impact).
1938      *   <li>Calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast.
1939      * </ul>
1940      *
1941      * <p>If you decide to enable this feature, you should be sure that calls to
1942      * {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast, that set span group index
1943      * caching is set to true via a call to
1944      * {@link SpanSizeLookup#setSpanGroupIndexCacheEnabled(boolean),
1945      * and span index caching is also enabled via a call to
1946      * {@link SpanSizeLookup#setSpanIndexCacheEnabled(boolean)}}.
1947      */
setUsingSpansToEstimateScrollbarDimensions( boolean useSpansToEstimateScrollBarDimensions)1948     public void setUsingSpansToEstimateScrollbarDimensions(
1949             boolean useSpansToEstimateScrollBarDimensions) {
1950         mUsingSpansToEstimateScrollBarDimensions = useSpansToEstimateScrollBarDimensions;
1951     }
1952 
1953     /**
1954      * Returns true if the scroll offset and scroll range calculations take account of span
1955      * information. See {@link #setUsingSpansToEstimateScrollbarDimensions(boolean)} for more
1956      * information on this topic. Defaults to {@code false}.
1957      *
1958      * @return true if the scroll offset and scroll range calculations take account of span
1959      * information.
1960      */
isUsingSpansToEstimateScrollbarDimensions()1961     public boolean isUsingSpansToEstimateScrollbarDimensions() {
1962         return mUsingSpansToEstimateScrollBarDimensions;
1963     }
1964 
computeScrollRangeWithSpanInfo(RecyclerView.State state)1965     private int computeScrollRangeWithSpanInfo(RecyclerView.State state) {
1966         if (getChildCount() == 0 || state.getItemCount() == 0) {
1967             return 0;
1968         }
1969         ensureLayoutState();
1970 
1971         View startChild = findFirstVisibleChildClosestToStart(!isSmoothScrollbarEnabled(), true);
1972         View endChild = findFirstVisibleChildClosestToEnd(!isSmoothScrollbarEnabled(), true);
1973 
1974         if (startChild == null || endChild == null) {
1975             return 0;
1976         }
1977         if (!isSmoothScrollbarEnabled()) {
1978             return mSpanSizeLookup.getCachedSpanGroupIndex(
1979                     state.getItemCount() - 1, mSpanCount) + 1;
1980         }
1981 
1982         // smooth scrollbar enabled. try to estimate better.
1983         final int laidOutArea = mOrientationHelper.getDecoratedEnd(endChild)
1984                 - mOrientationHelper.getDecoratedStart(startChild);
1985 
1986         final int firstVisibleSpan =
1987                 mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount);
1988         final int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild),
1989                 mSpanCount);
1990         final int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1,
1991                 mSpanCount) + 1;
1992         final int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1;
1993 
1994         // estimate a size for full list.
1995         return (int) (((float) laidOutArea / laidOutSpans) * totalSpans);
1996     }
1997 
computeScrollOffsetWithSpanInfo(RecyclerView.State state)1998     private int computeScrollOffsetWithSpanInfo(RecyclerView.State state) {
1999         if (getChildCount() == 0 || state.getItemCount() == 0) {
2000             return 0;
2001         }
2002         ensureLayoutState();
2003 
2004         boolean smoothScrollEnabled = isSmoothScrollbarEnabled();
2005         View startChild = findFirstVisibleChildClosestToStart(!smoothScrollEnabled, true);
2006         View endChild = findFirstVisibleChildClosestToEnd(!smoothScrollEnabled, true);
2007         if (startChild == null || endChild == null) {
2008             return 0;
2009         }
2010         int startChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild),
2011                 mSpanCount);
2012         int endChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild),
2013                 mSpanCount);
2014 
2015         final int minSpan = Math.min(startChildSpan, endChildSpan);
2016         final int maxSpan = Math.max(startChildSpan, endChildSpan);
2017         final int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1,
2018                 mSpanCount) + 1;
2019 
2020         final int spansBefore = mShouldReverseLayout
2021                 ? Math.max(0, totalSpans - maxSpan - 1)
2022                 : Math.max(0, minSpan);
2023         if (!smoothScrollEnabled) {
2024             return spansBefore;
2025         }
2026         final int laidOutArea = Math.abs(mOrientationHelper.getDecoratedEnd(endChild)
2027                 - mOrientationHelper.getDecoratedStart(startChild));
2028 
2029         final int firstVisibleSpan =
2030                 mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount);
2031         final int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild),
2032                 mSpanCount);
2033         final int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1;
2034         final float avgSizePerSpan = (float) laidOutArea / laidOutSpans;
2035 
2036         return Math.round(spansBefore * avgSizePerSpan + (mOrientationHelper.getStartAfterPadding()
2037             - mOrientationHelper.getDecoratedStart(startChild)));
2038     }
2039 
2040     /**
2041      * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
2042      */
2043     public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
2044 
2045         @Override
getSpanSize(int position)2046         public int getSpanSize(int position) {
2047             return 1;
2048         }
2049 
2050         @Override
getSpanIndex(int position, int spanCount)2051         public int getSpanIndex(int position, int spanCount) {
2052             return position % spanCount;
2053         }
2054     }
2055 
2056     /**
2057      * LayoutParams used by GridLayoutManager.
2058      * <p>
2059      * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the
2060      * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is
2061      * expected to fill all of the space given to it.
2062      */
2063     public static class LayoutParams extends RecyclerView.LayoutParams {
2064 
2065         /**
2066          * Span Id for Views that are not laid out yet.
2067          */
2068         public static final int INVALID_SPAN_ID = -1;
2069 
2070         int mSpanIndex = INVALID_SPAN_ID;
2071 
2072         int mSpanSize = 0;
2073 
LayoutParams(Context c, AttributeSet attrs)2074         public LayoutParams(Context c, AttributeSet attrs) {
2075             super(c, attrs);
2076         }
2077 
LayoutParams(int width, int height)2078         public LayoutParams(int width, int height) {
2079             super(width, height);
2080         }
2081 
LayoutParams(ViewGroup.MarginLayoutParams source)2082         public LayoutParams(ViewGroup.MarginLayoutParams source) {
2083             super(source);
2084         }
2085 
LayoutParams(ViewGroup.LayoutParams source)2086         public LayoutParams(ViewGroup.LayoutParams source) {
2087             super(source);
2088         }
2089 
LayoutParams(RecyclerView.LayoutParams source)2090         public LayoutParams(RecyclerView.LayoutParams source) {
2091             super(source);
2092         }
2093 
2094         /**
2095          * Returns the current span index of this View. If the View is not laid out yet, the return
2096          * value is <code>undefined</code>.
2097          * <p>
2098          * Starting with RecyclerView <b>24.2.0</b>, span indices are always indexed from position 0
2099          * even if the layout is RTL. In a vertical GridLayoutManager, <b>leftmost</b> span is span
2100          * 0 if the layout is <b>LTR</b> and <b>rightmost</b> span is span 0 if the layout is
2101          * <b>RTL</b>. Prior to 24.2.0, it was the opposite which was conflicting with
2102          * {@link SpanSizeLookup#getSpanIndex(int, int)}.
2103          * <p>
2104          * If the View occupies multiple spans, span with the minimum index is returned.
2105          *
2106          * @return The span index of the View.
2107          */
getSpanIndex()2108         public int getSpanIndex() {
2109             return mSpanIndex;
2110         }
2111 
2112         /**
2113          * Returns the number of spans occupied by this View. If the View not laid out yet, the
2114          * return value is <code>undefined</code>.
2115          *
2116          * @return The number of spans occupied by this View.
2117          */
getSpanSize()2118         public int getSpanSize() {
2119             return mSpanSize;
2120         }
2121     }
2122 
2123 
2124     @RequiresApi(21)
2125     private static class Api21Impl {
Api21Impl()2126         private Api21Impl() {
2127             // This class is not instantiable.
2128         }
2129 
isAccessibilityFocused(@onNull View view)2130         static boolean isAccessibilityFocused(@NonNull View view) {
2131             return view.isAccessibilityFocused();
2132         }
2133     }
2134 }