StaggeredGrid.java revision 79b86b227e6794937ec311522b50e727f8eec263
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.CircularArray;
17
18import java.io.PrintWriter;
19import java.util.ArrayList;
20import java.util.List;
21
22/**
23 * A dynamic data structure that maintains staggered grid position information
24 * for each individual child. The algorithm ensures that each row will be kept
25 * as balanced as possible when prepending and appending a child.
26 *
27 * <p>
28 * You may keep view {@link StaggeredGrid.Location} inside StaggeredGrid as much
29 * as possible since prepending and appending views is not symmetric: layout
30 * going from 0 to N will likely produce a different result than layout going
31 * from N to 0 for the staggered cases. If a user scrolls from 0 to N then
32 * scrolls back to 0 and we don't keep history location information, edges of
33 * the very beginning of rows will not be aligned. It is recommended to keep a
34 * list of tens of thousands of {@link StaggeredGrid.Location}s which will be
35 * big enough to remember a typical user's scroll history. There are situations
36 * where StaggeredGrid falls back to the simple case where we do not need save a
37 * huge list of locations inside StaggeredGrid:
38 * <ul>
39 *   <li>Only one row (e.g., a single row listview)</li>
40 *   <li> Each item has the same length (not staggered at all)</li>
41 * </ul>
42 *
43 * <p>
44 * This class is abstract and can be replaced with different implementations.
45 */
46abstract class StaggeredGrid {
47
48    /**
49     * TODO: document this
50     */
51    public static interface Provider {
52        /**
53         * Return how many items are in the adapter.
54         */
55        public abstract int getCount();
56
57        /**
58         * Create the object at a given row.
59         */
60        public abstract void createItem(int index, int row, boolean append);
61    }
62
63    /**
64     * Location of an item in the grid. For now it only saves row index but
65     * more information may be added in the future.
66     */
67    public final static class Location {
68        /**
69         * The index of the row for this Location.
70         */
71        public final int row;
72
73        /**
74         * Create a Location with the given row index.
75         */
76        public Location(int row) {
77            this.row = row;
78        }
79    }
80
81    /**
82     * TODO: document this
83     */
84    public final static class Row {
85        /**
86         * first view start location
87         */
88        public int low;
89        /**
90         * last view end location
91         */
92        public int high;
93    }
94
95    protected Provider mProvider;
96    protected int mNumRows = 1; // mRows.length
97    protected Row[] mRows;
98    protected CircularArray<Location> mLocations = new CircularArray<Location>(64);
99    private ArrayList<Integer>[] mTmpItemPositionsInRows;
100
101    /**
102     * A constant representing a default starting index, indicating that the
103     * developer did not provide a start index.
104     */
105    public static final int START_DEFAULT = -1;
106
107    // the first index that grid will layout
108    protected int mStartIndex = START_DEFAULT;
109    // the row to layout the first index
110    protected int mStartRow = START_DEFAULT;
111
112    protected int mFirstIndex = -1;
113
114    /**
115     * Sets the {@link Provider} for this staggered grid.
116     *
117     * @param provider The provider for this staggered grid.
118     */
119    public void setProvider(Provider provider) {
120        mProvider = provider;
121    }
122
123    /**
124     * Sets the array of {@link Row}s to fill into. For views that represent a
125     * horizontal list, this will be the rows of the view. For views that
126     * represent a vertical list, this will be the columns.
127     *
128     * @param row The array of {@link Row}s to be filled.
129     */
130    public final void setRows(Row[] row) {
131        if (row == null || row.length == 0) {
132            throw new IllegalArgumentException();
133        }
134        mNumRows = row.length;
135        mRows = row;
136        mTmpItemPositionsInRows = new ArrayList[mNumRows];
137        for (int i = 0; i < mNumRows; i++) {
138            mTmpItemPositionsInRows[i] = new ArrayList(32);
139        }
140    }
141
142    /**
143     * Returns the number of rows in the staggered grid.
144     */
145    public final int getNumRows() {
146        return mNumRows;
147    }
148
149    /**
150     * Set the first item index and the row index to load when there are no
151     * items.
152     *
153     * @param startIndex the index of the first item
154     * @param startRow the index of the row
155     */
156    public final void setStart(int startIndex, int startRow) {
157        mStartIndex = startIndex;
158        mStartRow = startRow;
159    }
160
161    /**
162     * Returns the first index in the staggered grid.
163     */
164    public final int getFirstIndex() {
165        return mFirstIndex;
166    }
167
168    /**
169     * Returns the last index in the staggered grid.
170     */
171    public final int getLastIndex() {
172        return mFirstIndex + mLocations.size() - 1;
173    }
174
175    /**
176     * Returns the size of the saved {@link Location}s.
177     */
178    public final int getSize() {
179        return mLocations.size();
180    }
181
182    /**
183     * Returns the {@link Location} at the given index.
184     */
185    public final Location getLocation(int index) {
186        if (mLocations.size() == 0) {
187            return null;
188        }
189        return mLocations.get(index - mFirstIndex);
190    }
191
192    /**
193     * Removes the first element.
194     */
195    public final void removeFirst() {
196        mFirstIndex++;
197        mLocations.popFirst();
198    }
199
200    /**
201     * Removes the last element.
202     */
203    public final void removeLast() {
204        mLocations.popLast();
205    }
206
207    public final void debugPrint(PrintWriter pw) {
208        for (int i = 0, size = mLocations.size(); i < size; i++) {
209            Location loc = mLocations.get(i);
210            pw.print("<" + (mFirstIndex + i) + "," + loc.row + ">");
211            pw.print(" ");
212            pw.println();
213        }
214    }
215
216    protected final int getMaxHighRowIndex() {
217        int maxHighRowIndex = 0;
218        for (int i = 1; i < mNumRows; i++) {
219            if (mRows[i].high > mRows[maxHighRowIndex].high) {
220                maxHighRowIndex = i;
221            }
222        }
223        return maxHighRowIndex;
224    }
225
226    protected final int getMinHighRowIndex() {
227        int minHighRowIndex = 0;
228        for (int i = 1; i < mNumRows; i++) {
229            if (mRows[i].high < mRows[minHighRowIndex].high) {
230                minHighRowIndex = i;
231            }
232        }
233        return minHighRowIndex;
234    }
235
236    protected final Location appendItemToRow(int itemIndex, int rowIndex) {
237        Location loc = new Location(rowIndex);
238        if (mLocations.size() == 0) {
239            mFirstIndex = itemIndex;
240        }
241        mLocations.addLast(loc);
242        mProvider.createItem(itemIndex, rowIndex, true);
243        return loc;
244    }
245
246    /**
247     * Append items until the high edge reaches upTo.
248     */
249    public abstract void appendItems(int upTo);
250
251    protected final int getMaxLowRowIndex() {
252        int maxLowRowIndex = 0;
253        for (int i = 1; i < mNumRows; i++) {
254            if (mRows[i].low > mRows[maxLowRowIndex].low) {
255                maxLowRowIndex = i;
256            }
257        }
258        return maxLowRowIndex;
259    }
260
261    protected final int getMinLowRowIndex() {
262        int minLowRowIndex = 0;
263        for (int i = 1; i < mNumRows; i++) {
264            if (mRows[i].low < mRows[minLowRowIndex].low) {
265                minLowRowIndex = i;
266            }
267        }
268        return minLowRowIndex;
269    }
270
271    protected final Location prependItemToRow(int itemIndex, int rowIndex) {
272        Location loc = new Location(rowIndex);
273        mFirstIndex = itemIndex;
274        mLocations.addFirst(loc);
275        mProvider.createItem(itemIndex, rowIndex, false);
276        return loc;
277    }
278
279    /**
280     * Return array of Lists for all rows, each List contains item positions
281     * on that row between startPos(included) and endPositions(included).
282     * Returned value is read only, do not change it.
283     */
284    public final List<Integer>[] getItemPositionsInRows(int startPos, int endPos) {
285        for (int i = 0; i < mNumRows; i++) {
286            mTmpItemPositionsInRows[i].clear();
287        }
288        if (startPos >= 0) {
289            for (int i = startPos; i <= endPos; i++) {
290                mTmpItemPositionsInRows[getLocation(i).row].add(i);
291            }
292        }
293        return mTmpItemPositionsInRows;
294    }
295
296    /**
297     * Prepend items until the low edge reaches downTo.
298     */
299    public abstract void prependItems(int downTo);
300
301    /**
302     * Strip items, keep a contiguous subset of items; the subset should include
303     * at least one item on every row that currently has at least one item.
304     *
305     * <p>
306     * TODO: document this better
307     */
308    public abstract void stripDownTo(int itemIndex);
309}
310