blob: 9608fecd53cf05560381e442a1e466ae03472c75 [file] [log] [blame]
/*
* 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 androidx.leanback.widget;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.CircularIntArray;
import androidx.recyclerview.widget.RecyclerView;
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);
}