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 androidx.collection.CircularArray; 17 import androidx.collection.CircularIntArray; 18 19 import java.io.PrintWriter; 20 21 /** 22 * A dynamic data structure that caches staggered grid position information 23 * for each individual child. The algorithm ensures that each row will be kept 24 * as balanced as possible when prepending and appending a child. 25 * 26 * <p> 27 * You may keep view {@link StaggeredGrid.Location} inside StaggeredGrid as much 28 * as possible since prepending and appending views is not symmetric: layout 29 * going from 0 to N will likely produce a different result than layout going 30 * from N to 0 for the staggered cases. If a user scrolls from 0 to N then 31 * scrolls back to 0 and we don't keep history location information, edges of 32 * the very beginning of rows will not be aligned. It is recommended to keep a 33 * list of tens of thousands of {@link StaggeredGrid.Location}s which will be 34 * big enough to remember a typical user's scroll history. 35 * 36 * <p> 37 * This class is abstract and can be replaced with different implementations. 38 */ 39 abstract class StaggeredGrid extends Grid { 40 41 /** 42 * Cached representation of Staggered item. 43 */ 44 public static class Location extends Grid.Location { 45 /** 46 * Offset to previous item location. 47 * min_edge(index) - min_edge(index - 1) for non reversed case 48 * max_edge(index) - max_edge(index - 1) for reversed case 49 */ 50 public int offset; 51 52 /** 53 * size of the item. 54 */ 55 public int size; 56 Location(int row, int offset, int size)57 public Location(int row, int offset, int size) { 58 super(row); 59 this.offset = offset; 60 this.size = size; 61 } 62 } 63 64 protected CircularArray<Location> mLocations = new CircularArray<Location>(64); 65 66 // mFirstIndex <= mFirstVisibleIndex <= mLastVisibleIndex 67 // <= mFirstIndex + mLocations.size() - 1 68 protected int mFirstIndex = -1; 69 70 protected Object mPendingItem; 71 protected int mPendingItemSize; 72 73 /** 74 * Returns index of first item (cached or visible) in the staggered grid. 75 * Returns negative value if no item. 76 */ getFirstIndex()77 public final int getFirstIndex() { 78 return mFirstIndex; 79 } 80 81 /** 82 * Returns index of last item (cached or visible) in the staggered grid. 83 * Returns negative value if no item. 84 */ getLastIndex()85 public final int getLastIndex() { 86 return mFirstIndex + mLocations.size() - 1; 87 } 88 89 /** 90 * Returns the size of the saved {@link Location}s. 91 */ getSize()92 public final int getSize() { 93 return mLocations.size(); 94 } 95 96 @Override getLocation(int index)97 public final Location getLocation(int index) { 98 final int indexInArray = index - mFirstIndex; 99 if (indexInArray < 0 || indexInArray >= mLocations.size()) { 100 return null; 101 } 102 return mLocations.get(indexInArray); 103 } 104 105 @Override debugPrint(PrintWriter pw)106 public final void debugPrint(PrintWriter pw) { 107 for (int i = 0, size = mLocations.size(); i < size; i++) { 108 Location loc = mLocations.get(i); 109 pw.print("<" + (mFirstIndex + i) + "," + loc.row + ">"); 110 pw.print(" "); 111 pw.println(); 112 } 113 } 114 115 @Override prependVisibleItems(int toLimit, boolean oneColumnMode)116 protected final boolean prependVisibleItems(int toLimit, boolean oneColumnMode) { 117 if (mProvider.getCount() == 0) { 118 return false; 119 } 120 if (!oneColumnMode && checkPrependOverLimit(toLimit)) { 121 return false; 122 } 123 try { 124 if (prependVisbleItemsWithCache(toLimit, oneColumnMode)) { 125 return true; 126 } 127 return prependVisibleItemsWithoutCache(toLimit, oneColumnMode); 128 } finally { 129 mTmpItem[0] = null; 130 mPendingItem = null; 131 } 132 } 133 134 /** 135 * Prepends items using cached locations, returning true if toLimit is reached. 136 * This method should only be called by prependVisibleItems(). 137 */ prependVisbleItemsWithCache(int toLimit, boolean oneColumnMode)138 protected final boolean prependVisbleItemsWithCache(int toLimit, boolean oneColumnMode) { 139 if (mLocations.size() == 0) { 140 return false; 141 } 142 int itemIndex; 143 int edge; 144 int offset; 145 if (mFirstVisibleIndex >= 0) { 146 // prepend visible items from first visible index 147 edge = mProvider.getEdge(mFirstVisibleIndex); 148 offset = getLocation(mFirstVisibleIndex).offset; 149 itemIndex = mFirstVisibleIndex - 1; 150 } else { 151 // prepend first visible item 152 edge = Integer.MAX_VALUE; 153 offset = 0; 154 itemIndex = mStartIndex != START_DEFAULT ? mStartIndex : 0; 155 if (itemIndex > getLastIndex() || itemIndex < getFirstIndex() - 1) { 156 // if the item is not within or adjacent to cached items, clear cache. 157 mLocations.clear(); 158 return false; 159 } else if (itemIndex < getFirstIndex()) { 160 // if the item is adjacent to first index, should prepend without cache. 161 return false; 162 } 163 } 164 int firstIndex = Math.max(mProvider.getMinIndex(), mFirstIndex); 165 for (; itemIndex >= firstIndex; itemIndex--) { 166 Location loc = getLocation(itemIndex); 167 int rowIndex = loc.row; 168 int size = mProvider.createItem(itemIndex, false, mTmpItem, false); 169 if (size != loc.size) { 170 mLocations.removeFromStart(itemIndex + 1 - mFirstIndex); 171 mFirstIndex = mFirstVisibleIndex; 172 // pending item will be added in prependVisibleItemsWithoutCache 173 mPendingItem = mTmpItem[0]; 174 mPendingItemSize = size; 175 return false; 176 } 177 mFirstVisibleIndex = itemIndex; 178 if (mLastVisibleIndex < 0) { 179 mLastVisibleIndex = itemIndex; 180 } 181 mProvider.addItem(mTmpItem[0], itemIndex, size, rowIndex, edge - offset); 182 if (!oneColumnMode && checkPrependOverLimit(toLimit)) { 183 return true; 184 } 185 edge = mProvider.getEdge(itemIndex); 186 offset = loc.offset; 187 // Check limit after filled a full column 188 if (rowIndex == 0) { 189 if (oneColumnMode) { 190 return true; 191 } 192 } 193 } 194 return false; 195 } 196 197 /** 198 * Calculate offset of item after last cached item. 199 */ calculateOffsetAfterLastItem(int row)200 private int calculateOffsetAfterLastItem(int row) { 201 // Find a cached item in same row, if not found, just use last item. 202 int cachedIndex = getLastIndex(); 203 boolean foundCachedItemInSameRow = false; 204 while (cachedIndex >= mFirstIndex) { 205 Location loc = getLocation(cachedIndex); 206 if (loc.row == row) { 207 foundCachedItemInSameRow = true; 208 break; 209 } 210 cachedIndex--; 211 } 212 if (!foundCachedItemInSameRow) { 213 cachedIndex = getLastIndex(); 214 } 215 // Assuming the cachedIndex is next to item on the same row, so the 216 // sum of offset of [cachedIndex + 1, itemIndex] should be size of the 217 // cached item plus spacing. 218 int offset = isReversedFlow() ? -getLocation(cachedIndex).size - mSpacing: 219 getLocation(cachedIndex).size + mSpacing; 220 for (int i = cachedIndex + 1; i <= getLastIndex(); i++) { 221 offset -= getLocation(i).offset; 222 } 223 return offset; 224 } 225 226 227 /** 228 * This implements the algorithm of layout staggered grid, the method should only be called by 229 * prependVisibleItems(). 230 */ prependVisibleItemsWithoutCache(int toLimit, boolean oneColumnMode)231 protected abstract boolean prependVisibleItemsWithoutCache(int toLimit, boolean oneColumnMode); 232 233 /** 234 * Prepends one visible item with new Location info. Only called from 235 * prependVisibleItemsWithoutCache(). 236 */ prependVisibleItemToRow(int itemIndex, int rowIndex, int edge)237 protected final int prependVisibleItemToRow(int itemIndex, int rowIndex, int edge) { 238 int offset; 239 if (mFirstVisibleIndex >= 0) { 240 if (mFirstVisibleIndex != getFirstIndex() || mFirstVisibleIndex != itemIndex + 1) { 241 // should never hit this when we prepend a new item with a new Location object. 242 throw new IllegalStateException(); 243 } 244 } 245 Location oldFirstLoc = mFirstIndex >= 0 ? getLocation(mFirstIndex) : null; 246 int oldFirstEdge = mProvider.getEdge(mFirstIndex); 247 Location loc = new Location(rowIndex, 0, 0); 248 mLocations.addFirst(loc); 249 Object item; 250 if (mPendingItem != null) { 251 loc.size = mPendingItemSize; 252 item = mPendingItem; 253 mPendingItem = null; 254 } else { 255 loc.size = mProvider.createItem(itemIndex, false, mTmpItem, false); 256 item = mTmpItem[0]; 257 } 258 mFirstIndex = mFirstVisibleIndex = itemIndex; 259 if (mLastVisibleIndex < 0) { 260 mLastVisibleIndex = itemIndex; 261 } 262 int thisEdge = !mReversedFlow ? edge - loc.size : edge + loc.size; 263 if (oldFirstLoc != null) { 264 oldFirstLoc.offset = oldFirstEdge - thisEdge; 265 } 266 mProvider.addItem(item, itemIndex, loc.size, rowIndex, thisEdge); 267 return loc.size; 268 } 269 270 @Override appendVisibleItems(int toLimit, boolean oneColumnMode)271 protected final boolean appendVisibleItems(int toLimit, boolean oneColumnMode) { 272 if (mProvider.getCount() == 0) { 273 return false; 274 } 275 if (!oneColumnMode && checkAppendOverLimit(toLimit)) { 276 return false; 277 } 278 try { 279 if (appendVisbleItemsWithCache(toLimit, oneColumnMode)) { 280 return true; 281 } 282 return appendVisibleItemsWithoutCache(toLimit, oneColumnMode); 283 } finally { 284 mTmpItem[0] = null; 285 mPendingItem = null; 286 } 287 } 288 289 /** 290 * Appends items using cached locations, returning true if at least one item is appended 291 * and (oneColumnMode is true or reach limit and aboveIndex). 292 * This method should only be called by appendVisibleItems() 293 */ appendVisbleItemsWithCache(int toLimit, boolean oneColumnMode)294 protected final boolean appendVisbleItemsWithCache(int toLimit, boolean oneColumnMode) { 295 if (mLocations.size() == 0) { 296 return false; 297 } 298 final int count = mProvider.getCount(); 299 int itemIndex; 300 int edge; 301 if (mLastVisibleIndex >= 0) { 302 // append visible items from last visible index 303 itemIndex = mLastVisibleIndex + 1; 304 edge = mProvider.getEdge(mLastVisibleIndex); 305 } else { 306 // append first visible item 307 edge = Integer.MAX_VALUE; 308 itemIndex = mStartIndex != START_DEFAULT ? mStartIndex : 0; 309 if (itemIndex > getLastIndex() + 1 || itemIndex < getFirstIndex()) { 310 // if the item is not within or adjacent to cached items, clear cache. 311 mLocations.clear(); 312 return false; 313 } else if (itemIndex > getLastIndex()) { 314 // if the item is adjacent to first index, should prepend without cache. 315 return false; 316 } 317 } 318 int lastIndex = getLastIndex(); 319 for (; itemIndex < count && itemIndex <= lastIndex; itemIndex++) { 320 Location loc = getLocation(itemIndex); 321 if (edge != Integer.MAX_VALUE) { 322 edge = edge + loc.offset; 323 } 324 int rowIndex = loc.row; 325 int size = mProvider.createItem(itemIndex, true, mTmpItem, false); 326 if (size != loc.size) { 327 loc.size = size; 328 mLocations.removeFromEnd(lastIndex - itemIndex); 329 lastIndex = itemIndex; 330 } 331 mLastVisibleIndex = itemIndex; 332 if (mFirstVisibleIndex < 0) { 333 mFirstVisibleIndex = itemIndex; 334 } 335 mProvider.addItem(mTmpItem[0], itemIndex, size, rowIndex, edge); 336 if (!oneColumnMode && checkAppendOverLimit(toLimit)) { 337 return true; 338 } 339 if (edge == Integer.MAX_VALUE) { 340 edge = mProvider.getEdge(itemIndex); 341 } 342 // Check limit after filled a full column 343 if (rowIndex == mNumRows - 1) { 344 if (oneColumnMode) { 345 return true; 346 } 347 } 348 } 349 return false; 350 } 351 352 /** 353 * algorithm of layout staggered grid, this method should only be called by 354 * appendVisibleItems(). 355 */ appendVisibleItemsWithoutCache(int toLimit, boolean oneColumnMode)356 protected abstract boolean appendVisibleItemsWithoutCache(int toLimit, boolean oneColumnMode); 357 358 /** 359 * Appends one visible item with new Location info. Only called from 360 * appendVisibleItemsWithoutCache(). 361 */ appendVisibleItemToRow(int itemIndex, int rowIndex, int location)362 protected final int appendVisibleItemToRow(int itemIndex, int rowIndex, int location) { 363 int offset; 364 if (mLastVisibleIndex >= 0) { 365 if (mLastVisibleIndex != getLastIndex() || mLastVisibleIndex != itemIndex - 1) { 366 // should never hit this when we append a new item with a new Location object. 367 throw new IllegalStateException(); 368 } 369 } 370 if (mLastVisibleIndex < 0) { 371 // if we append first visible item after existing cached items, we need update 372 // the offset later when prependVisbleItemsWithCache() 373 if (mLocations.size() > 0 && itemIndex == getLastIndex() + 1) { 374 offset = calculateOffsetAfterLastItem(rowIndex); 375 } else { 376 offset = 0; 377 } 378 } else { 379 offset = location - mProvider.getEdge(mLastVisibleIndex); 380 } 381 Location loc = new Location(rowIndex, offset, 0); 382 mLocations.addLast(loc); 383 Object item; 384 if (mPendingItem != null) { 385 loc.size = mPendingItemSize; 386 item = mPendingItem; 387 mPendingItem = null; 388 } else { 389 loc.size = mProvider.createItem(itemIndex, true, mTmpItem, false); 390 item = mTmpItem[0]; 391 } 392 if (mLocations.size() == 1) { 393 mFirstIndex = mFirstVisibleIndex = mLastVisibleIndex = itemIndex; 394 } else { 395 if (mLastVisibleIndex < 0) { 396 mFirstVisibleIndex = mLastVisibleIndex = itemIndex; 397 } else { 398 mLastVisibleIndex++; 399 } 400 } 401 mProvider.addItem(item, itemIndex, loc.size, rowIndex, location); 402 return loc.size; 403 } 404 405 @Override getItemPositionsInRows(int startPos, int endPos)406 public final CircularIntArray[] getItemPositionsInRows(int startPos, int endPos) { 407 for (int i = 0; i < mNumRows; i++) { 408 mTmpItemPositionsInRows[i].clear(); 409 } 410 if (startPos >= 0) { 411 for (int i = startPos; i <= endPos; i++) { 412 CircularIntArray row = mTmpItemPositionsInRows[getLocation(i).row]; 413 if (row.size() > 0 && row.getLast() == i - 1) { 414 // update continuous range 415 row.popLast(); 416 row.addLast(i); 417 } else { 418 // add single position 419 row.addLast(i); 420 row.addLast(i); 421 } 422 } 423 } 424 return mTmpItemPositionsInRows; 425 } 426 427 @Override invalidateItemsAfter(int index)428 public void invalidateItemsAfter(int index) { 429 super.invalidateItemsAfter(index); 430 mLocations.removeFromEnd(getLastIndex() - index + 1); 431 if (mLocations.size() == 0) { 432 mFirstIndex = -1; 433 } 434 } 435 436 } 437