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