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