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;
17import android.support.v4.util.CircularIntArray;
18
19import java.io.PrintWriter;
20import java.util.ArrayList;
21import 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 */
41abstract 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
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     */
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     */
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     */
96    public final int getSize() {
97        return mLocations.size();
98    }
99
100    @Override
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
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
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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
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
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