Home | History | Annotate | Download | only in widget
      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  */
     14 package androidx.leanback.widget;
     15 
     16 import android.util.SparseIntArray;
     17 
     18 import androidx.annotation.NonNull;
     19 import androidx.annotation.Nullable;
     20 import androidx.collection.CircularIntArray;
     21 import androidx.recyclerview.widget.RecyclerView;
     22 
     23 import java.io.PrintWriter;
     24 import java.util.Arrays;
     25 
     26 /**
     27  * A grid is representation of single or multiple rows layout data structure and algorithm.
     28  * Grid is the base class for single row, non-staggered grid and staggered grid.
     29  * <p>
     30  * To use the Grid, user must implement a Provider to create or remove visible item.
     31  * Grid maintains a list of visible items.  Visible items are created when
     32  * user calls appendVisibleItems() or prependVisibleItems() with certain limitation
     33  * (e.g. a max edge that append up to).  Visible items are deleted when user calls
     34  * removeInvisibleItemsAtEnd() or removeInvisibleItemsAtFront().  Grid's algorithm
     35  * uses size of visible item returned from Provider.createItem() to decide which row
     36  * to add a new visible item and may cache the algorithm results.   User must call
     37  * invalidateItemsAfter() when it detects item size changed to ask Grid to remove cached
     38  * results.
     39  */
     40 abstract class Grid {
     41 
     42     /**
     43      * A constant representing a default starting index, indicating that the
     44      * developer did not provide a start index.
     45      */
     46     public static final int START_DEFAULT = -1;
     47 
     48     Object[] mTmpItem = new Object[1];
     49 
     50     /**
     51      * When user uses Grid,  he should provide count of items and
     52      * the method to create item and remove item.
     53      */
     54     public static interface Provider {
     55 
     56         /**
     57          * Return how many items (are in the adapter).
     58          */
     59         int getCount();
     60 
     61         /**
     62          * @return Min index to prepend, usually it's 0; but in the preLayout case,
     63          * when grid was showing 5,6,7.  Removing 3,4,5 will make the layoutPosition to
     64          * be 3(deleted),4,5 in prelayout pass; Grid's index is still 5,6,7 in prelayout.
     65          * When we prepend in prelayout, we can call createItem(4), createItem(3), createItem(2),
     66          * the minimal index is 2, which is also the delta to mapping to layoutPosition in
     67          * prelayout pass.
     68          */
     69         int getMinIndex();
     70 
     71         /**
     72          * Create visible item and where the provider should measure it.
     73          * The call is always followed by addItem().
     74          * @param index     0-based index of the item in provider
     75          * @param append  True if new item is after last visible item, false if new item is
     76          *                before first visible item.
     77          * @param item    item[0] returns created item that will be passed in addItem() call.
     78          * @param disappearingItem The item is a disappearing item added by
     79          *                         {@link Grid#fillDisappearingItems(int[], int, SparseIntArray)}.
     80          *
     81          * @return length of the item.
     82          */
     83         int createItem(int index, boolean append, Object[] item, boolean disappearingItem);
     84 
     85         /**
     86          * add item to given row and given edge.  The call is always after createItem().
     87          * @param item      The object returned by createItem()
     88          * @param index     0-based index of the item in provider
     89          * @param length    The size of the object
     90          * @param rowIndex  Row index to put the item
     91          * @param edge      min_edge if not reversed or max_edge if reversed.
     92          */
     93         void addItem(Object item, int index, int length, int rowIndex, int edge);
     94 
     95         /**
     96          * Remove visible item at index.
     97          * @param index     0-based index of the item in provider
     98          */
     99         void removeItem(int index);
    100 
    101         /**
    102          * Get edge of an existing visible item. edge will be the min_edge
    103          * if not reversed or the max_edge if reversed.
    104          * @param index     0-based index of the item in provider
    105          */
    106         int getEdge(int index);
    107 
    108         /**
    109          * Get size of an existing visible item.
    110          * @param index     0-based index of the item in provider
    111          */
    112         int getSize(int index);
    113     }
    114 
    115     /**
    116      * Cached representation of an item in Grid.  May be subclassed.
    117      */
    118     public static class Location {
    119         /**
    120          * The index of the row for this Location.
    121          */
    122         public int row;
    123 
    124         public Location(int row) {
    125             this.row = row;
    126         }
    127     }
    128 
    129     protected Provider mProvider;
    130     protected boolean mReversedFlow;
    131     protected int mSpacing;
    132     protected int mNumRows;
    133     protected int mFirstVisibleIndex = -1;
    134     protected int mLastVisibleIndex = -1;
    135 
    136     protected CircularIntArray[] mTmpItemPositionsInRows;
    137 
    138     // the first index that grid will layout
    139     protected int mStartIndex = START_DEFAULT;
    140 
    141     /**
    142      * Creates a single or multiple rows (can be staggered or not staggered) grid
    143      */
    144     public static Grid createGrid(int rows) {
    145         Grid grid;
    146         if (rows == 1) {
    147             grid = new SingleRow();
    148         } else {
    149             // TODO support non staggered multiple rows grid
    150             grid = new StaggeredGridDefault();
    151             grid.setNumRows(rows);
    152         }
    153         return grid;
    154     }
    155 
    156     /**
    157      * Sets the space between items in a row
    158      */
    159     public final void setSpacing(int spacing) {
    160         mSpacing = spacing;
    161     }
    162 
    163     /**
    164      * Sets if reversed flow (rtl)
    165      */
    166     public final void setReversedFlow(boolean reversedFlow) {
    167         mReversedFlow = reversedFlow;
    168     }
    169 
    170     /**
    171      * Returns true if reversed flow (rtl)
    172      */
    173     public boolean isReversedFlow() {
    174         return mReversedFlow;
    175     }
    176 
    177     /**
    178      * Sets the {@link Provider} for this grid.
    179      *
    180      * @param provider The provider for this grid.
    181      */
    182     public void setProvider(Provider provider) {
    183         mProvider = provider;
    184     }
    185 
    186     /**
    187      * Sets the first item index to create when there are no items.
    188      *
    189      * @param startIndex the index of the first item
    190      */
    191     public void setStart(int startIndex) {
    192         mStartIndex = startIndex;
    193     }
    194 
    195     /**
    196      * Returns the number of rows in the grid.
    197      */
    198     public int getNumRows() {
    199         return mNumRows;
    200     }
    201 
    202     /**
    203      * Sets number of rows to fill into. For views that represent a
    204      * horizontal list, this will be the rows of the view. For views that
    205      * represent a vertical list, this will be the columns.
    206      *
    207      * @param numRows numberOfRows
    208      */
    209     void setNumRows(int numRows) {
    210         if (numRows <= 0) {
    211             throw new IllegalArgumentException();
    212         }
    213         if (mNumRows == numRows) {
    214             return;
    215         }
    216         mNumRows = numRows;
    217         mTmpItemPositionsInRows = new CircularIntArray[mNumRows];
    218         for (int i = 0; i < mNumRows; i++) {
    219             mTmpItemPositionsInRows[i] = new CircularIntArray();
    220         }
    221     }
    222 
    223     /**
    224      * Returns index of first visible item in the staggered grid.  Returns negative value
    225      * if no visible item.
    226      */
    227     public final int getFirstVisibleIndex() {
    228         return mFirstVisibleIndex;
    229     }
    230 
    231     /**
    232      * Returns index of last visible item in the staggered grid.  Returns negative value
    233      * if no visible item.
    234      */
    235     public final int getLastVisibleIndex() {
    236         return mLastVisibleIndex;
    237     }
    238 
    239     /**
    240      * Reset visible indices and keep cache (if exists)
    241      */
    242     public void resetVisibleIndex() {
    243         mFirstVisibleIndex = mLastVisibleIndex = -1;
    244     }
    245 
    246     /**
    247      * Invalidate items after or equal to index. This will remove visible items
    248      * after that and invalidate cache of layout results after that. Note that it's client's
    249      * responsibility to perform removing child action, {@link Provider#removeItem(int)} will not
    250      * be called because the index might be invalidated.
    251      */
    252     public void invalidateItemsAfter(int index) {
    253         if (index < 0) {
    254             return;
    255         }
    256         if (mLastVisibleIndex < 0) {
    257             return;
    258         }
    259         if (mLastVisibleIndex >= index) {
    260             mLastVisibleIndex = index - 1;
    261         }
    262         resetVisibleIndexIfEmpty();
    263         if (getFirstVisibleIndex() < 0) {
    264             setStart(index);
    265         }
    266     }
    267 
    268     /**
    269      * Gets the row index of item at given index.
    270      */
    271     public final int getRowIndex(int index) {
    272         Location location = getLocation(index);
    273         if (location == null) {
    274             return -1;
    275         }
    276         return location.row;
    277     }
    278 
    279     /**
    280      * Gets {@link Location} of item.  The return object is read only and temporarily.
    281      */
    282     public abstract Location getLocation(int index);
    283 
    284     /**
    285      * Finds the largest or smallest row min edge of visible items,
    286      * the row index is returned in indices[0], the item index is returned in indices[1].
    287      */
    288     public final int findRowMin(boolean findLarge, @Nullable int[] indices) {
    289         return findRowMin(findLarge, mReversedFlow ? mLastVisibleIndex : mFirstVisibleIndex,
    290                 indices);
    291     }
    292 
    293     /**
    294      * Finds the largest or smallest row min edge of visible items, starts searching from
    295      * indexLimit, the row index is returned in indices[0], the item index is returned in indices[1].
    296      */
    297     protected abstract int findRowMin(boolean findLarge, int indexLimit, int[] rowIndex);
    298 
    299     /**
    300      * Finds the largest or smallest row max edge of visible items, the row index is returned in
    301      * indices[0], the item index is returned in indices[1].
    302      */
    303     public final int findRowMax(boolean findLarge, @Nullable int[] indices) {
    304         return findRowMax(findLarge, mReversedFlow ? mFirstVisibleIndex : mLastVisibleIndex,
    305                 indices);
    306     }
    307 
    308     /**
    309      * Find largest or smallest row max edge of visible items, starts searching from indexLimit,
    310      * the row index is returned in indices[0], the item index is returned in indices[1].
    311      */
    312     protected abstract int findRowMax(boolean findLarge, int indexLimit, int[] indices);
    313 
    314     /**
    315      * Returns true if appending item has reached "toLimit"
    316      */
    317     protected final boolean checkAppendOverLimit(int toLimit) {
    318         if (mLastVisibleIndex < 0) {
    319             return false;
    320         }
    321         return mReversedFlow ? findRowMin(true, null) <= toLimit + mSpacing :
    322                     findRowMax(false, null) >= toLimit - mSpacing;
    323     }
    324 
    325     /**
    326      * Returns true if prepending item has reached "toLimit"
    327      */
    328     protected final boolean checkPrependOverLimit(int toLimit) {
    329         if (mLastVisibleIndex < 0) {
    330             return false;
    331         }
    332         return mReversedFlow ? findRowMax(false, null) >= toLimit - mSpacing :
    333                     findRowMin(true, null) <= toLimit + mSpacing;
    334     }
    335 
    336     /**
    337      * Return array of int array for all rows, each int array contains visible item positions
    338      * in pair on that row between startPos(included) and endPositions(included).
    339      * Returned value is read only, do not change it.
    340      * <p>
    341      * E.g. First row has 3,7,8, second row has 4,5,6.
    342      * getItemPositionsInRows(3, 8) returns { {3,3,7,8}, {4,6} }
    343      */
    344     public abstract CircularIntArray[] getItemPositionsInRows(int startPos, int endPos);
    345 
    346     /**
    347      * Return array of int array for all rows, each int array contains visible item positions
    348      * in pair on that row.
    349      * Returned value is read only, do not change it.
    350      * <p>
    351      * E.g. First row has 3,7,8, second row has 4,5,6  { {3,3,7,8}, {4,6} }
    352      */
    353     public final CircularIntArray[] getItemPositionsInRows() {
    354         return getItemPositionsInRows(getFirstVisibleIndex(), getLastVisibleIndex());
    355     }
    356 
    357     /**
    358      * Prepends items and stops after one column is filled.
    359      * (i.e. filled items from row 0 to row mNumRows - 1)
    360      * @return true if at least one item is filled.
    361      */
    362     public final boolean prependOneColumnVisibleItems() {
    363         return prependVisibleItems(mReversedFlow ? Integer.MIN_VALUE : Integer.MAX_VALUE, true);
    364     }
    365 
    366     /**
    367      * Prepends items until first item or reaches toLimit (min edge when not reversed or
    368      * max edge when reversed)
    369      */
    370     public final void prependVisibleItems(int toLimit) {
    371         prependVisibleItems(toLimit, false);
    372     }
    373 
    374     /**
    375      * Prepends items until first item or reaches toLimit (min edge when not reversed or
    376      * max edge when reversed).
    377      * @param oneColumnMode  true when fills one column and stops,  false
    378      * when checks if condition matches before filling first column.
    379      * @return true if at least one item is filled.
    380      */
    381     protected abstract boolean prependVisibleItems(int toLimit, boolean oneColumnMode);
    382 
    383     /**
    384      * Appends items and stops after one column is filled.
    385      * (i.e. filled items from row 0 to row mNumRows - 1)
    386      * @return true if at least one item is filled.
    387      */
    388     public boolean appendOneColumnVisibleItems() {
    389         return appendVisibleItems(mReversedFlow ? Integer.MAX_VALUE : Integer.MIN_VALUE, true);
    390     }
    391 
    392     /**
    393      * Append items until last item or reaches toLimit (max edge when not
    394      * reversed or min edge when reversed)
    395      */
    396     public final void appendVisibleItems(int toLimit) {
    397         appendVisibleItems(toLimit, false);
    398     }
    399 
    400     /**
    401      * Appends items until last or reaches toLimit (high edge when not
    402      * reversed or low edge when reversed).
    403      * @param oneColumnMode True when fills one column and stops,  false
    404      * when checks if condition matches before filling first column.
    405      * @return true if filled at least one item
    406      */
    407     protected abstract boolean appendVisibleItems(int toLimit, boolean oneColumnMode);
    408 
    409     /**
    410      * Removes invisible items from end until reaches item at aboveIndex or toLimit.
    411      * @param aboveIndex Don't remove items whose index is equals or smaller than aboveIndex
    412      * @param toLimit Don't remove items whose left edge is less than toLimit.
    413      */
    414     public void removeInvisibleItemsAtEnd(int aboveIndex, int toLimit) {
    415         while(mLastVisibleIndex >= mFirstVisibleIndex && mLastVisibleIndex > aboveIndex) {
    416             boolean offEnd = !mReversedFlow ? mProvider.getEdge(mLastVisibleIndex) >= toLimit
    417                     : mProvider.getEdge(mLastVisibleIndex) <= toLimit;
    418             if (offEnd) {
    419                 mProvider.removeItem(mLastVisibleIndex);
    420                 mLastVisibleIndex--;
    421             } else {
    422                 break;
    423             }
    424         }
    425         resetVisibleIndexIfEmpty();
    426     }
    427 
    428     /**
    429      * Removes invisible items from front until reaches item at belowIndex or toLimit.
    430      * @param belowIndex Don't remove items whose index is equals or larger than belowIndex
    431      * @param toLimit Don't remove items whose right edge is equals or greater than toLimit.
    432      */
    433     public void removeInvisibleItemsAtFront(int belowIndex, int toLimit) {
    434         while(mLastVisibleIndex >= mFirstVisibleIndex && mFirstVisibleIndex < belowIndex) {
    435             final int size = mProvider.getSize(mFirstVisibleIndex);
    436             boolean offFront = !mReversedFlow
    437                     ? mProvider.getEdge(mFirstVisibleIndex) + size <= toLimit
    438                     : mProvider.getEdge(mFirstVisibleIndex) - size >= toLimit;
    439             if (offFront) {
    440                 mProvider.removeItem(mFirstVisibleIndex);
    441                 mFirstVisibleIndex++;
    442             } else {
    443                 break;
    444             }
    445         }
    446         resetVisibleIndexIfEmpty();
    447     }
    448 
    449     private void resetVisibleIndexIfEmpty() {
    450         if (mLastVisibleIndex < mFirstVisibleIndex) {
    451             resetVisibleIndex();
    452         }
    453     }
    454 
    455     /**
    456      * Fill disappearing items, i.e. the items are moved out of window, we need give them final
    457      * location so recyclerview will run a slide out animation. The positions that was greater than
    458      * last visible index will be appended to end, the positions that was smaller than first visible
    459      * index will be prepend to beginning.
    460      * @param positions Sorted list of positions of disappearing items.
    461      * @param positionToRow Which row we want to put the disappearing item.
    462      */
    463     public void fillDisappearingItems(int[] positions, int positionsLength,
    464             SparseIntArray positionToRow) {
    465         final int lastPos = getLastVisibleIndex();
    466         final int resultSearchLast = lastPos >= 0
    467                 ? Arrays.binarySearch(positions, 0, positionsLength, lastPos) : 0;
    468         if (resultSearchLast < 0) {
    469             // we shouldn't find lastPos in disappearing position list.
    470             int firstDisappearingIndex = -resultSearchLast - 1;
    471             int edge;
    472             if (mReversedFlow) {
    473                 edge = mProvider.getEdge(lastPos) - mProvider.getSize(lastPos) - mSpacing;
    474             } else {
    475                 edge = mProvider.getEdge(lastPos) + mProvider.getSize(lastPos) + mSpacing;
    476             }
    477             for (int i = firstDisappearingIndex; i < positionsLength; i++) {
    478                 int disappearingIndex = positions[i];
    479                 int disappearingRow = positionToRow.get(disappearingIndex);
    480                 if (disappearingRow < 0) {
    481                     disappearingRow = 0; // if not found put in row 0
    482                 }
    483                 int size = mProvider.createItem(disappearingIndex, true, mTmpItem, true);
    484                 mProvider.addItem(mTmpItem[0], disappearingIndex, size, disappearingRow, edge);
    485                 if (mReversedFlow) {
    486                     edge = edge - size - mSpacing;
    487                 } else {
    488                     edge = edge + size + mSpacing;
    489                 }
    490             }
    491         }
    492 
    493         final int firstPos = getFirstVisibleIndex();
    494         final int resultSearchFirst = firstPos >= 0
    495                 ? Arrays.binarySearch(positions, 0, positionsLength, firstPos) : 0;
    496         if (resultSearchFirst < 0) {
    497             // we shouldn't find firstPos in disappearing position list.
    498             int firstDisappearingIndex = -resultSearchFirst - 2;
    499             int edge;
    500             if (mReversedFlow) {
    501                 edge = mProvider.getEdge(firstPos);
    502             } else {
    503                 edge = mProvider.getEdge(firstPos);
    504             }
    505             for (int i = firstDisappearingIndex; i >= 0; i--) {
    506                 int disappearingIndex = positions[i];
    507                 int disappearingRow = positionToRow.get(disappearingIndex);
    508                 if (disappearingRow < 0) {
    509                     disappearingRow = 0; // if not found put in row 0
    510                 }
    511                 int size = mProvider.createItem(disappearingIndex, false, mTmpItem, true);
    512                 if (mReversedFlow) {
    513                     edge = edge + mSpacing + size;
    514                 } else {
    515                     edge = edge - mSpacing - size;
    516                 }
    517                 mProvider.addItem(mTmpItem[0], disappearingIndex, size, disappearingRow, edge);
    518             }
    519         }
    520     }
    521 
    522     /**
    523      * Queries items adjacent to the viewport (in the direction of da) into the prefetch registry.
    524      */
    525     public void collectAdjacentPrefetchPositions(int fromLimit, int da,
    526             @NonNull RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry) {
    527     }
    528 
    529     public abstract void debugPrint(PrintWriter pw);
    530 }
    531