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