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