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 */
14package android.support.v17.leanback.widget;
15
16import android.support.v4.util.CircularIntArray;
17import android.util.Log;
18
19import 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 */
35abstract 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         */
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         */
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         */
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         */
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         */
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         */
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
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     */
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     */
139    public final void setMargin(int margin) {
140        mMargin = margin;
141    }
142
143    /**
144     * Sets if reversed flow (rtl)
145     */
146    public final void setReversedFlow(boolean reversedFlow) {
147        mReversedFlow = reversedFlow;
148    }
149
150    /**
151     * Returns true if reversed flow (rtl)
152     */
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     */
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     */
171    public void setStart(int startIndex) {
172        mStartIndex = startIndex;
173    }
174
175    /**
176     * Returns the number of rows in the grid.
177     */
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     */
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     */
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     */
215    public final int getLastVisibleIndex() {
216        return mLastVisibleIndex;
217    }
218
219    /**
220     * Reset visible indices and keep cache (if exists)
221     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
287    protected abstract int findRowMax(boolean findLarge, int indexLimit, int[] indices);
288
289    /**
290     * Returns true if appending item has reached "toLimit"
291     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
420    private void resetVisbileIndexIfEmpty() {
421        if (mLastVisibleIndex < mFirstVisibleIndex) {
422            resetVisibleIndex();
423        }
424    }
425
426    public abstract void debugPrint(PrintWriter pw);
427}
428