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