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 language 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 * Span size have been changed but we've not done a new layout calculation. 42 */ 43 boolean mPendingSpanCountChange = false; 44 int mSpanCount = DEFAULT_SPAN_COUNT; 45 /** 46 * Right borders for each span. 47 * <p>For <b>i-th</b> item start is {@link #mCachedBorders}[i-1] + 1 48 * and end is {@link #mCachedBorders}[i]. 49 */ 50 int [] mCachedBorders; 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 /** 63 * Constructor used when layout manager is set in XML by RecyclerView attribute 64 * "layoutManager". If spanCount is not specified in the XML, it defaults to a 65 * single column. 66 * 67 * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount 68 */ GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)69 public GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, 70 int defStyleRes) { 71 super(context, attrs, defStyleAttr, defStyleRes); 72 Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); 73 setSpanCount(properties.spanCount); 74 } 75 76 /** 77 * Creates a vertical GridLayoutManager 78 * 79 * @param context Current context, will be used to access resources. 80 * @param spanCount The number of columns in the grid 81 */ GridLayoutManager(Context context, int spanCount)82 public GridLayoutManager(Context context, int spanCount) { 83 super(context); 84 setSpanCount(spanCount); 85 } 86 87 /** 88 * @param context Current context, will be used to access resources. 89 * @param spanCount The number of columns or rows in the grid 90 * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link 91 * #VERTICAL}. 92 * @param reverseLayout When set to true, layouts from end to start. 93 */ GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout)94 public GridLayoutManager(Context context, int spanCount, int orientation, 95 boolean reverseLayout) { 96 super(context, orientation, reverseLayout); 97 setSpanCount(spanCount); 98 } 99 100 /** 101 * stackFromEnd is not supported by GridLayoutManager. Consider using 102 * {@link #setReverseLayout(boolean)}. 103 */ 104 @Override setStackFromEnd(boolean stackFromEnd)105 public void setStackFromEnd(boolean stackFromEnd) { 106 if (stackFromEnd) { 107 throw new UnsupportedOperationException( 108 "GridLayoutManager does not support stack from end." 109 + " Consider using reverse layout"); 110 } 111 super.setStackFromEnd(false); 112 } 113 114 @Override getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)115 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 116 RecyclerView.State state) { 117 if (mOrientation == HORIZONTAL) { 118 return mSpanCount; 119 } 120 if (state.getItemCount() < 1) { 121 return 0; 122 } 123 124 // Row count is one more than the last item's row index. 125 return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1; 126 } 127 128 @Override getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)129 public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, 130 RecyclerView.State state) { 131 if (mOrientation == VERTICAL) { 132 return mSpanCount; 133 } 134 if (state.getItemCount() < 1) { 135 return 0; 136 } 137 138 // Column count is one more than the last item's column index. 139 return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1; 140 } 141 142 @Override onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)143 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, 144 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { 145 ViewGroup.LayoutParams lp = host.getLayoutParams(); 146 if (!(lp instanceof LayoutParams)) { 147 super.onInitializeAccessibilityNodeInfoForItem(host, info); 148 return; 149 } 150 LayoutParams glp = (LayoutParams) lp; 151 int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition()); 152 if (mOrientation == HORIZONTAL) { 153 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 154 glp.getSpanIndex(), glp.getSpanSize(), 155 spanGroupIndex, 1, 156 mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false)); 157 } else { // VERTICAL 158 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 159 spanGroupIndex , 1, 160 glp.getSpanIndex(), glp.getSpanSize(), 161 mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false)); 162 } 163 } 164 165 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)166 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 167 if (state.isPreLayout()) { 168 cachePreLayoutSpanMapping(); 169 } 170 super.onLayoutChildren(recycler, state); 171 if (DEBUG) { 172 validateChildOrder(); 173 } 174 clearPreLayoutSpanMappingCache(); 175 } 176 177 @Override onLayoutCompleted(RecyclerView.State state)178 public void onLayoutCompleted(RecyclerView.State state) { 179 super.onLayoutCompleted(state); 180 mPendingSpanCountChange = false; 181 } 182 clearPreLayoutSpanMappingCache()183 private void clearPreLayoutSpanMappingCache() { 184 mPreLayoutSpanSizeCache.clear(); 185 mPreLayoutSpanIndexCache.clear(); 186 } 187 cachePreLayoutSpanMapping()188 private void cachePreLayoutSpanMapping() { 189 final int childCount = getChildCount(); 190 for (int i = 0; i < childCount; i++) { 191 final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 192 final int viewPosition = lp.getViewLayoutPosition(); 193 mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize()); 194 mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex()); 195 } 196 } 197 198 @Override onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)199 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 200 mSpanSizeLookup.invalidateSpanIndexCache(); 201 } 202 203 @Override onItemsChanged(RecyclerView recyclerView)204 public void onItemsChanged(RecyclerView recyclerView) { 205 mSpanSizeLookup.invalidateSpanIndexCache(); 206 } 207 208 @Override onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)209 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 210 mSpanSizeLookup.invalidateSpanIndexCache(); 211 } 212 213 @Override onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, Object payload)214 public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, 215 Object payload) { 216 mSpanSizeLookup.invalidateSpanIndexCache(); 217 } 218 219 @Override onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount)220 public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { 221 mSpanSizeLookup.invalidateSpanIndexCache(); 222 } 223 224 @Override generateDefaultLayoutParams()225 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 226 if (mOrientation == HORIZONTAL) { 227 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 228 ViewGroup.LayoutParams.MATCH_PARENT); 229 } else { 230 return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 231 ViewGroup.LayoutParams.WRAP_CONTENT); 232 } 233 } 234 235 @Override generateLayoutParams(Context c, AttributeSet attrs)236 public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 237 return new LayoutParams(c, attrs); 238 } 239 240 @Override generateLayoutParams(ViewGroup.LayoutParams lp)241 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 242 if (lp instanceof ViewGroup.MarginLayoutParams) { 243 return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 244 } else { 245 return new LayoutParams(lp); 246 } 247 } 248 249 @Override checkLayoutParams(RecyclerView.LayoutParams lp)250 public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 251 return lp instanceof LayoutParams; 252 } 253 254 /** 255 * Sets the source to get the number of spans occupied by each item in the adapter. 256 * 257 * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans 258 * occupied by each item 259 */ setSpanSizeLookup(SpanSizeLookup spanSizeLookup)260 public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) { 261 mSpanSizeLookup = spanSizeLookup; 262 } 263 264 /** 265 * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager. 266 * 267 * @return The current {@link SpanSizeLookup} used by the GridLayoutManager. 268 */ getSpanSizeLookup()269 public SpanSizeLookup getSpanSizeLookup() { 270 return mSpanSizeLookup; 271 } 272 updateMeasurements()273 private void updateMeasurements() { 274 int totalSpace; 275 if (getOrientation() == VERTICAL) { 276 totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); 277 } else { 278 totalSpace = getHeight() - getPaddingBottom() - getPaddingTop(); 279 } 280 calculateItemBorders(totalSpace); 281 } 282 283 @Override setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)284 public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { 285 if (mCachedBorders == null) { 286 super.setMeasuredDimension(childrenBounds, wSpec, hSpec); 287 } 288 final int width, height; 289 final int horizontalPadding = getPaddingLeft() + getPaddingRight(); 290 final int verticalPadding = getPaddingTop() + getPaddingBottom(); 291 if (mOrientation == VERTICAL) { 292 final int usedHeight = childrenBounds.height() + verticalPadding; 293 height = chooseSize(hSpec, usedHeight, getMinimumHeight()); 294 width = chooseSize(wSpec, mCachedBorders[mCachedBorders.length - 1] + horizontalPadding, 295 getMinimumWidth()); 296 } else { 297 final int usedWidth = childrenBounds.width() + horizontalPadding; 298 width = chooseSize(wSpec, usedWidth, getMinimumWidth()); 299 height = chooseSize(hSpec, mCachedBorders[mCachedBorders.length - 1] + verticalPadding, 300 getMinimumHeight()); 301 } 302 setMeasuredDimension(width, height); 303 } 304 305 /** 306 * @param totalSpace Total available space after padding is removed 307 */ calculateItemBorders(int totalSpace)308 private void calculateItemBorders(int totalSpace) { 309 mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace); 310 } 311 312 /** 313 * @param cachedBorders The out array 314 * @param spanCount number of spans 315 * @param totalSpace total available space after padding is removed 316 * @return The updated array. Might be the same instance as the provided array if its size 317 * has not changed. 318 */ calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace)319 static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) { 320 if (cachedBorders == null || cachedBorders.length != spanCount + 1 321 || cachedBorders[cachedBorders.length - 1] != totalSpace) { 322 cachedBorders = new int[spanCount + 1]; 323 } 324 cachedBorders[0] = 0; 325 int sizePerSpan = totalSpace / spanCount; 326 int sizePerSpanRemainder = totalSpace % spanCount; 327 int consumedPixels = 0; 328 int additionalSize = 0; 329 for (int i = 1; i <= spanCount; i++) { 330 int itemSize = sizePerSpan; 331 additionalSize += sizePerSpanRemainder; 332 if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) { 333 itemSize += 1; 334 additionalSize -= spanCount; 335 } 336 consumedPixels += itemSize; 337 cachedBorders[i] = consumedPixels; 338 } 339 return cachedBorders; 340 } 341 getSpaceForSpanRange(int startSpan, int spanSize)342 int getSpaceForSpanRange(int startSpan, int spanSize) { 343 if (mOrientation == VERTICAL && isLayoutRTL()) { 344 return mCachedBorders[mSpanCount - startSpan] 345 - mCachedBorders[mSpanCount - startSpan - spanSize]; 346 } else { 347 return mCachedBorders[startSpan + spanSize] - mCachedBorders[startSpan]; 348 } 349 } 350 351 @Override onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection)352 void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, 353 AnchorInfo anchorInfo, int itemDirection) { 354 super.onAnchorReady(recycler, state, anchorInfo, itemDirection); 355 updateMeasurements(); 356 if (state.getItemCount() > 0 && !state.isPreLayout()) { 357 ensureAnchorIsInCorrectSpan(recycler, state, anchorInfo, itemDirection); 358 } 359 ensureViewSet(); 360 } 361 ensureViewSet()362 private void ensureViewSet() { 363 if (mSet == null || mSet.length != mSpanCount) { 364 mSet = new View[mSpanCount]; 365 } 366 } 367 368 @Override scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)369 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 370 RecyclerView.State state) { 371 updateMeasurements(); 372 ensureViewSet(); 373 return super.scrollHorizontallyBy(dx, recycler, state); 374 } 375 376 @Override scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)377 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 378 RecyclerView.State state) { 379 updateMeasurements(); 380 ensureViewSet(); 381 return super.scrollVerticallyBy(dy, recycler, state); 382 } 383 ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection)384 private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, 385 RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection) { 386 final boolean layingOutInPrimaryDirection = 387 itemDirection == LayoutState.ITEM_DIRECTION_TAIL; 388 int span = getSpanIndex(recycler, state, anchorInfo.mPosition); 389 if (layingOutInPrimaryDirection) { 390 // choose span 0 391 while (span > 0 && anchorInfo.mPosition > 0) { 392 anchorInfo.mPosition--; 393 span = getSpanIndex(recycler, state, anchorInfo.mPosition); 394 } 395 } else { 396 // choose the max span we can get. hopefully last one 397 final int indexLimit = state.getItemCount() - 1; 398 int pos = anchorInfo.mPosition; 399 int bestSpan = span; 400 while (pos < indexLimit) { 401 int next = getSpanIndex(recycler, state, pos + 1); 402 if (next > bestSpan) { 403 pos += 1; 404 bestSpan = next; 405 } else { 406 break; 407 } 408 } 409 anchorInfo.mPosition = pos; 410 } 411 } 412 413 @Override findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, int start, int end, int itemCount)414 View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, 415 int start, int end, int itemCount) { 416 ensureLayoutState(); 417 View invalidMatch = null; 418 View outOfBoundsMatch = null; 419 final int boundsStart = mOrientationHelper.getStartAfterPadding(); 420 final int boundsEnd = mOrientationHelper.getEndAfterPadding(); 421 final int diff = end > start ? 1 : -1; 422 423 for (int i = start; i != end; i += diff) { 424 final View view = getChildAt(i); 425 final int position = getPosition(view); 426 if (position >= 0 && position < itemCount) { 427 final int span = getSpanIndex(recycler, state, position); 428 if (span != 0) { 429 continue; 430 } 431 if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) { 432 if (invalidMatch == null) { 433 invalidMatch = view; // removed item, least preferred 434 } 435 } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd 436 || mOrientationHelper.getDecoratedEnd(view) < boundsStart) { 437 if (outOfBoundsMatch == null) { 438 outOfBoundsMatch = view; // item is not visible, less preferred 439 } 440 } else { 441 return view; 442 } 443 } 444 } 445 return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch; 446 } 447 getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int viewPosition)448 private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, 449 int viewPosition) { 450 if (!state.isPreLayout()) { 451 return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount); 452 } 453 final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition); 454 if (adapterPosition == -1) { 455 if (DEBUG) { 456 throw new RuntimeException("Cannot find span group index for position " 457 + viewPosition); 458 } 459 Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition); 460 return 0; 461 } 462 return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount); 463 } 464 getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)465 private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) { 466 if (!state.isPreLayout()) { 467 return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount); 468 } 469 final int cached = mPreLayoutSpanIndexCache.get(pos, -1); 470 if (cached != -1) { 471 return cached; 472 } 473 final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); 474 if (adapterPosition == -1) { 475 if (DEBUG) { 476 throw new RuntimeException("Cannot find span index for pre layout position. It is" 477 + " not cached, not in the adapter. Pos:" + pos); 478 } 479 Log.w(TAG, "Cannot find span size for pre layout position. It is" 480 + " not cached, not in the adapter. Pos:" + pos); 481 return 0; 482 } 483 return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount); 484 } 485 getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)486 private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) { 487 if (!state.isPreLayout()) { 488 return mSpanSizeLookup.getSpanSize(pos); 489 } 490 final int cached = mPreLayoutSpanSizeCache.get(pos, -1); 491 if (cached != -1) { 492 return cached; 493 } 494 final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); 495 if (adapterPosition == -1) { 496 if (DEBUG) { 497 throw new RuntimeException("Cannot find span size for pre layout position. It is" 498 + " not cached, not in the adapter. Pos:" + pos); 499 } 500 Log.w(TAG, "Cannot find span size for pre layout position. It is" 501 + " not cached, not in the adapter. Pos:" + pos); 502 return 1; 503 } 504 return mSpanSizeLookup.getSpanSize(adapterPosition); 505 } 506 507 @Override collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, LayoutPrefetchRegistry layoutPrefetchRegistry)508 void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, 509 LayoutPrefetchRegistry layoutPrefetchRegistry) { 510 int remainingSpan = mSpanCount; 511 int count = 0; 512 while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { 513 final int pos = layoutState.mCurrentPosition; 514 layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset)); 515 final int spanSize = mSpanSizeLookup.getSpanSize(pos); 516 remainingSpan -= spanSize; 517 layoutState.mCurrentPosition += layoutState.mItemDirection; 518 count++; 519 } 520 } 521 522 @Override layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result)523 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, 524 LayoutState layoutState, LayoutChunkResult result) { 525 final int otherDirSpecMode = mOrientationHelper.getModeInOther(); 526 final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY; 527 final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0; 528 // if grid layout's dimensions are not specified, let the new row change the measurements 529 // This is not perfect since we not covering all rows but still solves an important case 530 // where they may have a header row which should be laid out according to children. 531 if (flexibleInOtherDir) { 532 updateMeasurements(); // reset measurements 533 } 534 final boolean layingOutInPrimaryDirection = 535 layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL; 536 int count = 0; 537 int consumedSpanCount = 0; 538 int remainingSpan = mSpanCount; 539 if (!layingOutInPrimaryDirection) { 540 int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition); 541 int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition); 542 remainingSpan = itemSpanIndex + itemSpanSize; 543 } 544 while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { 545 int pos = layoutState.mCurrentPosition; 546 final int spanSize = getSpanSize(recycler, state, pos); 547 if (spanSize > mSpanCount) { 548 throw new IllegalArgumentException("Item at position " + pos + " requires " 549 + spanSize + " spans but GridLayoutManager has only " + mSpanCount 550 + " spans."); 551 } 552 remainingSpan -= spanSize; 553 if (remainingSpan < 0) { 554 break; // item did not fit into this row or column 555 } 556 View view = layoutState.next(recycler); 557 if (view == null) { 558 break; 559 } 560 consumedSpanCount += spanSize; 561 mSet[count] = view; 562 count++; 563 } 564 565 if (count == 0) { 566 result.mFinished = true; 567 return; 568 } 569 570 int maxSize = 0; 571 float maxSizeInOther = 0; // use a float to get size per span 572 573 // we should assign spans before item decor offsets are calculated 574 assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection); 575 for (int i = 0; i < count; i++) { 576 View view = mSet[i]; 577 if (layoutState.mScrapList == null) { 578 if (layingOutInPrimaryDirection) { 579 addView(view); 580 } else { 581 addView(view, 0); 582 } 583 } else { 584 if (layingOutInPrimaryDirection) { 585 addDisappearingView(view); 586 } else { 587 addDisappearingView(view, 0); 588 } 589 } 590 calculateItemDecorationsForChild(view, mDecorInsets); 591 592 measureChild(view, otherDirSpecMode, false); 593 final int size = mOrientationHelper.getDecoratedMeasurement(view); 594 if (size > maxSize) { 595 maxSize = size; 596 } 597 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 598 final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) 599 / lp.mSpanSize; 600 if (otherSize > maxSizeInOther) { 601 maxSizeInOther = otherSize; 602 } 603 } 604 if (flexibleInOtherDir) { 605 // re-distribute columns 606 guessMeasurement(maxSizeInOther, currentOtherDirSize); 607 // now we should re-measure any item that was match parent. 608 maxSize = 0; 609 for (int i = 0; i < count; i++) { 610 View view = mSet[i]; 611 measureChild(view, View.MeasureSpec.EXACTLY, true); 612 final int size = mOrientationHelper.getDecoratedMeasurement(view); 613 if (size > maxSize) { 614 maxSize = size; 615 } 616 } 617 } 618 619 // Views that did not measure the maxSize has to be re-measured 620 // We will stop doing this once we introduce Gravity in the GLM layout params 621 for (int i = 0; i < count; i++) { 622 final View view = mSet[i]; 623 if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) { 624 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 625 final Rect decorInsets = lp.mDecorInsets; 626 final int verticalInsets = decorInsets.top + decorInsets.bottom 627 + lp.topMargin + lp.bottomMargin; 628 final int horizontalInsets = decorInsets.left + decorInsets.right 629 + lp.leftMargin + lp.rightMargin; 630 final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); 631 final int wSpec; 632 final int hSpec; 633 if (mOrientation == VERTICAL) { 634 wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY, 635 horizontalInsets, lp.width, false); 636 hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets, 637 View.MeasureSpec.EXACTLY); 638 } else { 639 wSpec = View.MeasureSpec.makeMeasureSpec(maxSize - horizontalInsets, 640 View.MeasureSpec.EXACTLY); 641 hSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY, 642 verticalInsets, lp.height, false); 643 } 644 measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true); 645 } 646 } 647 648 result.mConsumed = maxSize; 649 650 int left = 0, right = 0, top = 0, bottom = 0; 651 if (mOrientation == VERTICAL) { 652 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 653 bottom = layoutState.mOffset; 654 top = bottom - maxSize; 655 } else { 656 top = layoutState.mOffset; 657 bottom = top + maxSize; 658 } 659 } else { 660 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 661 right = layoutState.mOffset; 662 left = right - maxSize; 663 } else { 664 left = layoutState.mOffset; 665 right = left + maxSize; 666 } 667 } 668 for (int i = 0; i < count; i++) { 669 View view = mSet[i]; 670 LayoutParams params = (LayoutParams) view.getLayoutParams(); 671 if (mOrientation == VERTICAL) { 672 if (isLayoutRTL()) { 673 right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex]; 674 left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); 675 } else { 676 left = getPaddingLeft() + mCachedBorders[params.mSpanIndex]; 677 right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); 678 } 679 } else { 680 top = getPaddingTop() + mCachedBorders[params.mSpanIndex]; 681 bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); 682 } 683 // We calculate everything with View's bounding box (which includes decor and margins) 684 // To calculate correct layout position, we subtract margins. 685 layoutDecoratedWithMargins(view, left, top, right, bottom); 686 if (DEBUG) { 687 Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" 688 + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" 689 + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin) 690 + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize); 691 } 692 // Consume the available space if the view is not removed OR changed 693 if (params.isItemRemoved() || params.isItemChanged()) { 694 result.mIgnoreConsumed = true; 695 } 696 result.mFocusable |= view.hasFocusable(); 697 } 698 Arrays.fill(mSet, null); 699 } 700 701 /** 702 * Measures a child with currently known information. This is not necessarily the child's final 703 * measurement. (see fillChunk for details). 704 * 705 * @param view The child view to be measured 706 * @param otherDirParentSpecMode The RV measure spec that should be used in the secondary 707 * orientation 708 * @param alreadyMeasured True if we've already measured this view once 709 */ measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured)710 private void measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured) { 711 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 712 final Rect decorInsets = lp.mDecorInsets; 713 final int verticalInsets = decorInsets.top + decorInsets.bottom 714 + lp.topMargin + lp.bottomMargin; 715 final int horizontalInsets = decorInsets.left + decorInsets.right 716 + lp.leftMargin + lp.rightMargin; 717 final int availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); 718 final int wSpec; 719 final int hSpec; 720 if (mOrientation == VERTICAL) { 721 wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode, 722 horizontalInsets, lp.width, false); 723 hSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(), 724 verticalInsets, lp.height, true); 725 } else { 726 hSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode, 727 verticalInsets, lp.height, false); 728 wSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getWidthMode(), 729 horizontalInsets, lp.width, true); 730 } 731 measureChildWithDecorationsAndMargin(view, wSpec, hSpec, alreadyMeasured); 732 } 733 734 /** 735 * This is called after laying out a row (if vertical) or a column (if horizontal) when the 736 * RecyclerView does not have exact measurement specs. 737 * <p> 738 * Here we try to assign a best guess width or height and re-do the layout to update other 739 * views that wanted to MATCH_PARENT in the non-scroll orientation. 740 * 741 * @param maxSizeInOther The maximum size per span ratio from the measurement of the children. 742 * @param currentOtherDirSize The size before this layout chunk. There is no reason to go below. 743 */ guessMeasurement(float maxSizeInOther, int currentOtherDirSize)744 private void guessMeasurement(float maxSizeInOther, int currentOtherDirSize) { 745 final int contentSize = Math.round(maxSizeInOther * mSpanCount); 746 // always re-calculate because borders were stretched during the fill 747 calculateItemBorders(Math.max(contentSize, currentOtherDirSize)); 748 } 749 measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, boolean alreadyMeasured)750 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, 751 boolean alreadyMeasured) { 752 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); 753 final boolean measure; 754 if (alreadyMeasured) { 755 measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp); 756 } else { 757 measure = shouldMeasureChild(child, widthSpec, heightSpec, lp); 758 } 759 if (measure) { 760 child.measure(widthSpec, heightSpec); 761 } 762 } 763 assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, int consumedSpanCount, boolean layingOutInPrimaryDirection)764 private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, 765 int consumedSpanCount, boolean layingOutInPrimaryDirection) { 766 // spans are always assigned from 0 to N no matter if it is RTL or not. 767 // RTL is used only when positioning the view. 768 int span, start, end, diff; 769 // make sure we traverse from min position to max position 770 if (layingOutInPrimaryDirection) { 771 start = 0; 772 end = count; 773 diff = 1; 774 } else { 775 start = count - 1; 776 end = -1; 777 diff = -1; 778 } 779 span = 0; 780 for (int i = start; i != end; i += diff) { 781 View view = mSet[i]; 782 LayoutParams params = (LayoutParams) view.getLayoutParams(); 783 params.mSpanSize = getSpanSize(recycler, state, getPosition(view)); 784 params.mSpanIndex = span; 785 span += params.mSpanSize; 786 } 787 } 788 789 /** 790 * Returns the number of spans laid out by this grid. 791 * 792 * @return The number of spans 793 * @see #setSpanCount(int) 794 */ getSpanCount()795 public int getSpanCount() { 796 return mSpanCount; 797 } 798 799 /** 800 * Sets the number of spans to be laid out. 801 * <p> 802 * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns. 803 * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows. 804 * 805 * @param spanCount The total number of spans in the grid 806 * @see #getSpanCount() 807 */ setSpanCount(int spanCount)808 public void setSpanCount(int spanCount) { 809 if (spanCount == mSpanCount) { 810 return; 811 } 812 mPendingSpanCountChange = true; 813 if (spanCount < 1) { 814 throw new IllegalArgumentException("Span count should be at least 1. Provided " 815 + spanCount); 816 } 817 mSpanCount = spanCount; 818 mSpanSizeLookup.invalidateSpanIndexCache(); 819 requestLayout(); 820 } 821 822 /** 823 * A helper class to provide the number of spans each item occupies. 824 * <p> 825 * Default implementation sets each item to occupy exactly 1 span. 826 * 827 * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup) 828 */ 829 public abstract static class SpanSizeLookup { 830 831 final SparseIntArray mSpanIndexCache = new SparseIntArray(); 832 833 private boolean mCacheSpanIndices = false; 834 835 /** 836 * Returns the number of span occupied by the item at <code>position</code>. 837 * 838 * @param position The adapter position of the item 839 * @return The number of spans occupied by the item at the provided position 840 */ getSpanSize(int position)841 public abstract int getSpanSize(int position); 842 843 /** 844 * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or 845 * not. By default these values are not cached. If you are not overriding 846 * {@link #getSpanIndex(int, int)}, you should set this to true for better performance. 847 * 848 * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not. 849 */ setSpanIndexCacheEnabled(boolean cacheSpanIndices)850 public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) { 851 mCacheSpanIndices = cacheSpanIndices; 852 } 853 854 /** 855 * Clears the span index cache. GridLayoutManager automatically calls this method when 856 * adapter changes occur. 857 */ invalidateSpanIndexCache()858 public void invalidateSpanIndexCache() { 859 mSpanIndexCache.clear(); 860 } 861 862 /** 863 * Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not. 864 * 865 * @return True if results of {@link #getSpanIndex(int, int)} are cached. 866 */ isSpanIndexCacheEnabled()867 public boolean isSpanIndexCacheEnabled() { 868 return mCacheSpanIndices; 869 } 870 getCachedSpanIndex(int position, int spanCount)871 int getCachedSpanIndex(int position, int spanCount) { 872 if (!mCacheSpanIndices) { 873 return getSpanIndex(position, spanCount); 874 } 875 final int existing = mSpanIndexCache.get(position, -1); 876 if (existing != -1) { 877 return existing; 878 } 879 final int value = getSpanIndex(position, spanCount); 880 mSpanIndexCache.put(position, value); 881 return value; 882 } 883 884 /** 885 * Returns the final span index of the provided position. 886 * <p> 887 * If you have a faster way to calculate span index for your items, you should override 888 * this method. Otherwise, you should enable span index cache 889 * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is 890 * disabled, default implementation traverses all items from 0 to 891 * <code>position</code>. When caching is enabled, it calculates from the closest cached 892 * value before the <code>position</code>. 893 * <p> 894 * If you override this method, you need to make sure it is consistent with 895 * {@link #getSpanSize(int)}. GridLayoutManager does not call this method for 896 * each item. It is called only for the reference item and rest of the items 897 * are assigned to spans based on the reference item. For example, you cannot assign a 898 * position to span 2 while span 1 is empty. 899 * <p> 900 * Note that span offsets always start with 0 and are not affected by RTL. 901 * 902 * @param position The position of the item 903 * @param spanCount The total number of spans in the grid 904 * @return The final span position of the item. Should be between 0 (inclusive) and 905 * <code>spanCount</code>(exclusive) 906 */ getSpanIndex(int position, int spanCount)907 public int getSpanIndex(int position, int spanCount) { 908 int positionSpanSize = getSpanSize(position); 909 if (positionSpanSize == spanCount) { 910 return 0; // quick return for full-span items 911 } 912 int span = 0; 913 int startPos = 0; 914 // If caching is enabled, try to jump 915 if (mCacheSpanIndices && mSpanIndexCache.size() > 0) { 916 int prevKey = findReferenceIndexFromCache(position); 917 if (prevKey >= 0) { 918 span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey); 919 startPos = prevKey + 1; 920 } 921 } 922 for (int i = startPos; i < position; i++) { 923 int size = getSpanSize(i); 924 span += size; 925 if (span == spanCount) { 926 span = 0; 927 } else if (span > spanCount) { 928 // did not fit, moving to next row / column 929 span = size; 930 } 931 } 932 if (span + positionSpanSize <= spanCount) { 933 return span; 934 } 935 return 0; 936 } 937 findReferenceIndexFromCache(int position)938 int findReferenceIndexFromCache(int position) { 939 int lo = 0; 940 int hi = mSpanIndexCache.size() - 1; 941 942 while (lo <= hi) { 943 final int mid = (lo + hi) >>> 1; 944 final int midVal = mSpanIndexCache.keyAt(mid); 945 if (midVal < position) { 946 lo = mid + 1; 947 } else { 948 hi = mid - 1; 949 } 950 } 951 int index = lo - 1; 952 if (index >= 0 && index < mSpanIndexCache.size()) { 953 return mSpanIndexCache.keyAt(index); 954 } 955 return -1; 956 } 957 958 /** 959 * Returns the index of the group this position belongs. 960 * <p> 961 * For example, if grid has 3 columns and each item occupies 1 span, span group index 962 * for item 1 will be 0, item 5 will be 1. 963 * 964 * @param adapterPosition The position in adapter 965 * @param spanCount The total number of spans in the grid 966 * @return The index of the span group including the item at the given adapter position 967 */ getSpanGroupIndex(int adapterPosition, int spanCount)968 public int getSpanGroupIndex(int adapterPosition, int spanCount) { 969 int span = 0; 970 int group = 0; 971 int positionSpanSize = getSpanSize(adapterPosition); 972 for (int i = 0; i < adapterPosition; i++) { 973 int size = getSpanSize(i); 974 span += size; 975 if (span == spanCount) { 976 span = 0; 977 group++; 978 } else if (span > spanCount) { 979 // did not fit, moving to next row / column 980 span = size; 981 group++; 982 } 983 } 984 if (span + positionSpanSize > spanCount) { 985 group++; 986 } 987 return group; 988 } 989 } 990 991 @Override onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state)992 public View onFocusSearchFailed(View focused, int focusDirection, 993 RecyclerView.Recycler recycler, RecyclerView.State state) { 994 View prevFocusedChild = findContainingItemView(focused); 995 if (prevFocusedChild == null) { 996 return null; 997 } 998 LayoutParams lp = (LayoutParams) prevFocusedChild.getLayoutParams(); 999 final int prevSpanStart = lp.mSpanIndex; 1000 final int prevSpanEnd = lp.mSpanIndex + lp.mSpanSize; 1001 View view = super.onFocusSearchFailed(focused, focusDirection, recycler, state); 1002 if (view == null) { 1003 return null; 1004 } 1005 // LinearLayoutManager finds the last child. What we want is the child which has the same 1006 // spanIndex. 1007 final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection); 1008 final boolean ascend = (layoutDir == LayoutState.LAYOUT_END) != mShouldReverseLayout; 1009 final int start, inc, limit; 1010 if (ascend) { 1011 start = getChildCount() - 1; 1012 inc = -1; 1013 limit = -1; 1014 } else { 1015 start = 0; 1016 inc = 1; 1017 limit = getChildCount(); 1018 } 1019 final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); 1020 1021 // The focusable candidate to be picked if no perfect focusable candidate is found. 1022 // The best focusable candidate is the one with the highest amount of span overlap with 1023 // the currently focused view. 1024 View focusableWeakCandidate = null; // somewhat matches but not strong 1025 int focusableWeakCandidateSpanIndex = -1; 1026 int focusableWeakCandidateOverlap = 0; // how many spans overlap 1027 1028 // The unfocusable candidate to become visible on the screen next, if no perfect or 1029 // weak focusable candidates are found to receive focus next. 1030 // We are only interested in partially visible unfocusable views. These are views that are 1031 // not fully visible, that is either partially overlapping, or out-of-bounds and right below 1032 // or above RV's padded bounded area. The best unfocusable candidate is the one with the 1033 // highest amount of span overlap with the currently focused view. 1034 View unfocusableWeakCandidate = null; // somewhat matches but not strong 1035 int unfocusableWeakCandidateSpanIndex = -1; 1036 int unfocusableWeakCandidateOverlap = 0; // how many spans overlap 1037 1038 // The span group index of the start child. This indicates the span group index of the 1039 // next focusable item to receive focus, if a focusable item within the same span group 1040 // exists. Any focusable item beyond this group index are not relevant since they 1041 // were already stored in the layout before onFocusSearchFailed call and were not picked 1042 // by the focusSearch algorithm. 1043 int focusableSpanGroupIndex = getSpanGroupIndex(recycler, state, start); 1044 for (int i = start; i != limit; i += inc) { 1045 int spanGroupIndex = getSpanGroupIndex(recycler, state, i); 1046 View candidate = getChildAt(i); 1047 if (candidate == prevFocusedChild) { 1048 break; 1049 } 1050 1051 if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) { 1052 // We are past the allowable span group index for the next focusable item. 1053 // The search only continues if no focusable weak candidates have been found up 1054 // until this point, in order to find the best unfocusable candidate to become 1055 // visible on the screen next. 1056 if (focusableWeakCandidate != null) { 1057 break; 1058 } 1059 continue; 1060 } 1061 1062 final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams(); 1063 final int candidateStart = candidateLp.mSpanIndex; 1064 final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize; 1065 if (candidate.hasFocusable() && candidateStart == prevSpanStart 1066 && candidateEnd == prevSpanEnd) { 1067 return candidate; // perfect match 1068 } 1069 boolean assignAsWeek = false; 1070 if ((candidate.hasFocusable() && focusableWeakCandidate == null) 1071 || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) { 1072 assignAsWeek = true; 1073 } else { 1074 int maxStart = Math.max(candidateStart, prevSpanStart); 1075 int minEnd = Math.min(candidateEnd, prevSpanEnd); 1076 int overlap = minEnd - maxStart; 1077 if (candidate.hasFocusable()) { 1078 if (overlap > focusableWeakCandidateOverlap) { 1079 assignAsWeek = true; 1080 } else if (overlap == focusableWeakCandidateOverlap 1081 && preferLastSpan == (candidateStart 1082 > focusableWeakCandidateSpanIndex)) { 1083 assignAsWeek = true; 1084 } 1085 } else if (focusableWeakCandidate == null 1086 && isViewPartiallyVisible(candidate, false, true)) { 1087 if (overlap > unfocusableWeakCandidateOverlap) { 1088 assignAsWeek = true; 1089 } else if (overlap == unfocusableWeakCandidateOverlap 1090 && preferLastSpan == (candidateStart 1091 > unfocusableWeakCandidateSpanIndex)) { 1092 assignAsWeek = true; 1093 } 1094 } 1095 } 1096 1097 if (assignAsWeek) { 1098 if (candidate.hasFocusable()) { 1099 focusableWeakCandidate = candidate; 1100 focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; 1101 focusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) 1102 - Math.max(candidateStart, prevSpanStart); 1103 } else { 1104 unfocusableWeakCandidate = candidate; 1105 unfocusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; 1106 unfocusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) 1107 - Math.max(candidateStart, prevSpanStart); 1108 } 1109 } 1110 } 1111 return (focusableWeakCandidate != null) ? focusableWeakCandidate : unfocusableWeakCandidate; 1112 } 1113 1114 @Override supportsPredictiveItemAnimations()1115 public boolean supportsPredictiveItemAnimations() { 1116 return mPendingSavedState == null && !mPendingSpanCountChange; 1117 } 1118 1119 /** 1120 * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span. 1121 */ 1122 public static final class DefaultSpanSizeLookup extends SpanSizeLookup { 1123 1124 @Override getSpanSize(int position)1125 public int getSpanSize(int position) { 1126 return 1; 1127 } 1128 1129 @Override getSpanIndex(int position, int spanCount)1130 public int getSpanIndex(int position, int spanCount) { 1131 return position % spanCount; 1132 } 1133 } 1134 1135 /** 1136 * LayoutParams used by GridLayoutManager. 1137 * <p> 1138 * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the 1139 * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is 1140 * expected to fill all of the space given to it. 1141 */ 1142 public static class LayoutParams extends RecyclerView.LayoutParams { 1143 1144 /** 1145 * Span Id for Views that are not laid out yet. 1146 */ 1147 public static final int INVALID_SPAN_ID = -1; 1148 1149 int mSpanIndex = INVALID_SPAN_ID; 1150 1151 int mSpanSize = 0; 1152 LayoutParams(Context c, AttributeSet attrs)1153 public LayoutParams(Context c, AttributeSet attrs) { 1154 super(c, attrs); 1155 } 1156 LayoutParams(int width, int height)1157 public LayoutParams(int width, int height) { 1158 super(width, height); 1159 } 1160 LayoutParams(ViewGroup.MarginLayoutParams source)1161 public LayoutParams(ViewGroup.MarginLayoutParams source) { 1162 super(source); 1163 } 1164 LayoutParams(ViewGroup.LayoutParams source)1165 public LayoutParams(ViewGroup.LayoutParams source) { 1166 super(source); 1167 } 1168 LayoutParams(RecyclerView.LayoutParams source)1169 public LayoutParams(RecyclerView.LayoutParams source) { 1170 super(source); 1171 } 1172 1173 /** 1174 * Returns the current span index of this View. If the View is not laid out yet, the return 1175 * value is <code>undefined</code>. 1176 * <p> 1177 * Starting with RecyclerView <b>24.2.0</b>, span indices are always indexed from position 0 1178 * even if the layout is RTL. In a vertical GridLayoutManager, <b>leftmost</b> span is span 1179 * 0 if the layout is <b>LTR</b> and <b>rightmost</b> span is span 0 if the layout is 1180 * <b>RTL</b>. Prior to 24.2.0, it was the opposite which was conflicting with 1181 * {@link SpanSizeLookup#getSpanIndex(int, int)}. 1182 * <p> 1183 * If the View occupies multiple spans, span with the minimum index is returned. 1184 * 1185 * @return The span index of the View. 1186 */ getSpanIndex()1187 public int getSpanIndex() { 1188 return mSpanIndex; 1189 } 1190 1191 /** 1192 * Returns the number of spans occupied by this View. If the View not laid out yet, the 1193 * return value is <code>undefined</code>. 1194 * 1195 * @return The number of spans occupied by this View. 1196 */ getSpanSize()1197 public int getSpanSize() { 1198 return mSpanSize; 1199 } 1200 } 1201 1202 } 1203