1 /*
2  * Copyright 2021 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.leanback.widget;
17 
18 import android.util.SparseIntArray;
19 
20 import androidx.collection.CircularIntArray;
21 import androidx.recyclerview.widget.RecyclerView;
22 
23 import org.jspecify.annotations.NonNull;
24 import org.jspecify.annotations.Nullable;
25 
26 import java.io.PrintWriter;
27 import java.util.Arrays;
28 
29 /**
30  * A grid is representation of single or multiple rows layout data structure and algorithm.
31  * Grid is the base class for single row, non-staggered grid and staggered grid.
32  * <p>
33  * To use the Grid, user must implement a Provider to create or remove visible item.
34  * Grid maintains a list of visible items. Visible items are created when
35  * user calls appendVisibleItems() or prependVisibleItems() with certain limitation
36  * (e.g. a max edge that append up to). Visible items are deleted when user calls
37  * removeInvisibleItemsAtEnd() or removeInvisibleItemsAtFront(). Grid's algorithm
38  * uses size of visible item returned from Provider.createItem() to decide which row
39  * to add a new visible item and may cache the algorithm results. User must call
40  * invalidateItemsAfter() when it detects item size changed to ask Grid to remove cached
41  * results.
42  */
43 abstract class Grid {
44 
45     /**
46      * A constant representing a default starting index, indicating that the
47      * developer did not provide a start index.
48      */
49     static final int START_DEFAULT = -1;
50 
51     Object[] mTmpItem = new Object[1];
52 
53     /**
54      * When user uses Grid,  he should provide count of items and
55      * the method to create item and remove item.
56      */
57     interface Provider {
58 
59         /**
60          * Return how many items (are in the adapter).
61          */
getCount()62         int getCount();
63 
64         /**
65          * @return Min index to prepend, usually it's 0; but in the preLayout case,
66          * when grid was showing 5,6,7.  Removing 3,4,5 will make the layoutPosition to
67          * be 3(deleted),4,5 in prelayout pass; Grid's index is still 5,6,7 in prelayout.
68          * When we prepend in prelayout, we can call createItem(4), createItem(3), createItem(2),
69          * the minimal index is 2, which is also the delta to mapping to layoutPosition in
70          * prelayout pass.
71          */
getMinIndex()72         int getMinIndex();
73 
74         /**
75          * Create visible item and where the provider should measure it.
76          * The call is always followed by addItem().
77          *
78          * @param index            0-based index of the item in provider
79          * @param append           True if new item is after last visible item, false if new item is
80          *                         before first visible item.
81          * @param item             item[0] returns created item that will be passed in addItem()
82          *                         call.
83          * @param disappearingItem The item is a disappearing item added by
84          *                         {@link Grid#fillDisappearingItems(int[], int, SparseIntArray)}.
85          * @return length of the item.
86          */
createItem(int index, boolean append, Object[] item, boolean disappearingItem)87         int createItem(int index, boolean append, Object[] item, boolean disappearingItem);
88 
89         /**
90          * add item to given row and given edge.  The call is always after createItem().
91          *
92          * @param item     The object returned by createItem()
93          * @param index    0-based index of the item in provider
94          * @param length   The size of the object
95          * @param rowIndex Row index to put the item
96          * @param edge     min_edge if not reversed or max_edge if reversed.
97          */
addItem(Object item, int index, int length, int rowIndex, int edge)98         void addItem(Object item, int index, int length, int rowIndex, int edge);
99 
100         /**
101          * Remove visible item at index.
102          *
103          * @param index 0-based index of the item in provider
104          */
removeItem(int index)105         void removeItem(int index);
106 
107         /**
108          * Get edge of an existing visible item. edge will be the min_edge
109          * if not reversed or the max_edge if reversed.
110          *
111          * @param index 0-based index of the item in provider
112          */
getEdge(int index)113         int getEdge(int index);
114 
115         /**
116          * Get size of an existing visible item.
117          *
118          * @param index 0-based index of the item in provider
119          */
getSize(int index)120         int getSize(int index);
121     }
122 
123     /**
124      * Cached representation of an item in Grid.  May be subclassed.
125      */
126     static class Location {
127         /**
128          * The index of the row for this Location.
129          */
130         int mRow;
131 
Location(int row)132         Location(int row) {
133             this.mRow = row;
134         }
135     }
136 
137     protected Provider mProvider;
138     protected boolean mReversedFlow;
139     protected int mSpacing;
140     protected int mNumRows;
141     protected int mFirstVisibleIndex = -1;
142     protected int mLastVisibleIndex = -1;
143 
144     protected CircularIntArray[] mTmpItemPositionsInRows;
145 
146     // the first index that grid will layout
147     protected int mStartIndex = START_DEFAULT;
148 
149     /**
150      * Creates a single or multiple rows (can be staggered or not staggered) grid
151      */
createGrid(int rows)152     public static Grid createGrid(int rows) {
153         Grid grid;
154         if (rows == 1) {
155             grid = new SingleRow();
156         } else {
157             // TODO support non staggered multiple rows grid
158             grid = new StaggeredGridDefault();
159             grid.setNumRows(rows);
160         }
161         return grid;
162     }
163 
164     /**
165      * Sets the space between items in a row
166      */
setSpacing(int spacing)167     public final void setSpacing(int spacing) {
168         mSpacing = spacing;
169     }
170 
171     /**
172      * Sets if reversed flow (rtl)
173      */
setReversedFlow(boolean reversedFlow)174     public final void setReversedFlow(boolean reversedFlow) {
175         mReversedFlow = reversedFlow;
176     }
177 
178     /**
179      * Returns true if reversed flow (rtl)
180      */
isReversedFlow()181     public boolean isReversedFlow() {
182         return mReversedFlow;
183     }
184 
185     /**
186      * Sets the {@link Provider} for this grid.
187      *
188      * @param provider The provider for this grid.
189      */
setProvider(Provider provider)190     public void setProvider(Provider provider) {
191         mProvider = provider;
192     }
193 
194     /**
195      * Sets the first item index to create when there are no items.
196      *
197      * @param startIndex the index of the first item
198      */
setStart(int startIndex)199     public void setStart(int startIndex) {
200         mStartIndex = startIndex;
201     }
202 
203     /**
204      * Returns the number of rows in the grid.
205      */
getNumRows()206     public int getNumRows() {
207         return mNumRows;
208     }
209 
210     /**
211      * Sets number of rows to fill into. For views that represent a
212      * horizontal list, this will be the rows of the view. For views that
213      * represent a vertical list, this will be the columns.
214      *
215      * @param numRows numberOfRows
216      */
setNumRows(int numRows)217     void setNumRows(int numRows) {
218         if (numRows <= 0) {
219             throw new IllegalArgumentException();
220         }
221         if (mNumRows == numRows) {
222             return;
223         }
224         mNumRows = numRows;
225         mTmpItemPositionsInRows = new CircularIntArray[mNumRows];
226         for (int i = 0; i < mNumRows; i++) {
227             mTmpItemPositionsInRows[i] = new CircularIntArray();
228         }
229     }
230 
231     /**
232      * Returns index of first visible item in the staggered grid.  Returns negative value
233      * if no visible item.
234      */
getFirstVisibleIndex()235     public final int getFirstVisibleIndex() {
236         return mFirstVisibleIndex;
237     }
238 
239     /**
240      * Returns index of last visible item in the staggered grid.  Returns negative value
241      * if no visible item.
242      */
getLastVisibleIndex()243     public final int getLastVisibleIndex() {
244         return mLastVisibleIndex;
245     }
246 
247     /**
248      * Reset visible indices and keep cache (if exists)
249      */
resetVisibleIndex()250     public void resetVisibleIndex() {
251         mFirstVisibleIndex = mLastVisibleIndex = -1;
252     }
253 
254     /**
255      * Invalidate items after or equal to index. This will remove visible items
256      * after that and invalidate cache of layout results after that. Note that it's client's
257      * responsibility to perform removing child action, {@link Provider#removeItem(int)} will not
258      * be called because the index might be invalidated.
259      */
invalidateItemsAfter(int index)260     public void invalidateItemsAfter(int index) {
261         if (index < 0) {
262             return;
263         }
264         if (mLastVisibleIndex < 0) {
265             return;
266         }
267         if (mLastVisibleIndex >= index) {
268             mLastVisibleIndex = index - 1;
269         }
270         resetVisibleIndexIfEmpty();
271         if (getFirstVisibleIndex() < 0) {
272             setStart(index);
273         }
274     }
275 
276     /**
277      * Gets the row index of item at given index.
278      */
getRowIndex(int index)279     public final int getRowIndex(int index) {
280         Location location = getLocation(index);
281         if (location == null) {
282             return -1;
283         }
284         return location.mRow;
285     }
286 
287     /**
288      * Gets {@link Location} of item.  The return object is read only and temporarily.
289      */
getLocation(int index)290     public abstract Location getLocation(int index);
291 
292     /**
293      * Finds the largest or smallest row min edge of visible items,
294      * the row index is returned in indices[0], the item index is returned in indices[1].
295      */
findRowMin(boolean findLarge, int @Nullable [] indices)296     public final int findRowMin(boolean findLarge, int @Nullable [] indices) {
297         return findRowMin(findLarge, mReversedFlow ? mLastVisibleIndex : mFirstVisibleIndex,
298                 indices);
299     }
300 
301     /**
302      * Finds the largest or smallest row min edge of visible items, starts searching from
303      * indexLimit, the row index is returned in indices[0], the item index is returned in
304      * indices[1].
305      */
findRowMin(boolean findLarge, int indexLimit, int[] rowIndex)306     protected abstract int findRowMin(boolean findLarge, int indexLimit, int[] rowIndex);
307 
308     /**
309      * Finds the largest or smallest row max edge of visible items, the row index is returned in
310      * indices[0], the item index is returned in indices[1].
311      */
findRowMax(boolean findLarge, int @Nullable [] indices)312     public final int findRowMax(boolean findLarge, int @Nullable [] indices) {
313         return findRowMax(findLarge, mReversedFlow ? mFirstVisibleIndex : mLastVisibleIndex,
314                 indices);
315     }
316 
317     /**
318      * Find largest or smallest row max edge of visible items, starts searching from indexLimit,
319      * the row index is returned in indices[0], the item index is returned in indices[1].
320      */
findRowMax(boolean findLarge, int indexLimit, int[] indices)321     protected abstract int findRowMax(boolean findLarge, int indexLimit, int[] indices);
322 
323     /**
324      * Returns true if appending item has reached "toLimit"
325      */
checkAppendOverLimit(int toLimit)326     protected final boolean checkAppendOverLimit(int toLimit) {
327         if (mLastVisibleIndex < 0) {
328             return false;
329         }
330         return mReversedFlow ? findRowMin(true, null) <= toLimit + mSpacing :
331                 findRowMax(false, null) >= toLimit - mSpacing;
332     }
333 
334     /**
335      * Returns true if prepending item has reached "toLimit"
336      */
checkPrependOverLimit(int toLimit)337     protected final boolean checkPrependOverLimit(int toLimit) {
338         if (mLastVisibleIndex < 0) {
339             return false;
340         }
341         return mReversedFlow ? findRowMax(false, null) >= toLimit - mSpacing :
342                 findRowMin(true, null) <= toLimit + mSpacing;
343     }
344 
345     /**
346      * Return array of int array for all rows, each int array contains visible item positions
347      * in pair on that row between startPos(included) and endPositions(included).
348      * Returned value is read only, do not change it.
349      * <p>
350      * E.g. First row has 3,7,8, second row has 4,5,6.
351      * getItemPositionsInRows(3, 8) returns { {3,3,7,8}, {4,6} }
352      */
getItemPositionsInRows(int startPos, int endPos)353     public abstract CircularIntArray[] getItemPositionsInRows(int startPos, int endPos);
354 
355     /**
356      * Return array of int array for all rows, each int array contains visible item positions
357      * in pair on that row.
358      * Returned value is read only, do not change it.
359      * <p>
360      * E.g. First row has 3,7,8, second row has 4,5,6  { {3,3,7,8}, {4,6} }
361      */
getItemPositionsInRows()362     public final CircularIntArray[] getItemPositionsInRows() {
363         return getItemPositionsInRows(getFirstVisibleIndex(), getLastVisibleIndex());
364     }
365 
366     /**
367      * Prepends items and stops after one column is filled.
368      * (i.e. filled items from row 0 to row mNumRows - 1)
369      *
370      * @return true if at least one item is filled.
371      */
prependOneColumnVisibleItems()372     public final boolean prependOneColumnVisibleItems() {
373         return prependVisibleItems(mReversedFlow ? Integer.MIN_VALUE : Integer.MAX_VALUE, true);
374     }
375 
376     /**
377      * Prepends items until first item or reaches toLimit (min edge when not reversed or
378      * max edge when reversed)
379      */
prependVisibleItems(int toLimit)380     public final void prependVisibleItems(int toLimit) {
381         prependVisibleItems(toLimit, false);
382     }
383 
384     /**
385      * Prepends items until first item or reaches toLimit (min edge when not reversed or
386      * max edge when reversed).
387      *
388      * @param oneColumnMode true when fills one column and stops,  false
389      *                      when checks if condition matches before filling first column.
390      * @return true if at least one item is filled.
391      */
prependVisibleItems(int toLimit, boolean oneColumnMode)392     protected abstract boolean prependVisibleItems(int toLimit, boolean oneColumnMode);
393 
394     /**
395      * Appends items and stops after one column is filled.
396      * (i.e. filled items from row 0 to row mNumRows - 1)
397      *
398      * @return true if at least one item is filled.
399      */
appendOneColumnVisibleItems()400     public boolean appendOneColumnVisibleItems() {
401         return appendVisibleItems(mReversedFlow ? Integer.MAX_VALUE : Integer.MIN_VALUE, true);
402     }
403 
404     /**
405      * Append items until last item or reaches toLimit (max edge when not
406      * reversed or min edge when reversed)
407      */
appendVisibleItems(int toLimit)408     public final void appendVisibleItems(int toLimit) {
409         appendVisibleItems(toLimit, false);
410     }
411 
412     /**
413      * Appends items until last or reaches toLimit (high edge when not
414      * reversed or low edge when reversed).
415      *
416      * @param oneColumnMode True when fills one column and stops,  false
417      *                      when checks if condition matches before filling first column.
418      * @return true if filled at least one item
419      */
appendVisibleItems(int toLimit, boolean oneColumnMode)420     protected abstract boolean appendVisibleItems(int toLimit, boolean oneColumnMode);
421 
422     /**
423      * Removes invisible items from end until reaches item at aboveIndex or toLimit.
424      *
425      * @param aboveIndex Don't remove items whose index is equals or smaller than aboveIndex
426      * @param toLimit    Don't remove items whose left edge is less than toLimit.
427      */
removeInvisibleItemsAtEnd(int aboveIndex, int toLimit)428     public void removeInvisibleItemsAtEnd(int aboveIndex, int toLimit) {
429         while (mLastVisibleIndex >= mFirstVisibleIndex && mLastVisibleIndex > aboveIndex) {
430             boolean offEnd = !mReversedFlow ? mProvider.getEdge(mLastVisibleIndex) >= toLimit
431                     : mProvider.getEdge(mLastVisibleIndex) <= toLimit;
432             if (offEnd) {
433                 mProvider.removeItem(mLastVisibleIndex);
434                 mLastVisibleIndex--;
435             } else {
436                 break;
437             }
438         }
439         resetVisibleIndexIfEmpty();
440     }
441 
442     /**
443      * Removes invisible items from front until reaches item at belowIndex or toLimit.
444      *
445      * @param belowIndex Don't remove items whose index is equals or larger than belowIndex
446      * @param toLimit    Don't remove items whose right edge is equals or greater than toLimit.
447      */
removeInvisibleItemsAtFront(int belowIndex, int toLimit)448     public void removeInvisibleItemsAtFront(int belowIndex, int toLimit) {
449         while (mLastVisibleIndex >= mFirstVisibleIndex && mFirstVisibleIndex < belowIndex) {
450             final int size = mProvider.getSize(mFirstVisibleIndex);
451             boolean offFront = !mReversedFlow
452                     ? mProvider.getEdge(mFirstVisibleIndex) + size <= toLimit
453                     : mProvider.getEdge(mFirstVisibleIndex) - size >= toLimit;
454             if (offFront) {
455                 mProvider.removeItem(mFirstVisibleIndex);
456                 mFirstVisibleIndex++;
457             } else {
458                 break;
459             }
460         }
461         resetVisibleIndexIfEmpty();
462     }
463 
resetVisibleIndexIfEmpty()464     private void resetVisibleIndexIfEmpty() {
465         if (mLastVisibleIndex < mFirstVisibleIndex) {
466             resetVisibleIndex();
467         }
468     }
469 
470     /**
471      * Fill disappearing items, i.e. the items are moved out of window, we need give them final
472      * location so recyclerview will run a slide out animation. The positions that was greater than
473      * last visible index will be appended to end, the positions that was smaller than first visible
474      * index will be prepend to beginning.
475      *
476      * @param positions     Sorted list of positions of disappearing items.
477      * @param positionToRow Which row we want to put the disappearing item.
478      */
fillDisappearingItems(int[] positions, int positionsLength, SparseIntArray positionToRow)479     public void fillDisappearingItems(int[] positions, int positionsLength,
480             SparseIntArray positionToRow) {
481         final int lastPos = getLastVisibleIndex();
482         final int resultSearchLast = lastPos >= 0
483                 ? Arrays.binarySearch(positions, 0, positionsLength, lastPos) : 0;
484         if (resultSearchLast < 0) {
485             // we shouldn't find lastPos in disappearing position list.
486             int firstDisappearingIndex = -resultSearchLast - 1;
487             int edge;
488             if (mReversedFlow) {
489                 edge = mProvider.getEdge(lastPos) - mProvider.getSize(lastPos) - mSpacing;
490             } else {
491                 edge = mProvider.getEdge(lastPos) + mProvider.getSize(lastPos) + mSpacing;
492             }
493             for (int i = firstDisappearingIndex; i < positionsLength; i++) {
494                 int disappearingIndex = positions[i];
495                 int disappearingRow = positionToRow.get(disappearingIndex);
496                 if (disappearingRow < 0) {
497                     disappearingRow = 0; // if not found put in row 0
498                 }
499                 int size = mProvider.createItem(disappearingIndex, true, mTmpItem, true);
500                 mProvider.addItem(mTmpItem[0], disappearingIndex, size, disappearingRow, edge);
501                 if (mReversedFlow) {
502                     edge = edge - size - mSpacing;
503                 } else {
504                     edge = edge + size + mSpacing;
505                 }
506             }
507         }
508 
509         final int firstPos = getFirstVisibleIndex();
510         final int resultSearchFirst = firstPos >= 0
511                 ? Arrays.binarySearch(positions, 0, positionsLength, firstPos) : 0;
512         if (resultSearchFirst < 0) {
513             // we shouldn't find firstPos in disappearing position list.
514             int firstDisappearingIndex = -resultSearchFirst - 2;
515             int edge;
516             if (mReversedFlow) {
517                 edge = mProvider.getEdge(firstPos);
518             } else {
519                 edge = mProvider.getEdge(firstPos);
520             }
521             for (int i = firstDisappearingIndex; i >= 0; i--) {
522                 int disappearingIndex = positions[i];
523                 int disappearingRow = positionToRow.get(disappearingIndex);
524                 if (disappearingRow < 0) {
525                     disappearingRow = 0; // if not found put in row 0
526                 }
527                 int size = mProvider.createItem(disappearingIndex, false, mTmpItem, true);
528                 if (mReversedFlow) {
529                     edge = edge + mSpacing + size;
530                 } else {
531                     edge = edge - mSpacing - size;
532                 }
533                 mProvider.addItem(mTmpItem[0], disappearingIndex, size, disappearingRow, edge);
534             }
535         }
536     }
537 
538     /**
539      * Queries items adjacent to the viewport (in the direction of da) into the prefetch registry.
540      */
collectAdjacentPrefetchPositions(int fromLimit, int da, RecyclerView.LayoutManager.@NonNull LayoutPrefetchRegistry layoutPrefetchRegistry)541     public void collectAdjacentPrefetchPositions(int fromLimit, int da,
542             RecyclerView.LayoutManager.@NonNull LayoutPrefetchRegistry layoutPrefetchRegistry) {
543     }
544 
debugPrint(PrintWriter pw)545     public abstract void debugPrint(PrintWriter pw);
546 }
547