1 /* 2 * Copyright (C) 2014 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 languag`e governing permissions and 14 * limitations under the License. 15 */ 16 package android.support.v7.widget; 17 18 import android.content.Context; 19 import android.graphics.Rect; 20 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 21 import android.util.AttributeSet; 22 import android.util.Log; 23 import android.util.SparseIntArray; 24 import android.view.View; 25 import android.view.ViewGroup; 26 27 import java.util.Arrays; 28 29 /** 30 * A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid. 31 * <p> 32 * By default, each item occupies 1 span. You can change it by providing a custom 33 * {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}. 34 */ 35 public class GridLayoutManager extends LinearLayoutManager { 36 37 private static final boolean DEBUG = false; 38 private static final String TAG = "GridLayoutManager"; 39 public static final int DEFAULT_SPAN_COUNT = -1; 40 /** 41 * The measure spec for the scroll direction. 42 */ 43 static final int MAIN_DIR_SPEC = 44 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 45 46 int mSpanCount = DEFAULT_SPAN_COUNT; 47 /** 48 * The size of each span 49 */ 50 int mSizePerSpan; 51 /** 52 * Temporary array to keep views in layoutChunk method 53 */ 54 View[] mSet; 55 final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray(); 56 final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray(); 57 SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); 58 // re-used variable to acquire decor insets from RecyclerView 59 final Rect mDecorInsets = new Rect(); 60 61 /** 62 * Creates a vertical GridLayoutManager 63 * 64 * @param context Current context, will be used to access resources. 65 * @param spanCount The number of columns in the grid 66 */ GridLayoutManager(Context context, int spanCount)67 public GridLayoutManager(Context context, int spanCount) { 68 super(context); 69 setSpanCount(spanCount); 70 } 71 72 /** 73 * @param context Current context, will be used to access resources. 74 * @param spanCount The number of columns or rows in the grid 75 * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link 76 * #VERTICAL}. 77 * @param reverseLayout When set to true, layouts from end to start. 78 */ GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout)79 public GridLayoutManager(Context context, int spanCount, int orientation, 80 boolean reverseLayout) { 81 super(context, orientation, reverseLayout); 82 setSpanCount(spanCount); 83 } 84 85 /** 86 * stackFromEnd is not supported by GridLayoutManager. Consider using 87 * {@link #setReverseLayout(boolean)}. 88 */ 89 @Override setStackFromEnd(boolean stackFromEnd)90 public void setStackFromEnd(boolean stackFromEnd) { 91 if (stackFromEnd) { 92 throw new UnsupportedOperationException( 93 "GridLayoutManager does not support stack from end." 94 + " Consider using reverse layout"); 95 } 96 super.setStackFromEnd(false); 97 } 98 99 @Override getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)100 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 101 RecyclerView.State state) { 102 if (mOrientation == HORIZONTAL) { 103 return mSpanCount; 104 } 105 if (state.getItemCount() < 1) { 106 return 0; 107 } 108 return getSpanGroupIndex(recycler, state, state.getItemCount() - 1); 109 } 110 111 @Override getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)112 public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, 113 RecyclerView.State state) { 114 if (mOrientation == VERTICAL) { 115 return mSpanCount; 116 } 117 if (state.getItemCount() < 1) { 118 return 0; 119 } 120 return getSpanGroupIndex(recycler, state, state.getItemCount() - 1); 121 } 122 123 @Override onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)124 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, 125 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { 126 ViewGroup.LayoutParams lp = host.getLayoutParams(); 127 if (!(lp instanceof LayoutParams)) { 128 super.onInitializeAccessibilityNodeInfoForItem(host, info); 129 return; 130 } 131 LayoutParams glp = (LayoutParams) lp; 132 int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition()); 133 if (mOrientation == HORIZONTAL) { 134 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 135 glp.getSpanIndex(), glp.getSpanSize(), 136 spanGroupIndex, 1, 137 mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false)); 138 } else { // VERTICAL 139 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 140 spanGroupIndex , 1, 141 glp.getSpanIndex(), glp.getSpanSize(), 142 mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false)); 143 } 144 } 145 146 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)147 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 148 if (state.isPreLayout()) { 149 cachePreLayoutSpanMapping(); 150 } 151 super.onLayoutChildren(recycler, state); 152 if (DEBUG) { 153 validateChildOrder(); 154 } 155 clearPreLayoutSpanMappingCache(); 156 } 157 clearPreLayoutSpanMappingCache()158 private void clearPreLayoutSpanMappingCache() { 159 mPreLayoutSpanSizeCache.clear(); 160 mPreLayoutSpanIndexCache.clear(); 161 } 162 cachePreLayoutSpanMapping()163 private void cachePreLayoutSpanMapping() { 164 final int childCount = getChildCount(); 165 for (int i = 0; i < childCount; i++) { 166 final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 167 final int viewPosition = lp.getViewLayoutPosition(); 168 mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize()); 169 mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex()); 170 } 171 } 172 173 @Override onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)174 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 175 mSpanSizeLookup.invalidateSpanIndexCache(); 176 } 177 178 @Override onItemsChanged(RecyclerView recyclerView)179 public void onItemsChanged(RecyclerView recyclerView) { 180 mSpanSizeLookup.invalidateSpanIndexCache(); 181 } 182 183 @Override onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)184 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 185 mSpanSizeLookup.invalidateSpanIndexCache(); 186 } 187 188 @Override onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount)189 public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) { 190 mSpanSizeLookup.invalidateSpanIndexCache(); 191 } 192 193 @Override onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount)194 public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { 195 mSpanSizeLookup.invalidateSpanIndexCache(); 196 } 197 198 @Override generateDefaultLayoutParams()199 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 200 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 201 ViewGroup.LayoutParams.WRAP_CONTENT); 202 } 203 204 @Override generateLayoutParams(Context c, AttributeSet attrs)205 public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 206 return new LayoutParams(c, attrs); 207 } 208 209 @Override generateLayoutParams(ViewGroup.LayoutParams lp)210 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 211 if (lp instanceof ViewGroup.MarginLayoutParams) { 212 return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 213 } else { 214 return new LayoutParams(lp); 215 } 216 } 217 218 @Override checkLayoutParams(RecyclerView.LayoutParams lp)219 public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 220 return lp instanceof LayoutParams; 221 } 222 223 /** 224 * Sets the source to get the number of spans occupied by each item in the adapter. 225 * 226 * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans 227 * occupied by each item 228 */ setSpanSizeLookup(SpanSizeLookup spanSizeLookup)229 public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) { 230 mSpanSizeLookup = spanSizeLookup; 231 } 232 233 /** 234 * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager. 235 * 236 * @return The current {@link SpanSizeLookup} used by the GridLayoutManager. 237 */ getSpanSizeLookup()238 public SpanSizeLookup getSpanSizeLookup() { 239 return mSpanSizeLookup; 240 } 241 updateMeasurements()242 private void updateMeasurements() { 243 int totalSpace; 244 if (getOrientation() == VERTICAL) { 245 totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); 246 } else { 247 totalSpace = getHeight() - getPaddingBottom() - getPaddingTop(); 248 } 249 mSizePerSpan = totalSpace / mSpanCount; 250 } 251 252 @Override onAnchorReady(RecyclerView.State state, AnchorInfo anchorInfo)253 void onAnchorReady(RecyclerView.State state, AnchorInfo anchorInfo) { 254 super.onAnchorReady(state, anchorInfo); 255 updateMeasurements(); 256 if (state.getItemCount() > 0 && !state.isPreLayout()) { 257 ensureAnchorIsInFirstSpan(anchorInfo); 258 } 259 if (mSet == null || mSet.length != mSpanCount) { 260 mSet = new View[mSpanCount]; 261 } 262 } 263 ensureAnchorIsInFirstSpan(AnchorInfo anchorInfo)264 private void ensureAnchorIsInFirstSpan(AnchorInfo anchorInfo) { 265 int span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount); 266 while (span > 0 && anchorInfo.mPosition > 0) { 267 anchorInfo.mPosition--; 268 span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount); 269 } 270 } 271 getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int viewPosition)272 private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, 273 int viewPosition) { 274 if (!state.isPreLayout()) { 275 return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount); 276 } 277 final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition); 278 if (adapterPosition == -1) { 279 if (DEBUG) { 280 throw new RuntimeException("Cannot find span group index for position " 281 + viewPosition); 282 } 283 Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition); 284 return 0; 285 } 286 return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount); 287 } 288 getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)289 private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) { 290 if (!state.isPreLayout()) { 291 return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount); 292 } 293 final int cached = mPreLayoutSpanIndexCache.get(pos, -1); 294 if (cached != -1) { 295 return cached; 296 } 297 final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); 298 if (adapterPosition == -1) { 299 if (DEBUG) { 300 throw new RuntimeException("Cannot find span index for pre layout position. It is" 301 + " not cached, not in the adapter. Pos:" + pos); 302 } 303 Log.w(TAG, "Cannot find span size for pre layout position. It is" 304 + " not cached, not in the adapter. Pos:" + pos); 305 return 0; 306 } 307 return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount); 308 } 309 getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)310 private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) { 311 if (!state.isPreLayout()) { 312 return mSpanSizeLookup.getSpanSize(pos); 313 } 314 final int cached = mPreLayoutSpanSizeCache.get(pos, -1); 315 if (cached != -1) { 316 return cached; 317 } 318 final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); 319 if (adapterPosition == -1) { 320 if (DEBUG) { 321 throw new RuntimeException("Cannot find span size for pre layout position. It is" 322 + " not cached, not in the adapter. Pos:" + pos); 323 } 324 Log.w(TAG, "Cannot find span size for pre layout position. It is" 325 + " not cached, not in the adapter. Pos:" + pos); 326 return 1; 327 } 328 return mSpanSizeLookup.getSpanSize(adapterPosition); 329 } 330 331 @Override layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result)332 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, 333 LayoutState layoutState, LayoutChunkResult result) { 334 final boolean layingOutInPrimaryDirection = 335 layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL; 336 int count = 0; 337 int consumedSpanCount = 0; 338 int remainingSpan = mSpanCount; 339 if (!layingOutInPrimaryDirection) { 340 int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition); 341 int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition); 342 remainingSpan = itemSpanIndex + itemSpanSize; 343 } 344 while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { 345 int pos = layoutState.mCurrentPosition; 346 final int spanSize = getSpanSize(recycler, state, pos); 347 if (spanSize > mSpanCount) { 348 throw new IllegalArgumentException("Item at position " + pos + " requires " + 349 spanSize + " spans but GridLayoutManager has only " + mSpanCount 350 + " spans."); 351 } 352 remainingSpan -= spanSize; 353 if (remainingSpan < 0) { 354 break; // item did not fit into this row or column 355 } 356 View view = layoutState.next(recycler); 357 if (view == null) { 358 break; 359 } 360 consumedSpanCount += spanSize; 361 mSet[count] = view; 362 count++; 363 } 364 365 if (count == 0) { 366 result.mFinished = true; 367 return; 368 } 369 370 int maxSize = 0; 371 372 // we should assign spans before item decor offsets are calculated 373 assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection); 374 for (int i = 0; i < count; i++) { 375 View view = mSet[i]; 376 if (layoutState.mScrapList == null) { 377 if (layingOutInPrimaryDirection) { 378 addView(view); 379 } else { 380 addView(view, 0); 381 } 382 } else { 383 if (layingOutInPrimaryDirection) { 384 addDisappearingView(view); 385 } else { 386 addDisappearingView(view, 0); 387 } 388 } 389 390 int spanSize = getSpanSize(recycler, state, getPosition(view)); 391 final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize, 392 View.MeasureSpec.EXACTLY); 393 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 394 if (mOrientation == VERTICAL) { 395 measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height)); 396 } else { 397 measureChildWithDecorationsAndMargin(view, getMainDirSpec(lp.width), spec); 398 } 399 final int size = mOrientationHelper.getDecoratedMeasurement(view); 400 if (size > maxSize) { 401 maxSize = size; 402 } 403 } 404 405 // views that did not measure the maxSize has to be re-measured 406 final int maxMeasureSpec = getMainDirSpec(maxSize); 407 for (int i = 0; i < count; i ++) { 408 final View view = mSet[i]; 409 if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) { 410 int spanSize = getSpanSize(recycler, state, getPosition(view)); 411 final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize, 412 View.MeasureSpec.EXACTLY); 413 if (mOrientation == VERTICAL) { 414 measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec); 415 } else { 416 measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec); 417 } 418 } 419 } 420 421 result.mConsumed = maxSize; 422 423 int left = 0, right = 0, top = 0, bottom = 0; 424 if (mOrientation == VERTICAL) { 425 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 426 bottom = layoutState.mOffset; 427 top = bottom - maxSize; 428 } else { 429 top = layoutState.mOffset; 430 bottom = top + maxSize; 431 } 432 } else { 433 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 434 right = layoutState.mOffset; 435 left = right - maxSize; 436 } else { 437 left = layoutState.mOffset; 438 right = left + maxSize; 439 } 440 } 441 for (int i = 0; i < count; i++) { 442 View view = mSet[i]; 443 LayoutParams params = (LayoutParams) view.getLayoutParams(); 444 if (mOrientation == VERTICAL) { 445 left = getPaddingLeft() + mSizePerSpan * params.mSpanIndex; 446 right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); 447 } else { 448 top = getPaddingTop() + mSizePerSpan * params.mSpanIndex; 449 bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); 450 } 451 // We calculate everything with View's bounding box (which includes decor and margins) 452 // To calculate correct layout position, we subtract margins. 453 layoutDecorated(view, left + params.leftMargin, top + params.topMargin, 454 right - params.rightMargin, bottom - params.bottomMargin); 455 if (DEBUG) { 456 Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" 457 + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" 458 + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin) 459 + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize); 460 } 461 // Consume the available space if the view is not removed OR changed 462 if (params.isItemRemoved() || params.isItemChanged()) { 463 result.mIgnoreConsumed = true; 464 } 465 result.mFocusable |= view.isFocusable(); 466 } 467 Arrays.fill(mSet, null); 468 } 469 getMainDirSpec(int dim)470 private int getMainDirSpec(int dim) { 471 if (dim < 0) { 472 return MAIN_DIR_SPEC; 473 } else { 474 return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY); 475 } 476 } 477 measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec)478 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) { 479 calculateItemDecorationsForChild(child, mDecorInsets); 480 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); 481 widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left, 482 lp.rightMargin + mDecorInsets.right); 483 heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top, 484 lp.bottomMargin + mDecorInsets.bottom); 485 child.measure(widthSpec, heightSpec); 486 } 487 updateSpecWithExtra(int spec, int startInset, int endInset)488 private int updateSpecWithExtra(int spec, int startInset, int endInset) { 489 if (startInset == 0 && endInset == 0) { 490 return spec; 491 } 492 final int mode = View.MeasureSpec.getMode(spec); 493 if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { 494 return View.MeasureSpec.makeMeasureSpec( 495 View.MeasureSpec.getSize(spec) - startInset - endInset, mode); 496 } 497 return spec; 498 } 499 assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, int consumedSpanCount, boolean layingOutInPrimaryDirection)500 private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, 501 int consumedSpanCount, boolean layingOutInPrimaryDirection) { 502 int span, spanDiff, start, end, diff; 503 // make sure we traverse from min position to max position 504 if (layingOutInPrimaryDirection) { 505 start = 0; 506 end = count; 507 diff = 1; 508 } else { 509 start = count - 1; 510 end = -1; 511 diff = -1; 512 } 513 if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span 514 span = consumedSpanCount - 1; 515 spanDiff = -1; 516 } else { 517 span = 0; 518 spanDiff = 1; 519 } 520 for (int i = start; i != end; i += diff) { 521 View view = mSet[i]; 522 LayoutParams params = (LayoutParams) view.getLayoutParams(); 523 params.mSpanSize = getSpanSize(recycler, state, getPosition(view)); 524 if (spanDiff == -1 && params.mSpanSize > 1) { 525 params.mSpanIndex = span - (params.mSpanSize - 1); 526 } else { 527 params.mSpanIndex = span; 528 } 529 span += spanDiff * params.mSpanSize; 530 } 531 } 532 533 /** 534 * Returns the number of spans laid out by this grid. 535 * 536 * @return The number of spans 537 * @see #setSpanCount(int) 538 */ getSpanCount()539 public int getSpanCount() { 540 return mSpanCount; 541 } 542 543 /** 544 * Sets the number of spans to be laid out. 545 * <p> 546 * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns. 547 * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows. 548 * 549 * @param spanCount The total number of spans in the grid 550 * @see #getSpanCount() 551 */ setSpanCount(int spanCount)552 public void setSpanCount(int spanCount) { 553 if (spanCount == mSpanCount) { 554 return; 555 } 556 if (spanCount < 1) { 557 throw new IllegalArgumentException("Span count should be at least 1. Provided " 558 + spanCount); 559 } 560 mSpanCount = spanCount; 561 mSpanSizeLookup.invalidateSpanIndexCache(); 562 } 563 564 /** 565 * A helper class to provide the number of spans each item occupies. 566 * <p> 567 * Default implementation sets each item to occupy exactly 1 span. 568 * 569 * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup) 570 */ 571 public static abstract class SpanSizeLookup { 572 573 final SparseIntArray mSpanIndexCache = new SparseIntArray(); 574 575 private boolean mCacheSpanIndices = false; 576 577 /** 578 * Returns the number of span occupied by the item at <code>position</code>. 579 * 580 * @param position The adapter position of the item 581 * @return The number of spans occupied by the item at the provided position 582 */ getSpanSize(int position)583 abstract public int getSpanSize(int position); 584 585 /** 586 * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or 587 * not. By default these values are not cached. If you are not overriding 588 * {@link #getSpanIndex(int, int)}, you should set this to true for better performance. 589 * 590 * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not. 591 */ setSpanIndexCacheEnabled(boolean cacheSpanIndices)592 public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) { 593 mCacheSpanIndices = cacheSpanIndices; 594 } 595 596 /** 597 * Clears the span index cache. GridLayoutManager automatically calls this method when 598 * adapter changes occur. 599 */ invalidateSpanIndexCache()600 public void invalidateSpanIndexCache() { 601 mSpanIndexCache.clear(); 602 } 603 604 /** 605 * Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not. 606 * 607 * @return True if results of {@link #getSpanIndex(int, int)} are cached. 608 */ isSpanIndexCacheEnabled()609 public boolean isSpanIndexCacheEnabled() { 610 return mCacheSpanIndices; 611 } 612 getCachedSpanIndex(int position, int spanCount)613 int getCachedSpanIndex(int position, int spanCount) { 614 if (!mCacheSpanIndices) { 615 return getSpanIndex(position, spanCount); 616 } 617 final int existing = mSpanIndexCache.get(position, -1); 618 if (existing != -1) { 619 return existing; 620 } 621 final int value = getSpanIndex(position, spanCount); 622 mSpanIndexCache.put(position, value); 623 return value; 624 } 625 626 /** 627 * Returns the final span index of the provided position. 628 * <p> 629 * If you have a faster way to calculate span index for your items, you should override 630 * this method. Otherwise, you should enable span index cache 631 * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is 632 * disabled, default implementation traverses all items from 0 to 633 * <code>position</code>. When caching is enabled, it calculates from the closest cached 634 * value before the <code>position</code>. 635 * <p> 636 * If you override this method, you need to make sure it is consistent with 637 * {@link #getSpanSize(int)}. GridLayoutManager does not call this method for 638 * each item. It is called only for the reference item and rest of the items 639 * are assigned to spans based on the reference item. For example, you cannot assign a 640 * position to span 2 while span 1 is empty. 641 * <p> 642 * Note that span offsets always start with 0 and are not affected by RTL. 643 * 644 * @param position The position of the item 645 * @param spanCount The total number of spans in the grid 646 * @return The final span position of the item. Should be between 0 (inclusive) and 647 * <code>spanCount</code>(exclusive) 648 */ getSpanIndex(int position, int spanCount)649 public int getSpanIndex(int position, int spanCount) { 650 int positionSpanSize = getSpanSize(position); 651 if (positionSpanSize == spanCount) { 652 return 0; // quick return for full-span items 653 } 654 int span = 0; 655 int startPos = 0; 656 // If caching is enabled, try to jump 657 if (mCacheSpanIndices && mSpanIndexCache.size() > 0) { 658 int prevKey = findReferenceIndexFromCache(position); 659 if (prevKey >= 0) { 660 span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey); 661 startPos = prevKey + 1; 662 } 663 } 664 for (int i = startPos; i < position; i++) { 665 int size = getSpanSize(i); 666 span += size; 667 if (span == spanCount) { 668 span = 0; 669 } else if (span > spanCount) { 670 // did not fit, moving to next row / column 671 span = size; 672 } 673 } 674 if (span + positionSpanSize <= spanCount) { 675 return span; 676 } 677 return 0; 678 } 679 findReferenceIndexFromCache(int position)680 int findReferenceIndexFromCache(int position) { 681 int lo = 0; 682 int hi = mSpanIndexCache.size() - 1; 683 684 while (lo <= hi) { 685 final int mid = (lo + hi) >>> 1; 686 final int midVal = mSpanIndexCache.keyAt(mid); 687 if (midVal < position) { 688 lo = mid + 1; 689 } else { 690 hi = mid - 1; 691 } 692 } 693 int index = lo - 1; 694 if (index >= 0 && index < mSpanIndexCache.size()) { 695 return mSpanIndexCache.keyAt(index); 696 } 697 return -1; 698 } 699 700 /** 701 * Returns the index of the group this position belongs. 702 * <p> 703 * For example, if grid has 3 columns and each item occupies 1 span, span group index 704 * for item 1 will be 0, item 5 will be 1. 705 * 706 * @param adapterPosition The position in adapter 707 * @param spanCount The total number of spans in the grid 708 * @return The index of the span group including the item at the given adapter position 709 */ getSpanGroupIndex(int adapterPosition, int spanCount)710 public int getSpanGroupIndex(int adapterPosition, int spanCount) { 711 int span = 0; 712 int group = 0; 713 int positionSpanSize = getSpanSize(adapterPosition); 714 for (int i = 0; i < adapterPosition; i++) { 715 int size = getSpanSize(i); 716 span += size; 717 if (span == spanCount) { 718 span = 0; 719 group++; 720 } else if (span > spanCount) { 721 // did not fit, moving to next row / column 722 span = size; 723 group++; 724 } 725 } 726 if (span + positionSpanSize > spanCount) { 727 group++; 728 } 729 return group; 730 } 731 } 732 733 @Override supportsPredictiveItemAnimations()734 public boolean supportsPredictiveItemAnimations() { 735 return mPendingSavedState == null; 736 } 737 738 /** 739 * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span. 740 */ 741 public static final class DefaultSpanSizeLookup extends SpanSizeLookup { 742 743 @Override getSpanSize(int position)744 public int getSpanSize(int position) { 745 return 1; 746 } 747 748 @Override getSpanIndex(int position, int spanCount)749 public int getSpanIndex(int position, int spanCount) { 750 return position % spanCount; 751 } 752 } 753 754 /** 755 * LayoutParams used by GridLayoutManager. 756 * <p> 757 * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the 758 * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is 759 * expected to fill all of the space given to it. 760 */ 761 public static class LayoutParams extends RecyclerView.LayoutParams { 762 763 /** 764 * Span Id for Views that are not laid out yet. 765 */ 766 public static final int INVALID_SPAN_ID = -1; 767 768 private int mSpanIndex = INVALID_SPAN_ID; 769 770 private int mSpanSize = 0; 771 LayoutParams(Context c, AttributeSet attrs)772 public LayoutParams(Context c, AttributeSet attrs) { 773 super(c, attrs); 774 } 775 LayoutParams(int width, int height)776 public LayoutParams(int width, int height) { 777 super(width, height); 778 } 779 LayoutParams(ViewGroup.MarginLayoutParams source)780 public LayoutParams(ViewGroup.MarginLayoutParams source) { 781 super(source); 782 } 783 LayoutParams(ViewGroup.LayoutParams source)784 public LayoutParams(ViewGroup.LayoutParams source) { 785 super(source); 786 } 787 LayoutParams(RecyclerView.LayoutParams source)788 public LayoutParams(RecyclerView.LayoutParams source) { 789 super(source); 790 } 791 792 /** 793 * Returns the current span index of this View. If the View is not laid out yet, the return 794 * value is <code>undefined</code>. 795 * <p> 796 * Note that span index may change by whether the RecyclerView is RTL or not. For 797 * example, if the number of spans is 3 and layout is RTL, the rightmost item will have 798 * span index of 2. If the layout changes back to LTR, span index for this view will be 0. 799 * If the item was occupying 2 spans, span indices would be 1 and 0 respectively. 800 * <p> 801 * If the View occupies multiple spans, span with the minimum index is returned. 802 * 803 * @return The span index of the View. 804 */ getSpanIndex()805 public int getSpanIndex() { 806 return mSpanIndex; 807 } 808 809 /** 810 * Returns the number of spans occupied by this View. If the View not laid out yet, the 811 * return value is <code>undefined</code>. 812 * 813 * @return The number of spans occupied by this View. 814 */ getSpanSize()815 public int getSpanSize() { 816 return mSpanSize; 817 } 818 } 819 820 } 821