blob: 4b5abe8037100974f047c5a37e6eb9716d0ab47a [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 android.support.v17.leanback.widget;
import android.support.v4.util.CircularArray;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
/**
* A dynamic data structure that maintains staggered grid position information
* for each individual child. The algorithm ensures that each row will be kept
* as balanced as possible when prepending and appending a child.
*
* <p>
* You may keep view {@link StaggeredGrid.Location} inside StaggeredGrid as much
* as possible since prepending and appending views is not symmetric: layout
* going from 0 to N will likely produce a different result than layout going
* from N to 0 for the staggered cases. If a user scrolls from 0 to N then
* scrolls back to 0 and we don't keep history location information, edges of
* the very beginning of rows will not be aligned. It is recommended to keep a
* list of tens of thousands of {@link StaggeredGrid.Location}s which will be
* big enough to remember a typical user's scroll history. There are situations
* where StaggeredGrid falls back to the simple case where we do not need save a
* huge list of locations inside StaggeredGrid:
* <ul>
* <li>Only one row (e.g., a single row listview)</li>
* <li> Each item has the same length (not staggered at all)</li>
* </ul>
*
* <p>
* This class is abstract and can be replaced with different implementations.
*/
abstract class StaggeredGrid {
/**
* TODO: document this
*/
public static interface Provider {
/**
* Return how many items are in the adapter.
*/
public abstract int getCount();
/**
* Create the object at a given row.
*/
public abstract void createItem(int index, int row, boolean append);
}
/**
* Location of an item in the grid. For now it only saves row index but
* more information may be added in the future.
*/
public final static class Location {
/**
* The index of the row for this Location.
*/
public final int row;
/**
* Create a Location with the given row index.
*/
public Location(int row) {
this.row = row;
}
}
/**
* TODO: document this
*/
public final static class Row {
/**
* first view start location
*/
public int low;
/**
* last view end location
*/
public int high;
}
protected Provider mProvider;
protected int mNumRows = 1; // mRows.length
protected Row[] mRows;
protected CircularArray<Location> mLocations = new CircularArray<Location>(64);
private ArrayList<Integer>[] mTmpItemPositionsInRows;
protected boolean mReversedFlow;
/**
* A constant representing a default starting index, indicating that the
* developer did not provide a start index.
*/
public static final int START_DEFAULT = -1;
// the first index that grid will layout
protected int mStartIndex = START_DEFAULT;
// the row to layout the first index
protected int mStartRow = START_DEFAULT;
protected int mFirstIndex = -1;
public void setReversedFlow(boolean reversedFlow) {
mReversedFlow = reversedFlow;
}
/**
* Sets the {@link Provider} for this staggered grid.
*
* @param provider The provider for this staggered grid.
*/
public void setProvider(Provider provider) {
mProvider = provider;
}
/**
* Sets the array of {@link Row}s 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 row The array of {@link Row}s to be filled.
*/
public final void setRows(Row[] row) {
if (row == null || row.length == 0) {
throw new IllegalArgumentException();
}
mNumRows = row.length;
mRows = row;
mTmpItemPositionsInRows = new ArrayList[mNumRows];
for (int i = 0; i < mNumRows; i++) {
mTmpItemPositionsInRows[i] = new ArrayList(32);
}
}
/**
* Returns the number of rows in the staggered grid.
*/
public final int getNumRows() {
return mNumRows;
}
/**
* Set the first item index and the row index to load when there are no
* items.
*
* @param startIndex the index of the first item
* @param startRow the index of the row
*/
public final void setStart(int startIndex, int startRow) {
mStartIndex = startIndex;
mStartRow = startRow;
}
/**
* Returns the first index in the staggered grid.
*/
public final int getFirstIndex() {
return mFirstIndex;
}
/**
* Returns the last index in the staggered grid.
*/
public final int getLastIndex() {
return mFirstIndex + mLocations.size() - 1;
}
/**
* Returns the size of the saved {@link Location}s.
*/
public final int getSize() {
return mLocations.size();
}
/**
* Returns the {@link Location} at the given index.
*/
public final Location getLocation(int index) {
if (mLocations.size() == 0) {
return null;
}
return mLocations.get(index - mFirstIndex);
}
/**
* Removes the first element.
*/
public final void removeFirst() {
mFirstIndex++;
mLocations.popFirst();
}
/**
* Removes the last element.
*/
public final void removeLast() {
mLocations.popLast();
}
public final void debugPrint(PrintWriter pw) {
for (int i = 0, size = mLocations.size(); i < size; i++) {
Location loc = mLocations.get(i);
pw.print("<" + (mFirstIndex + i) + "," + loc.row + ">");
pw.print(" ");
pw.println();
}
}
protected final int getMaxHighRowIndex() {
int maxHighRowIndex = 0;
for (int i = 1; i < mNumRows; i++) {
if (mRows[i].high > mRows[maxHighRowIndex].high) {
maxHighRowIndex = i;
}
}
return maxHighRowIndex;
}
protected final int getMinHighRowIndex() {
int minHighRowIndex = 0;
for (int i = 1; i < mNumRows; i++) {
if (mRows[i].high < mRows[minHighRowIndex].high) {
minHighRowIndex = i;
}
}
return minHighRowIndex;
}
protected final Location appendItemToRow(int itemIndex, int rowIndex) {
Location loc = new Location(rowIndex);
if (mLocations.size() == 0) {
mFirstIndex = itemIndex;
}
mLocations.addLast(loc);
mProvider.createItem(itemIndex, rowIndex, true);
return loc;
}
/**
* Append items until the high edge reaches upTo.
*/
public abstract void appendItems(int toLimit);
protected final int getMaxLowRowIndex() {
int maxLowRowIndex = 0;
for (int i = 1; i < mNumRows; i++) {
if (mRows[i].low > mRows[maxLowRowIndex].low) {
maxLowRowIndex = i;
}
}
return maxLowRowIndex;
}
protected final int getMinLowRowIndex() {
int minLowRowIndex = 0;
for (int i = 1; i < mNumRows; i++) {
if (mRows[i].low < mRows[minLowRowIndex].low) {
minLowRowIndex = i;
}
}
return minLowRowIndex;
}
protected final Location prependItemToRow(int itemIndex, int rowIndex) {
Location loc = new Location(rowIndex);
mFirstIndex = itemIndex;
mLocations.addFirst(loc);
mProvider.createItem(itemIndex, rowIndex, false);
return loc;
}
/**
* Return array of Lists for all rows, each List contains item positions
* on that row between startPos(included) and endPositions(included).
* Returned value is read only, do not change it.
*/
public final List<Integer>[] getItemPositionsInRows(int startPos, int endPos) {
for (int i = 0; i < mNumRows; i++) {
mTmpItemPositionsInRows[i].clear();
}
if (startPos >= 0) {
for (int i = startPos; i <= endPos; i++) {
mTmpItemPositionsInRows[getLocation(i).row].add(i);
}
}
return mTmpItemPositionsInRows;
}
/**
* Prepend items until the low edge reaches downTo.
*/
public abstract void prependItems(int toLimit);
/**
* Strip items, keep a contiguous subset of items; the subset should include
* at least one item on every row that currently has at least one item.
*
* <p>
* TODO: document this better
*/
public abstract void stripDownTo(int itemIndex);
}