/* * 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. *

* 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. *

* 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. *

* 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); }