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 android.support.v17.leanback.widget;
15 
16 import android.support.v4.util.CircularIntArray;
17 import android.util.Log;
18 
19 import java.io.PrintWriter;
20 
21 /**
22  * A grid is representation of single or multiple rows layout data structure and algorithm.
23  * Grid is the base class for single row, non-staggered grid and staggered grid.
24  * <p>
25  * To use the Grid, user must implement a Provider to create or remove visible item.
26  * Grid maintains a list of visible items.  Visible items are created when
27  * user calls appendVisibleItems() or prependVisibleItems() with certain limitation
28  * (e.g. a max edge that append up to).  Visible items are deleted when user calls
29  * removeInvisibleItemsAtEnd() or removeInvisibleItemsAtFront().  Grid's algorithm
30  * uses size of visible item returned from Provider.createItem() to decide which row
31  * to add a new visible item and may cache the algorithm results.   User must call
32  * invalidateItemsAfter() when it detects item size changed to ask Grid to remove cached
33  * results.
34  */
35 abstract class Grid {
36 
37     /**
38      * A constant representing a default starting index, indicating that the
39      * developer did not provide a start index.
40      */
41     public static final int START_DEFAULT = -1;
42 
43     /**
44      * When user uses Grid,  he should provide count of items and
45      * the method to create item and remove item.
46      */
47     public static interface Provider {
48 
49         /**
50          * Return how many items (are in the adapter).
51          */
getCount()52         public abstract int getCount();
53 
54         /**
55          * Create visible item and where the provider should measure it.
56          * The call is always followed by addItem().
57          * @param index     0-based index of the item in provider
58          * @param append  True if new item is after last visible item, false if new item is
59          *                before first visible item.
60          * @param item    item[0] returns created item that will be passed in addItem() call.
61          * @return length of the item.
62          */
createItem(int index, boolean append, Object[] item)63         public abstract int createItem(int index, boolean append, Object[] item);
64 
65         /**
66          * add item to given row and given edge.  The call is always after createItem().
67          * @param item      The object returned by createItem()
68          * @param index     0-based index of the item in provider
69          * @param length    The size of the object
70          * @param rowIndex  Row index to put the item
71          * @param edge      min_edge if not reversed or max_edge if reversed.
72          */
addItem(Object item, int index, int length, int rowIndex, int edge)73         public abstract void addItem(Object item, int index, int length, int rowIndex, int edge);
74 
75         /**
76          * Remove visible item at index.
77          * @param index     0-based index of the item in provider
78          */
removeItem(int index)79         public abstract void removeItem(int index);
80 
81         /**
82          * Get edge of an existing visible item. edge will be the min_edge
83          * if not reversed or the max_edge if reversed.
84          * @param index     0-based index of the item in provider
85          */
getEdge(int index)86         public abstract int getEdge(int index);
87 
88         /**
89          * Get size of an existing visible item.
90          * @param index     0-based index of the item in provider
91          */
getSize(int index)92         public abstract int getSize(int index);
93     }
94 
95     /**
96      * Cached representation of an item in Grid.  May be subclassed.
97      */
98     public static class Location {
99         /**
100          * The index of the row for this Location.
101          */
102         public int row;
103 
Location(int row)104         public Location(int row) {
105             this.row = row;
106         }
107     }
108 
109     protected Provider mProvider;
110     protected boolean mReversedFlow;
111     protected int mMargin;
112     protected int mNumRows;
113     protected int mFirstVisibleIndex = -1;
114     protected int mLastVisibleIndex = -1;
115 
116     protected CircularIntArray[] mTmpItemPositionsInRows;
117 
118     // the first index that grid will layout
119     protected int mStartIndex = START_DEFAULT;
120 
121     /**
122      * Creates a single or multiple rows (can be staggered or not staggered) grid
123      */
createGrid(int rows)124     public static Grid createGrid(int rows) {
125         Grid grid;
126         if (rows == 1) {
127             grid = new SingleRow();
128         } else {
129             // TODO support non staggered multiple rows grid
130             grid = new StaggeredGridDefault();
131             grid.setNumRows(rows);
132         }
133         return grid;
134     }
135 
136     /**
137      * Sets the margin between items in a row
138      */
setMargin(int margin)139     public final void setMargin(int margin) {
140         mMargin = margin;
141     }
142 
143     /**
144      * Sets if reversed flow (rtl)
145      */
setReversedFlow(boolean reversedFlow)146     public final void setReversedFlow(boolean reversedFlow) {
147         mReversedFlow = reversedFlow;
148     }
149 
150     /**
151      * Returns true if reversed flow (rtl)
152      */
isReversedFlow()153     public boolean isReversedFlow() {
154         return mReversedFlow;
155     }
156 
157     /**
158      * Sets the {@link Provider} for this grid.
159      *
160      * @param provider The provider for this grid.
161      */
setProvider(Provider provider)162     public void setProvider(Provider provider) {
163         mProvider = provider;
164     }
165 
166     /**
167      * Sets the first item index to create when there are no items.
168      *
169      * @param startIndex the index of the first item
170      */
setStart(int startIndex)171     public void setStart(int startIndex) {
172         mStartIndex = startIndex;
173     }
174 
175     /**
176      * Returns the number of rows in the grid.
177      */
getNumRows()178     public int getNumRows() {
179         return mNumRows;
180     }
181 
182     /**
183      * Sets number of rows to fill into. For views that represent a
184      * horizontal list, this will be the rows of the view. For views that
185      * represent a vertical list, this will be the columns.
186      *
187      * @param numRows numberOfRows
188      */
setNumRows(int numRows)189     void setNumRows(int numRows) {
190         if (numRows <= 0) {
191             throw new IllegalArgumentException();
192         }
193         if (mNumRows == numRows) {
194             return;
195         }
196         mNumRows = numRows;
197         mTmpItemPositionsInRows = new CircularIntArray[mNumRows];
198         for (int i = 0; i < mNumRows; i++) {
199             mTmpItemPositionsInRows[i] = new CircularIntArray();
200         }
201     }
202 
203     /**
204      * Returns index of first visible item in the staggered grid.  Returns negative value
205      * if no visible item.
206      */
getFirstVisibleIndex()207     public final int getFirstVisibleIndex() {
208         return mFirstVisibleIndex;
209     }
210 
211     /**
212      * Returns index of last visible item in the staggered grid.  Returns negative value
213      * if no visible item.
214      */
getLastVisibleIndex()215     public final int getLastVisibleIndex() {
216         return mLastVisibleIndex;
217     }
218 
219     /**
220      * Reset visible indices and keep cache (if exists)
221      */
resetVisibleIndex()222     public void resetVisibleIndex() {
223         mFirstVisibleIndex = mLastVisibleIndex = -1;
224     }
225 
226     /**
227      * Invalidate items after or equal to index. This will remove visible items
228      * after that and invalidate cache of layout results after that.
229      */
invalidateItemsAfter(int index)230     public void invalidateItemsAfter(int index) {
231         if (index < 0) {
232             return;
233         }
234         if (mLastVisibleIndex < 0) {
235             return;
236         }
237         while (mLastVisibleIndex >= index) {
238             mProvider.removeItem(mLastVisibleIndex);
239             mLastVisibleIndex--;
240         }
241         resetVisbileIndexIfEmpty();
242         if (getFirstVisibleIndex() < 0) {
243             setStart(index);
244         }
245     }
246 
247     /**
248      * Gets the row index of item at given index.
249      */
getRowIndex(int index)250     public final int getRowIndex(int index) {
251         return getLocation(index).row;
252     }
253 
254     /**
255      * Gets {@link Location} of item.  The return object is read only and temporarily.
256      */
getLocation(int index)257     public abstract Location getLocation(int index);
258 
259     /**
260      * Finds the largest or smallest row min edge of visible items,
261      * the row index is returned in indices[0], the item index is returned in indices[1].
262      */
findRowMin(boolean findLarge, int[] indices)263     public final int findRowMin(boolean findLarge, int[] indices) {
264         return findRowMin(findLarge, mReversedFlow ? mLastVisibleIndex : mFirstVisibleIndex,
265                 indices);
266     }
267 
268     /**
269      * Finds the largest or smallest row min edge of visible items, starts searching from
270      * indexLimit, the row index is returned in indices[0], the item index is returned in indices[1].
271      */
findRowMin(boolean findLarge, int indexLimit, int[] rowIndex)272     protected abstract int findRowMin(boolean findLarge, int indexLimit, int[] rowIndex);
273 
274     /**
275      * Finds the largest or smallest row max edge of visible items, the row index is returned in
276      * indices[0], the item index is returned in indices[1].
277      */
findRowMax(boolean findLarge, int[] indices)278     public final int findRowMax(boolean findLarge, int[] indices) {
279         return findRowMax(findLarge, mReversedFlow ? mFirstVisibleIndex : mLastVisibleIndex,
280                 indices);
281     }
282 
283     /**
284      * Find largest or smallest row max edge of visible items, starts searching from indexLimit,
285      * the row index is returned in indices[0], the item index is returned in indices[1].
286      */
findRowMax(boolean findLarge, int indexLimit, int[] indices)287     protected abstract int findRowMax(boolean findLarge, int indexLimit, int[] indices);
288 
289     /**
290      * Returns true if appending item has reached "toLimit"
291      */
checkAppendOverLimit(int toLimit)292     protected final boolean checkAppendOverLimit(int toLimit) {
293         if (mLastVisibleIndex < 0) {
294             return false;
295         }
296         return mReversedFlow ? findRowMin(true, null) <= toLimit + mMargin :
297                     findRowMax(false, null) >= toLimit - mMargin;
298     }
299 
300     /**
301      * Returns true if prepending item has reached "toLimit"
302      */
checkPrependOverLimit(int toLimit)303     protected final boolean checkPrependOverLimit(int toLimit) {
304         if (mLastVisibleIndex < 0) {
305             return false;
306         }
307         return mReversedFlow ? findRowMax(false, null) >= toLimit + mMargin :
308                     findRowMin(true, null) <= toLimit - mMargin;
309     }
310 
311     /**
312      * Return array of int array for all rows, each int array contains visible item positions
313      * in pair on that row between startPos(included) and endPositions(included).
314      * Returned value is read only, do not change it.
315      * <p>
316      * E.g. First row has 3,7,8, second row has 4,5,6.
317      * getItemPositionsInRows(3, 8) returns { {3,3,7,8}, {4,6} }
318      */
getItemPositionsInRows(int startPos, int endPos)319     public abstract CircularIntArray[] getItemPositionsInRows(int startPos, int endPos);
320 
321     /**
322      * Return array of int array for all rows, each int array contains visible item positions
323      * in pair on that row.
324      * Returned value is read only, do not change it.
325      * <p>
326      * E.g. First row has 3,7,8, second row has 4,5,6  { {3,3,7,8}, {4,6} }
327      */
getItemPositionsInRows()328     public final CircularIntArray[] getItemPositionsInRows() {
329         return getItemPositionsInRows(getFirstVisibleIndex(), getLastVisibleIndex());
330     }
331 
332     /**
333      * Prepends items and stops after one column is filled.
334      * (i.e. filled items from row 0 to row mNumRows - 1)
335      * @return true if at least one item is filled.
336      */
prependOneColumnVisibleItems()337     public final boolean prependOneColumnVisibleItems() {
338         return prependVisibleItems(mReversedFlow ? Integer.MIN_VALUE : Integer.MAX_VALUE, true);
339     }
340 
341     /**
342      * Prepends items until first item or reaches toLimit (min edge when not reversed or
343      * max edge when reversed)
344      */
prependVisibleItems(int toLimit)345     public final void prependVisibleItems(int toLimit) {
346         prependVisibleItems(toLimit, false);
347     }
348 
349     /**
350      * Prepends items until first item or reaches toLimit (min edge when not reversed or
351      * max edge when reversed).
352      * @param oneColumnMode  true when fills one column and stops,  false
353      * when checks if condition matches before filling first column.
354      * @return true if at least one item is filled.
355      */
prependVisibleItems(int toLimit, boolean oneColumnMode)356     protected abstract boolean prependVisibleItems(int toLimit, boolean oneColumnMode);
357 
358     /**
359      * Appends items and stops after one column is filled.
360      * (i.e. filled items from row 0 to row mNumRows - 1)
361      * @return true if at least one item is filled.
362      */
appendOneColumnVisibleItems()363     public boolean appendOneColumnVisibleItems() {
364         return appendVisibleItems(mReversedFlow ? Integer.MAX_VALUE : Integer.MIN_VALUE, true);
365     }
366 
367     /**
368      * Append items until last item or reaches toLimit (max edge when not
369      * reversed or min edge when reversed)
370      */
appendVisibleItems(int toLimit)371     public final void appendVisibleItems(int toLimit) {
372         appendVisibleItems(toLimit, false);
373     }
374 
375     /**
376      * Appends items until last or reaches toLimit (high edge when not
377      * reversed or low edge when reversed).
378      * @param oneColumnMode True when fills one column and stops,  false
379      * when checks if condition matches before filling first column.
380      * @return true if filled at least one item
381      */
appendVisibleItems(int toLimit, boolean oneColumnMode)382     protected abstract boolean appendVisibleItems(int toLimit, boolean oneColumnMode);
383 
384     /**
385      * Removes invisible items from end until reaches item at aboveIndex or toLimit.
386      */
removeInvisibleItemsAtEnd(int aboveIndex, int toLimit)387     public void removeInvisibleItemsAtEnd(int aboveIndex, int toLimit) {
388         while(mLastVisibleIndex >= mFirstVisibleIndex && mLastVisibleIndex > aboveIndex) {
389             boolean offEnd = !mReversedFlow ? mProvider.getEdge(mLastVisibleIndex) >= toLimit
390                     : mProvider.getEdge(mLastVisibleIndex) <= toLimit;
391             if (offEnd) {
392                 mProvider.removeItem(mLastVisibleIndex);
393                 mLastVisibleIndex--;
394             } else {
395                 break;
396             }
397         }
398         resetVisbileIndexIfEmpty();
399     }
400 
401     /**
402      * Removes invisible items from front until reaches item at belowIndex or toLimit.
403      */
removeInvisibleItemsAtFront(int belowIndex, int toLimit)404     public void removeInvisibleItemsAtFront(int belowIndex, int toLimit) {
405         while(mLastVisibleIndex >= mFirstVisibleIndex && mFirstVisibleIndex < belowIndex) {
406             boolean offFront = !mReversedFlow ? mProvider.getEdge(mFirstVisibleIndex)
407                     + mProvider.getSize(mFirstVisibleIndex) <= toLimit
408                     : mProvider.getEdge(mFirstVisibleIndex)
409                             - mProvider.getSize(mFirstVisibleIndex) >= toLimit;
410             if (offFront) {
411                 mProvider.removeItem(mFirstVisibleIndex);
412                 mFirstVisibleIndex++;
413             } else {
414                 break;
415             }
416         }
417         resetVisbileIndexIfEmpty();
418     }
419 
resetVisbileIndexIfEmpty()420     private void resetVisbileIndexIfEmpty() {
421         if (mLastVisibleIndex < mFirstVisibleIndex) {
422             resetVisibleIndex();
423         }
424     }
425 
debugPrint(PrintWriter pw)426     public abstract void debugPrint(PrintWriter pw);
427 }
428