• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.CircularArray;
17 import android.support.v4.util.CircularIntArray;
18 
19 import java.io.PrintWriter;
20 import java.util.ArrayList;
21 import java.util.List;
22 
23 /**
24  * A dynamic data structure that caches staggered grid position information
25  * for each individual child. The algorithm ensures that each row will be kept
26  * as balanced as possible when prepending and appending a child.
27  *
28  * <p>
29  * You may keep view {@link StaggeredGrid.Location} inside StaggeredGrid as much
30  * as possible since prepending and appending views is not symmetric: layout
31  * going from 0 to N will likely produce a different result than layout going
32  * from N to 0 for the staggered cases. If a user scrolls from 0 to N then
33  * scrolls back to 0 and we don't keep history location information, edges of
34  * the very beginning of rows will not be aligned. It is recommended to keep a
35  * list of tens of thousands of {@link StaggeredGrid.Location}s which will be
36  * big enough to remember a typical user's scroll history.
37  *
38  * <p>
39  * This class is abstract and can be replaced with different implementations.
40  */
41 abstract class StaggeredGrid extends Grid {
42 
43     /**
44      * Cached representation of Staggered item.
45      */
46     public static class Location extends Grid.Location {
47         /**
48          * Offset to previous item location.
49          * min_edge(index) - min_edge(index - 1) for non reversed case
50          * max_edge(index) - max_edge(index - 1) for reversed case
51          */
52         public int offset;
53 
54         /**
55          * size of the item.
56          */
57         public int size;
58 
Location(int row, int offset, int size)59         public Location(int row, int offset, int size) {
60             super(row);
61             this.offset = offset;
62             this.size = size;
63         }
64     }
65 
66     protected CircularArray<Location> mLocations = new CircularArray<Location>(64);
67 
68     // mFirstIndex <= mFirstVisibleIndex <= mLastVisibleIndex
69     //    <= mFirstIndex + mLocations.size() - 1
70     protected int mFirstIndex = -1;
71 
72     private Object[] mTmpItem = new Object[1];
73 
74     protected Object mPendingItem;
75     protected int mPendingItemSize;
76 
77     /**
78      * Returns index of first item (cached or visible) in the staggered grid.
79      * Returns negative value if no item.
80      */
getFirstIndex()81     public final int getFirstIndex() {
82         return mFirstIndex;
83     }
84 
85     /**
86      * Returns index of last item (cached or visible) in the staggered grid.
87      * Returns negative value if no item.
88      */
getLastIndex()89     public final int getLastIndex() {
90         return mFirstIndex + mLocations.size() - 1;
91     }
92 
93     /**
94      * Returns the size of the saved {@link Location}s.
95      */
getSize()96     public final int getSize() {
97         return mLocations.size();
98     }
99 
100     @Override
getLocation(int index)101     public final Location getLocation(int index) {
102         if (mLocations.size() == 0) {
103             return null;
104         }
105         return mLocations.get(index - mFirstIndex);
106     }
107 
108     @Override
debugPrint(PrintWriter pw)109     public final void debugPrint(PrintWriter pw) {
110         for (int i = 0, size = mLocations.size(); i < size; i++) {
111             Location loc = mLocations.get(i);
112             pw.print("<" + (mFirstIndex + i) + "," + loc.row + ">");
113             pw.print(" ");
114             pw.println();
115         }
116     }
117 
118     @Override
prependVisibleItems(int toLimit, boolean oneColumnMode)119     protected final boolean prependVisibleItems(int toLimit, boolean oneColumnMode) {
120         if (mProvider.getCount() == 0) {
121             return false;
122         }
123         if (!oneColumnMode && checkPrependOverLimit(toLimit)) {
124             return false;
125         }
126         try {
127             if (prependVisbleItemsWithCache(toLimit, oneColumnMode)) {
128                 return true;
129             }
130             return prependVisibleItemsWithoutCache(toLimit, oneColumnMode);
131         } finally {
132             mTmpItem[0] = null;
133             mPendingItem = null;
134         }
135     }
136 
137     /**
138      * Prepends items using cached locations,  returning true if toLimit is reached.
139      * This method should only be called by prependVisibleItems().
140      */
prependVisbleItemsWithCache(int toLimit, boolean oneColumnMode)141     protected final boolean prependVisbleItemsWithCache(int toLimit, boolean oneColumnMode) {
142         if (mLocations.size() == 0) {
143             return false;
144         }
145         final int count = mProvider.getCount();
146         final int firstIndex = getFirstIndex();
147         int itemIndex;
148         int edge;
149         int offset;
150         if (mFirstVisibleIndex >= 0) {
151             // prepend visible items from first visible index
152             edge = mProvider.getEdge(mFirstVisibleIndex);
153             offset = getLocation(mFirstVisibleIndex).offset;
154             itemIndex = mFirstVisibleIndex - 1;
155         } else {
156             // prepend first visible item
157             edge = Integer.MAX_VALUE;
158             offset = 0;
159             itemIndex = mStartIndex != START_DEFAULT ? mStartIndex : 0;
160             if (itemIndex > getLastIndex() || itemIndex < getFirstIndex() - 1) {
161                 // if the item is not within or adjacent to cached items, clear cache.
162                 mLocations.clear();
163                 return false;
164             } else if (itemIndex < getFirstIndex()) {
165                 // if the item is adjacent to first index, should prepend without cache.
166                 return false;
167             }
168         }
169         for (; itemIndex >= mFirstIndex; itemIndex--) {
170             Location loc = getLocation(itemIndex);
171             int rowIndex = loc.row;
172             int size = mProvider.createItem(itemIndex, false, mTmpItem);
173             if (size != loc.size) {
174                 mLocations.removeFromStart(itemIndex + 1 - mFirstIndex);
175                 mFirstIndex = mFirstVisibleIndex;
176                 // pending item will be added in prependVisibleItemsWithoutCache
177                 mPendingItem = mTmpItem[0];
178                 mPendingItemSize = size;
179                 return false;
180             }
181             mFirstVisibleIndex = itemIndex;
182             if (mLastVisibleIndex < 0) {
183                 mLastVisibleIndex = itemIndex;
184             }
185             mProvider.addItem(mTmpItem[0], itemIndex, size, rowIndex, edge - offset);
186             if (!oneColumnMode && checkPrependOverLimit(toLimit)) {
187                 return true;
188             }
189             edge = mProvider.getEdge(itemIndex);
190             offset = loc.offset;
191             // Check limit after filled a full column
192             if (rowIndex == 0) {
193                 if (oneColumnMode) {
194                     return true;
195                 }
196             }
197         }
198         return false;
199     }
200 
201     /**
202      * Calculate offset of item after last cached item.
203      */
calculateOffsetAfterLastItem(int row)204     private int calculateOffsetAfterLastItem(int row) {
205         // Find a cached item in same row, if not found, just use last item.
206         int cachedIndex = getLastIndex();
207         boolean foundCachedItemInSameRow = false;
208         while (cachedIndex >= mFirstIndex) {
209             Location loc = getLocation(cachedIndex);
210             if (loc.row == row) {
211                 foundCachedItemInSameRow = true;
212                 break;
213             }
214             cachedIndex--;
215         }
216         if (!foundCachedItemInSameRow) {
217             cachedIndex = getLastIndex();
218         }
219         // Assuming the cachedIndex is next to item on the same row, so the
220         // sum of offset of [cachedIndex + 1, itemIndex] should be size of the
221         // cached item plus margin.
222         int offset = isReversedFlow() ?  -getLocation(cachedIndex).size - mMargin:
223                 getLocation(cachedIndex).size + mMargin;
224         for (int i = cachedIndex + 1; i <= getLastIndex(); i++) {
225             offset -= getLocation(i).offset;
226         }
227         return offset;
228     }
229 
230 
231     /**
232      * This implements the algorithm of layout staggered grid, the method should only be called by
233      * prependVisibleItems().
234      */
prependVisibleItemsWithoutCache(int toLimit, boolean oneColumnMode)235     protected abstract boolean prependVisibleItemsWithoutCache(int toLimit, boolean oneColumnMode);
236 
237     /**
238      * Prepends one visible item with new Location info.  Only called from
239      * prependVisibleItemsWithoutCache().
240      */
prependVisibleItemToRow(int itemIndex, int rowIndex, int edge)241     protected final int prependVisibleItemToRow(int itemIndex, int rowIndex, int edge) {
242         int offset;
243         if (mFirstVisibleIndex >= 0) {
244             if (mFirstVisibleIndex != getFirstIndex() || mFirstVisibleIndex != itemIndex + 1) {
245                 // should never hit this when we prepend a new item with a new Location object.
246                 throw new IllegalStateException();
247             }
248         }
249         Location oldFirstLoc = mFirstIndex >= 0 ? getLocation(mFirstIndex) : null;
250         int oldFirstEdge = mProvider.getEdge(mFirstIndex);
251         Location loc = new Location(rowIndex, 0, 0);
252         mLocations.addFirst(loc);
253         Object item;
254         if (mPendingItem != null) {
255             loc.size = mPendingItemSize;
256             item = mPendingItem;
257             mPendingItem = null;
258         } else {
259             loc.size = mProvider.createItem(itemIndex, false, mTmpItem);
260             item = mTmpItem[0];
261         }
262         mFirstIndex = mFirstVisibleIndex = itemIndex;
263         if (mLastVisibleIndex < 0) {
264             mLastVisibleIndex = itemIndex;
265         }
266         int thisEdge = !mReversedFlow ? edge - loc.size : edge + loc.size;
267         if (oldFirstLoc != null) {
268             oldFirstLoc.offset = oldFirstEdge - thisEdge;
269         }
270         mProvider.addItem(item, itemIndex, loc.size, rowIndex, thisEdge);
271         return loc.size;
272     }
273 
274     @Override
appendVisibleItems(int toLimit, boolean oneColumnMode)275     protected final boolean appendVisibleItems(int toLimit, boolean oneColumnMode) {
276         if (mProvider.getCount() == 0) {
277             return false;
278         }
279         if (!oneColumnMode && checkAppendOverLimit(toLimit)) {
280             return false;
281         }
282         try {
283             if (appendVisbleItemsWithCache(toLimit, oneColumnMode)) {
284                 return true;
285             }
286             return appendVisibleItemsWithoutCache(toLimit, oneColumnMode);
287         } finally {
288             mTmpItem[0] = null;
289             mPendingItem = null;
290         }
291     }
292 
293     /**
294      * Appends items using cached locations,  returning true if at least one item is appended
295      * and (oneColumnMode is true or reach limit and aboveIndex).
296      * This method should only be called by appendVisibleItems()
297      */
appendVisbleItemsWithCache(int toLimit, boolean oneColumnMode)298     protected final boolean appendVisbleItemsWithCache(int toLimit, boolean oneColumnMode) {
299         if (mLocations.size() == 0) {
300             return false;
301         }
302         final int count = mProvider.getCount();
303         int itemIndex;
304         int edge;
305         if (mLastVisibleIndex >= 0) {
306             // append visible items from last visible index
307             itemIndex = mLastVisibleIndex + 1;
308             edge = mProvider.getEdge(mLastVisibleIndex);
309         } else {
310             // append first visible item
311             edge = Integer.MAX_VALUE;
312             itemIndex = mStartIndex != START_DEFAULT ? mStartIndex : 0;
313             if (itemIndex > getLastIndex() + 1 || itemIndex < getFirstIndex()) {
314                 // if the item is not within or adjacent to cached items, clear cache.
315                 mLocations.clear();
316                 return false;
317             } else if (itemIndex > getLastIndex()) {
318                 // if the item is adjacent to first index, should prepend without cache.
319                 return false;
320             }
321         }
322         int lastIndex = getLastIndex();
323         for (; itemIndex < count && itemIndex <= lastIndex; itemIndex++) {
324             Location loc = getLocation(itemIndex);
325             if (edge != Integer.MAX_VALUE) {
326                 edge = edge + loc.offset;
327             }
328             int rowIndex = loc.row;
329             int size = mProvider.createItem(itemIndex, true, mTmpItem);
330             if (size != loc.size) {
331                 loc.size = size;
332                 mLocations.removeFromEnd(lastIndex - itemIndex);
333                 lastIndex = itemIndex;
334             }
335             mLastVisibleIndex = itemIndex;
336             if (mFirstVisibleIndex < 0) {
337                 mFirstVisibleIndex = itemIndex;
338             }
339             mProvider.addItem(mTmpItem[0], itemIndex, size, rowIndex, edge);
340             if (!oneColumnMode && checkAppendOverLimit(toLimit)) {
341                 return true;
342             }
343             if (edge == Integer.MAX_VALUE) {
344                 edge = mProvider.getEdge(itemIndex);
345             }
346             // Check limit after filled a full column
347             if (rowIndex == mNumRows - 1) {
348                 if (oneColumnMode) {
349                     return true;
350                 }
351             }
352         }
353         return false;
354     }
355 
356     /**
357      * algorithm of layout staggered grid, this method should only be called by
358      * appendVisibleItems().
359      */
appendVisibleItemsWithoutCache(int toLimit, boolean oneColumnMode)360     protected abstract boolean appendVisibleItemsWithoutCache(int toLimit, boolean oneColumnMode);
361 
362     /**
363      * Appends one visible item with new Location info.  Only called from
364      * appendVisibleItemsWithoutCache().
365      */
appendVisibleItemToRow(int itemIndex, int rowIndex, int location)366     protected final int appendVisibleItemToRow(int itemIndex, int rowIndex, int location) {
367         int offset;
368         if (mLastVisibleIndex >= 0) {
369             if (mLastVisibleIndex != getLastIndex() || mLastVisibleIndex != itemIndex - 1) {
370                 // should never hit this when we append a new item with a new Location object.
371                 throw new IllegalStateException();
372             }
373         }
374         if (mLastVisibleIndex < 0) {
375             // if we append first visible item after existing cached items,  we need update
376             // the offset later when prependVisbleItemsWithCache()
377             if (mLocations.size() > 0 && itemIndex == getLastIndex() + 1) {
378                 offset = calculateOffsetAfterLastItem(rowIndex);
379             } else {
380                 offset = 0;
381             }
382         } else {
383             offset = location - mProvider.getEdge(mLastVisibleIndex);
384         }
385         Location loc = new Location(rowIndex, offset, 0);
386         mLocations.addLast(loc);
387         Object item;
388         if (mPendingItem != null) {
389             loc.size = mPendingItemSize;
390             item = mPendingItem;
391             mPendingItem = null;
392         } else {
393             loc.size = mProvider.createItem(itemIndex, true, mTmpItem);
394             item = mTmpItem[0];
395         }
396         if (mLocations.size() == 1) {
397             mFirstIndex = mFirstVisibleIndex = mLastVisibleIndex = itemIndex;
398         } else {
399             if (mLastVisibleIndex < 0) {
400                 mFirstVisibleIndex = mLastVisibleIndex = itemIndex;
401             } else {
402                 mLastVisibleIndex++;
403             }
404         }
405         mProvider.addItem(item, itemIndex, loc.size, rowIndex, location);
406         return loc.size;
407     }
408 
409     @Override
getItemPositionsInRows(int startPos, int endPos)410     public final CircularIntArray[] getItemPositionsInRows(int startPos, int endPos) {
411         for (int i = 0; i < mNumRows; i++) {
412             mTmpItemPositionsInRows[i].clear();
413         }
414         if (startPos >= 0) {
415             for (int i = startPos; i <= endPos; i++) {
416                 CircularIntArray row = mTmpItemPositionsInRows[getLocation(i).row];
417                 if (row.size() > 0 && row.getLast() == i - 1) {
418                     // update continuous range
419                     row.popLast();
420                     row.addLast(i);
421                 } else {
422                     // add single position
423                     row.addLast(i);
424                     row.addLast(i);
425                 }
426             }
427         }
428         return mTmpItemPositionsInRows;
429     }
430 
431     @Override
invalidateItemsAfter(int index)432     public void invalidateItemsAfter(int index) {
433         super.invalidateItemsAfter(index);
434         mLocations.removeFromEnd(getLastIndex() - index + 1);
435         if (mLocations.size() == 0) {
436             mFirstIndex = -1;
437         }
438     }
439 
440 }
441