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