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 */ getCount()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 */ getMinIndex()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 */ createItem(int index, boolean append, Object[] item, boolean disappearingItem)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 */ addItem(Object item, int index, int length, int rowIndex, int edge)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 */ removeItem(int index)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 */ getEdge(int index)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 */ getSize(int index)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 Location(int row)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 */ createGrid(int rows)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 */ setSpacing(int spacing)159 public final void setSpacing(int spacing) { 160 mSpacing = spacing; 161 } 162 163 /** 164 * Sets if reversed flow (rtl) 165 */ setReversedFlow(boolean reversedFlow)166 public final void setReversedFlow(boolean reversedFlow) { 167 mReversedFlow = reversedFlow; 168 } 169 170 /** 171 * Returns true if reversed flow (rtl) 172 */ isReversedFlow()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 */ setProvider(Provider provider)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 */ setStart(int startIndex)191 public void setStart(int startIndex) { 192 mStartIndex = startIndex; 193 } 194 195 /** 196 * Returns the number of rows in the grid. 197 */ getNumRows()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 */ setNumRows(int numRows)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 */ getFirstVisibleIndex()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 */ getLastVisibleIndex()235 public final int getLastVisibleIndex() { 236 return mLastVisibleIndex; 237 } 238 239 /** 240 * Reset visible indices and keep cache (if exists) 241 */ resetVisibleIndex()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 */ invalidateItemsAfter(int index)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 */ getRowIndex(int index)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 */ getLocation(int index)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 */ findRowMin(boolean findLarge, @Nullable int[] indices)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 */ findRowMin(boolean findLarge, int indexLimit, int[] rowIndex)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 */ findRowMax(boolean findLarge, @Nullable int[] indices)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 */ findRowMax(boolean findLarge, int indexLimit, int[] indices)312 protected abstract int findRowMax(boolean findLarge, int indexLimit, int[] indices); 313 314 /** 315 * Returns true if appending item has reached "toLimit" 316 */ checkAppendOverLimit(int toLimit)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 */ checkPrependOverLimit(int toLimit)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 */ getItemPositionsInRows(int startPos, int endPos)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 */ getItemPositionsInRows()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 */ prependOneColumnVisibleItems()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 */ prependVisibleItems(int toLimit)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 */ prependVisibleItems(int toLimit, boolean oneColumnMode)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 */ appendOneColumnVisibleItems()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 */ appendVisibleItems(int toLimit)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 */ appendVisibleItems(int toLimit, boolean oneColumnMode)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 */ removeInvisibleItemsAtEnd(int aboveIndex, int toLimit)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 */ removeInvisibleItemsAtFront(int belowIndex, int toLimit)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 resetVisibleIndexIfEmpty()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 */ fillDisappearingItems(int[] positions, int positionsLength, SparseIntArray positionToRow)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 */ collectAdjacentPrefetchPositions(int fromLimit, int da, @NonNull RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry)525 public void collectAdjacentPrefetchPositions(int fromLimit, int da, 526 @NonNull RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry) { 527 } 528 debugPrint(PrintWriter pw)529 public abstract void debugPrint(PrintWriter pw); 530 } 531