| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
| * in compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package android.support.v17.leanback.widget; |
| |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.v4.util.CircularIntArray; |
| import android.support.v7.widget.RecyclerView; |
| import android.util.SparseIntArray; |
| |
| import java.io.PrintWriter; |
| import java.util.Arrays; |
| |
| /** |
| * A grid is representation of single or multiple rows layout data structure and algorithm. |
| * Grid is the base class for single row, non-staggered grid and staggered grid. |
| * <p> |
| * To use the Grid, user must implement a Provider to create or remove visible item. |
| * Grid maintains a list of visible items. Visible items are created when |
| * user calls appendVisibleItems() or prependVisibleItems() with certain limitation |
| * (e.g. a max edge that append up to). Visible items are deleted when user calls |
| * removeInvisibleItemsAtEnd() or removeInvisibleItemsAtFront(). Grid's algorithm |
| * uses size of visible item returned from Provider.createItem() to decide which row |
| * to add a new visible item and may cache the algorithm results. User must call |
| * invalidateItemsAfter() when it detects item size changed to ask Grid to remove cached |
| * results. |
| */ |
| abstract class Grid { |
| |
| /** |
| * A constant representing a default starting index, indicating that the |
| * developer did not provide a start index. |
| */ |
| public static final int START_DEFAULT = -1; |
| |
| Object[] mTmpItem = new Object[1]; |
| |
| /** |
| * When user uses Grid, he should provide count of items and |
| * the method to create item and remove item. |
| */ |
| public static interface Provider { |
| |
| /** |
| * Return how many items (are in the adapter). |
| */ |
| int getCount(); |
| |
| /** |
| * @return Min index to prepend, usually it's 0; but in the preLayout case, |
| * when grid was showing 5,6,7. Removing 3,4,5 will make the layoutPosition to |
| * be 3(deleted),4,5 in prelayout pass; Grid's index is still 5,6,7 in prelayout. |
| * When we prepend in prelayout, we can call createItem(4), createItem(3), createItem(2), |
| * the minimal index is 2, which is also the delta to mapping to layoutPosition in |
| * prelayout pass. |
| */ |
| int getMinIndex(); |
| |
| /** |
| * Create visible item and where the provider should measure it. |
| * The call is always followed by addItem(). |
| * @param index 0-based index of the item in provider |
| * @param append True if new item is after last visible item, false if new item is |
| * before first visible item. |
| * @param item item[0] returns created item that will be passed in addItem() call. |
| * @param disappearingItem The item is a disappearing item added by |
| * {@link Grid#fillDisappearingItems(int[], int, SparseIntArray)}. |
| * |
| * @return length of the item. |
| */ |
| int createItem(int index, boolean append, Object[] item, boolean disappearingItem); |
| |
| /** |
| * add item to given row and given edge. The call is always after createItem(). |
| * @param item The object returned by createItem() |
| * @param index 0-based index of the item in provider |
| * @param length The size of the object |
| * @param rowIndex Row index to put the item |
| * @param edge min_edge if not reversed or max_edge if reversed. |
| */ |
| void addItem(Object item, int index, int length, int rowIndex, int edge); |
| |
| /** |
| * Remove visible item at index. |
| * @param index 0-based index of the item in provider |
| */ |
| void removeItem(int index); |
| |
| /** |
| * Get edge of an existing visible item. edge will be the min_edge |
| * if not reversed or the max_edge if reversed. |
| * @param index 0-based index of the item in provider |
| */ |
| int getEdge(int index); |
| |
| /** |
| * Get size of an existing visible item. |
| * @param index 0-based index of the item in provider |
| */ |
| int getSize(int index); |
| } |
| |
| /** |
| * Cached representation of an item in Grid. May be subclassed. |
| */ |
| public static class Location { |
| /** |
| * The index of the row for this Location. |
| */ |
| public int row; |
| |
| public Location(int row) { |
| this.row = row; |
| } |
| } |
| |
| protected Provider mProvider; |
| protected boolean mReversedFlow; |
| protected int mSpacing; |
| protected int mNumRows; |
| protected int mFirstVisibleIndex = -1; |
| protected int mLastVisibleIndex = -1; |
| |
| protected CircularIntArray[] mTmpItemPositionsInRows; |
| |
| // the first index that grid will layout |
| protected int mStartIndex = START_DEFAULT; |
| |
| /** |
| * Creates a single or multiple rows (can be staggered or not staggered) grid |
| */ |
| public static Grid createGrid(int rows) { |
| Grid grid; |
| if (rows == 1) { |
| grid = new SingleRow(); |
| } else { |
| // TODO support non staggered multiple rows grid |
| grid = new StaggeredGridDefault(); |
| grid.setNumRows(rows); |
| } |
| return grid; |
| } |
| |
| /** |
| * Sets the space between items in a row |
| */ |
| public final void setSpacing(int spacing) { |
| mSpacing = spacing; |
| } |
| |
| /** |
| * Sets if reversed flow (rtl) |
| */ |
| public final void setReversedFlow(boolean reversedFlow) { |
| mReversedFlow = reversedFlow; |
| } |
| |
| /** |
| * Returns true if reversed flow (rtl) |
| */ |
| public boolean isReversedFlow() { |
| return mReversedFlow; |
| } |
| |
| /** |
| * Sets the {@link Provider} for this grid. |
| * |
| * @param provider The provider for this grid. |
| */ |
| public void setProvider(Provider provider) { |
| mProvider = provider; |
| } |
| |
| /** |
| * Sets the first item index to create when there are no items. |
| * |
| * @param startIndex the index of the first item |
| */ |
| public void setStart(int startIndex) { |
| mStartIndex = startIndex; |
| } |
| |
| /** |
| * Returns the number of rows in the grid. |
| */ |
| public int getNumRows() { |
| return mNumRows; |
| } |
| |
| /** |
| * Sets number of rows to fill into. For views that represent a |
| * horizontal list, this will be the rows of the view. For views that |
| * represent a vertical list, this will be the columns. |
| * |
| * @param numRows numberOfRows |
| */ |
| void setNumRows(int numRows) { |
| if (numRows <= 0) { |
| throw new IllegalArgumentException(); |
| } |
| if (mNumRows == numRows) { |
| return; |
| } |
| mNumRows = numRows; |
| mTmpItemPositionsInRows = new CircularIntArray[mNumRows]; |
| for (int i = 0; i < mNumRows; i++) { |
| mTmpItemPositionsInRows[i] = new CircularIntArray(); |
| } |
| } |
| |
| /** |
| * Returns index of first visible item in the staggered grid. Returns negative value |
| * if no visible item. |
| */ |
| public final int getFirstVisibleIndex() { |
| return mFirstVisibleIndex; |
| } |
| |
| /** |
| * Returns index of last visible item in the staggered grid. Returns negative value |
| * if no visible item. |
| */ |
| public final int getLastVisibleIndex() { |
| return mLastVisibleIndex; |
| } |
| |
| /** |
| * Reset visible indices and keep cache (if exists) |
| */ |
| public void resetVisibleIndex() { |
| mFirstVisibleIndex = mLastVisibleIndex = -1; |
| } |
| |
| /** |
| * Invalidate items after or equal to index. This will remove visible items |
| * after that and invalidate cache of layout results after that. Note that it's client's |
| * responsibility to perform removing child action, {@link Provider#removeItem(int)} will not |
| * be called because the index might be invalidated. |
| */ |
| public void invalidateItemsAfter(int index) { |
| if (index < 0) { |
| return; |
| } |
| if (mLastVisibleIndex < 0) { |
| return; |
| } |
| if (mLastVisibleIndex >= index) { |
| mLastVisibleIndex = index - 1; |
| } |
| resetVisibleIndexIfEmpty(); |
| if (getFirstVisibleIndex() < 0) { |
| setStart(index); |
| } |
| } |
| |
| /** |
| * Gets the row index of item at given index. |
| */ |
| public final int getRowIndex(int index) { |
| Location location = getLocation(index); |
| if (location == null) { |
| return -1; |
| } |
| return location.row; |
| } |
| |
| /** |
| * Gets {@link Location} of item. The return object is read only and temporarily. |
| */ |
| public abstract Location getLocation(int index); |
| |
| /** |
| * Finds the largest or smallest row min edge of visible items, |
| * the row index is returned in indices[0], the item index is returned in indices[1]. |
| */ |
| public final int findRowMin(boolean findLarge, @Nullable int[] indices) { |
| return findRowMin(findLarge, mReversedFlow ? mLastVisibleIndex : mFirstVisibleIndex, |
| indices); |
| } |
| |
| /** |
| * Finds the largest or smallest row min edge of visible items, starts searching from |
| * indexLimit, the row index is returned in indices[0], the item index is returned in indices[1]. |
| */ |
| protected abstract int findRowMin(boolean findLarge, int indexLimit, int[] rowIndex); |
| |
| /** |
| * Finds the largest or smallest row max edge of visible items, the row index is returned in |
| * indices[0], the item index is returned in indices[1]. |
| */ |
| public final int findRowMax(boolean findLarge, @Nullable int[] indices) { |
| return findRowMax(findLarge, mReversedFlow ? mFirstVisibleIndex : mLastVisibleIndex, |
| indices); |
| } |
| |
| /** |
| * Find largest or smallest row max edge of visible items, starts searching from indexLimit, |
| * the row index is returned in indices[0], the item index is returned in indices[1]. |
| */ |
| protected abstract int findRowMax(boolean findLarge, int indexLimit, int[] indices); |
| |
| /** |
| * Returns true if appending item has reached "toLimit" |
| */ |
| protected final boolean checkAppendOverLimit(int toLimit) { |
| if (mLastVisibleIndex < 0) { |
| return false; |
| } |
| return mReversedFlow ? findRowMin(true, null) <= toLimit + mSpacing : |
| findRowMax(false, null) >= toLimit - mSpacing; |
| } |
| |
| /** |
| * Returns true if prepending item has reached "toLimit" |
| */ |
| protected final boolean checkPrependOverLimit(int toLimit) { |
| if (mLastVisibleIndex < 0) { |
| return false; |
| } |
| return mReversedFlow ? findRowMax(false, null) >= toLimit - mSpacing : |
| findRowMin(true, null) <= toLimit + mSpacing; |
| } |
| |
| /** |
| * Return array of int array for all rows, each int array contains visible item positions |
| * in pair on that row between startPos(included) and endPositions(included). |
| * Returned value is read only, do not change it. |
| * <p> |
| * E.g. First row has 3,7,8, second row has 4,5,6. |
| * getItemPositionsInRows(3, 8) returns { {3,3,7,8}, {4,6} } |
| */ |
| public abstract CircularIntArray[] getItemPositionsInRows(int startPos, int endPos); |
| |
| /** |
| * Return array of int array for all rows, each int array contains visible item positions |
| * in pair on that row. |
| * Returned value is read only, do not change it. |
| * <p> |
| * E.g. First row has 3,7,8, second row has 4,5,6 { {3,3,7,8}, {4,6} } |
| */ |
| public final CircularIntArray[] getItemPositionsInRows() { |
| return getItemPositionsInRows(getFirstVisibleIndex(), getLastVisibleIndex()); |
| } |
| |
| /** |
| * Prepends items and stops after one column is filled. |
| * (i.e. filled items from row 0 to row mNumRows - 1) |
| * @return true if at least one item is filled. |
| */ |
| public final boolean prependOneColumnVisibleItems() { |
| return prependVisibleItems(mReversedFlow ? Integer.MIN_VALUE : Integer.MAX_VALUE, true); |
| } |
| |
| /** |
| * Prepends items until first item or reaches toLimit (min edge when not reversed or |
| * max edge when reversed) |
| */ |
| public final void prependVisibleItems(int toLimit) { |
| prependVisibleItems(toLimit, false); |
| } |
| |
| /** |
| * Prepends items until first item or reaches toLimit (min edge when not reversed or |
| * max edge when reversed). |
| * @param oneColumnMode true when fills one column and stops, false |
| * when checks if condition matches before filling first column. |
| * @return true if at least one item is filled. |
| */ |
| protected abstract boolean prependVisibleItems(int toLimit, boolean oneColumnMode); |
| |
| /** |
| * Appends items and stops after one column is filled. |
| * (i.e. filled items from row 0 to row mNumRows - 1) |
| * @return true if at least one item is filled. |
| */ |
| public boolean appendOneColumnVisibleItems() { |
| return appendVisibleItems(mReversedFlow ? Integer.MAX_VALUE : Integer.MIN_VALUE, true); |
| } |
| |
| /** |
| * Append items until last item or reaches toLimit (max edge when not |
| * reversed or min edge when reversed) |
| */ |
| public final void appendVisibleItems(int toLimit) { |
| appendVisibleItems(toLimit, false); |
| } |
| |
| /** |
| * Appends items until last or reaches toLimit (high edge when not |
| * reversed or low edge when reversed). |
| * @param oneColumnMode True when fills one column and stops, false |
| * when checks if condition matches before filling first column. |
| * @return true if filled at least one item |
| */ |
| protected abstract boolean appendVisibleItems(int toLimit, boolean oneColumnMode); |
| |
| /** |
| * Removes invisible items from end until reaches item at aboveIndex or toLimit. |
| * @param aboveIndex Don't remove items whose index is equals or smaller than aboveIndex |
| * @param toLimit Don't remove items whose left edge is less than toLimit. |
| */ |
| public void removeInvisibleItemsAtEnd(int aboveIndex, int toLimit) { |
| while(mLastVisibleIndex >= mFirstVisibleIndex && mLastVisibleIndex > aboveIndex) { |
| boolean offEnd = !mReversedFlow ? mProvider.getEdge(mLastVisibleIndex) >= toLimit |
| : mProvider.getEdge(mLastVisibleIndex) <= toLimit; |
| if (offEnd) { |
| mProvider.removeItem(mLastVisibleIndex); |
| mLastVisibleIndex--; |
| } else { |
| break; |
| } |
| } |
| resetVisibleIndexIfEmpty(); |
| } |
| |
| /** |
| * Removes invisible items from front until reaches item at belowIndex or toLimit. |
| * @param belowIndex Don't remove items whose index is equals or larger than belowIndex |
| * @param toLimit Don't remove items whose right edge is equals or greater than toLimit. |
| */ |
| public void removeInvisibleItemsAtFront(int belowIndex, int toLimit) { |
| while(mLastVisibleIndex >= mFirstVisibleIndex && mFirstVisibleIndex < belowIndex) { |
| final int size = mProvider.getSize(mFirstVisibleIndex); |
| boolean offFront = !mReversedFlow |
| ? mProvider.getEdge(mFirstVisibleIndex) + size <= toLimit |
| : mProvider.getEdge(mFirstVisibleIndex) - size >= toLimit; |
| if (offFront) { |
| mProvider.removeItem(mFirstVisibleIndex); |
| mFirstVisibleIndex++; |
| } else { |
| break; |
| } |
| } |
| resetVisibleIndexIfEmpty(); |
| } |
| |
| private void resetVisibleIndexIfEmpty() { |
| if (mLastVisibleIndex < mFirstVisibleIndex) { |
| resetVisibleIndex(); |
| } |
| } |
| |
| /** |
| * Fill disappearing items, i.e. the items are moved out of window, we need give them final |
| * location so recyclerview will run a slide out animation. The positions that was greater than |
| * last visible index will be appended to end, the positions that was smaller than first visible |
| * index will be prepend to beginning. |
| * @param positions Sorted list of positions of disappearing items. |
| * @param positionToRow Which row we want to put the disappearing item. |
| */ |
| public void fillDisappearingItems(int[] positions, int positionsLength, |
| SparseIntArray positionToRow) { |
| final int lastPos = getLastVisibleIndex(); |
| final int resultSearchLast = lastPos >= 0 |
| ? Arrays.binarySearch(positions, 0, positionsLength, lastPos) : 0; |
| if (resultSearchLast < 0) { |
| // we shouldn't find lastPos in disappearing position list. |
| int firstDisappearingIndex = -resultSearchLast - 1; |
| int edge; |
| if (mReversedFlow) { |
| edge = mProvider.getEdge(lastPos) - mProvider.getSize(lastPos) - mSpacing; |
| } else { |
| edge = mProvider.getEdge(lastPos) + mProvider.getSize(lastPos) + mSpacing; |
| } |
| for (int i = firstDisappearingIndex; i < positionsLength; i++) { |
| int disappearingIndex = positions[i]; |
| int disappearingRow = positionToRow.get(disappearingIndex); |
| if (disappearingRow < 0) { |
| disappearingRow = 0; // if not found put in row 0 |
| } |
| int size = mProvider.createItem(disappearingIndex, true, mTmpItem, true); |
| mProvider.addItem(mTmpItem[0], disappearingIndex, size, disappearingRow, edge); |
| if (mReversedFlow) { |
| edge = edge - size - mSpacing; |
| } else { |
| edge = edge + size + mSpacing; |
| } |
| } |
| } |
| |
| final int firstPos = getFirstVisibleIndex(); |
| final int resultSearchFirst = firstPos >= 0 |
| ? Arrays.binarySearch(positions, 0, positionsLength, firstPos) : 0; |
| if (resultSearchFirst < 0) { |
| // we shouldn't find firstPos in disappearing position list. |
| int firstDisappearingIndex = -resultSearchFirst - 2; |
| int edge; |
| if (mReversedFlow) { |
| edge = mProvider.getEdge(firstPos); |
| } else { |
| edge = mProvider.getEdge(firstPos); |
| } |
| for (int i = firstDisappearingIndex; i >= 0; i--) { |
| int disappearingIndex = positions[i]; |
| int disappearingRow = positionToRow.get(disappearingIndex); |
| if (disappearingRow < 0) { |
| disappearingRow = 0; // if not found put in row 0 |
| } |
| int size = mProvider.createItem(disappearingIndex, false, mTmpItem, true); |
| if (mReversedFlow) { |
| edge = edge + mSpacing + size; |
| } else { |
| edge = edge - mSpacing - size; |
| } |
| mProvider.addItem(mTmpItem[0], disappearingIndex, size, disappearingRow, edge); |
| } |
| } |
| } |
| |
| /** |
| * Queries items adjacent to the viewport (in the direction of da) into the prefetch registry. |
| */ |
| public void collectAdjacentPrefetchPositions(int fromLimit, int da, |
| @NonNull RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry) { |
| } |
| |
| public abstract void debugPrint(PrintWriter pw); |
| } |