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