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 layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result)508 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, 509 LayoutState layoutState, LayoutChunkResult result) { 510 final int otherDirSpecMode = mOrientationHelper.getModeInOther(); 511 final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY; 512 final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0; 513 // if grid layout's dimensions are not specified, let the new row change the measurements 514 // This is not perfect since we not covering all rows but still solves an important case 515 // where they may have a header row which should be laid out according to children. 516 if (flexibleInOtherDir) { 517 updateMeasurements(); // reset measurements 518 } 519 final boolean layingOutInPrimaryDirection = 520 layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL; 521 int count = 0; 522 int consumedSpanCount = 0; 523 int remainingSpan = mSpanCount; 524 if (!layingOutInPrimaryDirection) { 525 int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition); 526 int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition); 527 remainingSpan = itemSpanIndex + itemSpanSize; 528 } 529 while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { 530 int pos = layoutState.mCurrentPosition; 531 final int spanSize = getSpanSize(recycler, state, pos); 532 if (spanSize > mSpanCount) { 533 throw new IllegalArgumentException("Item at position " + pos + " requires " + 534 spanSize + " spans but GridLayoutManager has only " + mSpanCount 535 + " spans."); 536 } 537 remainingSpan -= spanSize; 538 if (remainingSpan < 0) { 539 break; // item did not fit into this row or column 540 } 541 View view = layoutState.next(recycler); 542 if (view == null) { 543 break; 544 } 545 consumedSpanCount += spanSize; 546 mSet[count] = view; 547 count++; 548 } 549 550 if (count == 0) { 551 result.mFinished = true; 552 return; 553 } 554 555 int maxSize = 0; 556 float maxSizeInOther = 0; // use a float to get size per span 557 558 // we should assign spans before item decor offsets are calculated 559 assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection); 560 for (int i = 0; i < count; i++) { 561 View view = mSet[i]; 562 if (layoutState.mScrapList == null) { 563 if (layingOutInPrimaryDirection) { 564 addView(view); 565 } else { 566 addView(view, 0); 567 } 568 } else { 569 if (layingOutInPrimaryDirection) { 570 addDisappearingView(view); 571 } else { 572 addDisappearingView(view, 0); 573 } 574 } 575 calculateItemDecorationsForChild(view, mDecorInsets); 576 577 measureChild(view, otherDirSpecMode, false); 578 final int size = mOrientationHelper.getDecoratedMeasurement(view); 579 if (size > maxSize) { 580 maxSize = size; 581 } 582 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 583 final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) / 584 lp.mSpanSize; 585 if (otherSize > maxSizeInOther) { 586 maxSizeInOther = otherSize; 587 } 588 } 589 if (flexibleInOtherDir) { 590 // re-distribute columns 591 guessMeasurement(maxSizeInOther, currentOtherDirSize); 592 // now we should re-measure any item that was match parent. 593 maxSize = 0; 594 for (int i = 0; i < count; i++) { 595 View view = mSet[i]; 596 measureChild(view, View.MeasureSpec.EXACTLY, true); 597 final int size = mOrientationHelper.getDecoratedMeasurement(view); 598 if (size > maxSize) { 599 maxSize = size; 600 } 601 } 602 } 603 604 // Views that did not measure the maxSize has to be re-measured 605 // We will stop doing this once we introduce Gravity in the GLM layout params 606 for (int i = 0; i < count; i ++) { 607 final View view = mSet[i]; 608 if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) { 609 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 610 final Rect decorInsets = lp.mDecorInsets; 611 final int verticalInsets = decorInsets.top + decorInsets.bottom 612 + lp.topMargin + lp.bottomMargin; 613 final int horizontalInsets = decorInsets.left + decorInsets.right 614 + lp.leftMargin + lp.rightMargin; 615 final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); 616 final int wSpec; 617 final int hSpec; 618 if (mOrientation == VERTICAL) { 619 wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY, 620 horizontalInsets, lp.width, false); 621 hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets, 622 View.MeasureSpec.EXACTLY); 623 } else { 624 wSpec = View.MeasureSpec.makeMeasureSpec(maxSize - horizontalInsets, 625 View.MeasureSpec.EXACTLY); 626 hSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY, 627 verticalInsets, lp.height, false); 628 } 629 measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true); 630 } 631 } 632 633 result.mConsumed = maxSize; 634 635 int left = 0, right = 0, top = 0, bottom = 0; 636 if (mOrientation == VERTICAL) { 637 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 638 bottom = layoutState.mOffset; 639 top = bottom - maxSize; 640 } else { 641 top = layoutState.mOffset; 642 bottom = top + maxSize; 643 } 644 } else { 645 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 646 right = layoutState.mOffset; 647 left = right - maxSize; 648 } else { 649 left = layoutState.mOffset; 650 right = left + maxSize; 651 } 652 } 653 for (int i = 0; i < count; i++) { 654 View view = mSet[i]; 655 LayoutParams params = (LayoutParams) view.getLayoutParams(); 656 if (mOrientation == VERTICAL) { 657 if (isLayoutRTL()) { 658 right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex]; 659 left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); 660 } else { 661 left = getPaddingLeft() + mCachedBorders[params.mSpanIndex]; 662 right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); 663 } 664 } else { 665 top = getPaddingTop() + mCachedBorders[params.mSpanIndex]; 666 bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); 667 } 668 // We calculate everything with View's bounding box (which includes decor and margins) 669 // To calculate correct layout position, we subtract margins. 670 layoutDecoratedWithMargins(view, left, top, right, bottom); 671 if (DEBUG) { 672 Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" 673 + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" 674 + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin) 675 + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize); 676 } 677 // Consume the available space if the view is not removed OR changed 678 if (params.isItemRemoved() || params.isItemChanged()) { 679 result.mIgnoreConsumed = true; 680 } 681 result.mFocusable |= view.isFocusable(); 682 } 683 Arrays.fill(mSet, null); 684 } 685 686 /** 687 * Measures a child with currently known information. This is not necessarily the child's final 688 * measurement. (see fillChunk for details). 689 * 690 * @param view The child view to be measured 691 * @param otherDirParentSpecMode The RV measure spec that should be used in the secondary 692 * orientation 693 * @param alreadyMeasured True if we've already measured this view once 694 */ measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured)695 private void measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured) { 696 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 697 final Rect decorInsets = lp.mDecorInsets; 698 final int verticalInsets = decorInsets.top + decorInsets.bottom 699 + lp.topMargin + lp.bottomMargin; 700 final int horizontalInsets = decorInsets.left + decorInsets.right 701 + lp.leftMargin + lp.rightMargin; 702 final int availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); 703 final int wSpec; 704 final int hSpec; 705 if (mOrientation == VERTICAL) { 706 wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode, 707 horizontalInsets, lp.width, false); 708 hSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(), 709 verticalInsets, lp.height, true); 710 } else { 711 hSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode, 712 verticalInsets, lp.height, false); 713 wSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getWidthMode(), 714 horizontalInsets, lp.width, true); 715 } 716 measureChildWithDecorationsAndMargin(view, wSpec, hSpec, alreadyMeasured); 717 } 718 719 /** 720 * This is called after laying out a row (if vertical) or a column (if horizontal) when the 721 * RecyclerView does not have exact measurement specs. 722 * <p> 723 * Here we try to assign a best guess width or height and re-do the layout to update other 724 * views that wanted to MATCH_PARENT in the non-scroll orientation. 725 * 726 * @param maxSizeInOther The maximum size per span ratio from the measurement of the children. 727 * @param currentOtherDirSize The size before this layout chunk. There is no reason to go below. 728 */ guessMeasurement(float maxSizeInOther, int currentOtherDirSize)729 private void guessMeasurement(float maxSizeInOther, int currentOtherDirSize) { 730 final int contentSize = Math.round(maxSizeInOther * mSpanCount); 731 // always re-calculate because borders were stretched during the fill 732 calculateItemBorders(Math.max(contentSize, currentOtherDirSize)); 733 } 734 measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, boolean alreadyMeasured)735 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, 736 boolean alreadyMeasured) { 737 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); 738 final boolean measure; 739 if (alreadyMeasured) { 740 measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp); 741 } else { 742 measure = shouldMeasureChild(child, widthSpec, heightSpec, lp); 743 } 744 if (measure) { 745 child.measure(widthSpec, heightSpec); 746 } 747 } 748 assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, int consumedSpanCount, boolean layingOutInPrimaryDirection)749 private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, 750 int consumedSpanCount, boolean layingOutInPrimaryDirection) { 751 // spans are always assigned from 0 to N no matter if it is RTL or not. 752 // RTL is used only when positioning the view. 753 int span, start, end, diff; 754 // make sure we traverse from min position to max position 755 if (layingOutInPrimaryDirection) { 756 start = 0; 757 end = count; 758 diff = 1; 759 } else { 760 start = count - 1; 761 end = -1; 762 diff = -1; 763 } 764 span = 0; 765 for (int i = start; i != end; i += diff) { 766 View view = mSet[i]; 767 LayoutParams params = (LayoutParams) view.getLayoutParams(); 768 params.mSpanSize = getSpanSize(recycler, state, getPosition(view)); 769 params.mSpanIndex = span; 770 span += params.mSpanSize; 771 } 772 } 773 774 /** 775 * Returns the number of spans laid out by this grid. 776 * 777 * @return The number of spans 778 * @see #setSpanCount(int) 779 */ getSpanCount()780 public int getSpanCount() { 781 return mSpanCount; 782 } 783 784 /** 785 * Sets the number of spans to be laid out. 786 * <p> 787 * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns. 788 * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows. 789 * 790 * @param spanCount The total number of spans in the grid 791 * @see #getSpanCount() 792 */ setSpanCount(int spanCount)793 public void setSpanCount(int spanCount) { 794 if (spanCount == mSpanCount) { 795 return; 796 } 797 mPendingSpanCountChange = true; 798 if (spanCount < 1) { 799 throw new IllegalArgumentException("Span count should be at least 1. Provided " 800 + spanCount); 801 } 802 mSpanCount = spanCount; 803 mSpanSizeLookup.invalidateSpanIndexCache(); 804 requestLayout(); 805 } 806 807 /** 808 * A helper class to provide the number of spans each item occupies. 809 * <p> 810 * Default implementation sets each item to occupy exactly 1 span. 811 * 812 * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup) 813 */ 814 public static abstract class SpanSizeLookup { 815 816 final SparseIntArray mSpanIndexCache = new SparseIntArray(); 817 818 private boolean mCacheSpanIndices = false; 819 820 /** 821 * Returns the number of span occupied by the item at <code>position</code>. 822 * 823 * @param position The adapter position of the item 824 * @return The number of spans occupied by the item at the provided position 825 */ getSpanSize(int position)826 abstract public int getSpanSize(int position); 827 828 /** 829 * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or 830 * not. By default these values are not cached. If you are not overriding 831 * {@link #getSpanIndex(int, int)}, you should set this to true for better performance. 832 * 833 * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not. 834 */ setSpanIndexCacheEnabled(boolean cacheSpanIndices)835 public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) { 836 mCacheSpanIndices = cacheSpanIndices; 837 } 838 839 /** 840 * Clears the span index cache. GridLayoutManager automatically calls this method when 841 * adapter changes occur. 842 */ invalidateSpanIndexCache()843 public void invalidateSpanIndexCache() { 844 mSpanIndexCache.clear(); 845 } 846 847 /** 848 * Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not. 849 * 850 * @return True if results of {@link #getSpanIndex(int, int)} are cached. 851 */ isSpanIndexCacheEnabled()852 public boolean isSpanIndexCacheEnabled() { 853 return mCacheSpanIndices; 854 } 855 getCachedSpanIndex(int position, int spanCount)856 int getCachedSpanIndex(int position, int spanCount) { 857 if (!mCacheSpanIndices) { 858 return getSpanIndex(position, spanCount); 859 } 860 final int existing = mSpanIndexCache.get(position, -1); 861 if (existing != -1) { 862 return existing; 863 } 864 final int value = getSpanIndex(position, spanCount); 865 mSpanIndexCache.put(position, value); 866 return value; 867 } 868 869 /** 870 * Returns the final span index of the provided position. 871 * <p> 872 * If you have a faster way to calculate span index for your items, you should override 873 * this method. Otherwise, you should enable span index cache 874 * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is 875 * disabled, default implementation traverses all items from 0 to 876 * <code>position</code>. When caching is enabled, it calculates from the closest cached 877 * value before the <code>position</code>. 878 * <p> 879 * If you override this method, you need to make sure it is consistent with 880 * {@link #getSpanSize(int)}. GridLayoutManager does not call this method for 881 * each item. It is called only for the reference item and rest of the items 882 * are assigned to spans based on the reference item. For example, you cannot assign a 883 * position to span 2 while span 1 is empty. 884 * <p> 885 * Note that span offsets always start with 0 and are not affected by RTL. 886 * 887 * @param position The position of the item 888 * @param spanCount The total number of spans in the grid 889 * @return The final span position of the item. Should be between 0 (inclusive) and 890 * <code>spanCount</code>(exclusive) 891 */ getSpanIndex(int position, int spanCount)892 public int getSpanIndex(int position, int spanCount) { 893 int positionSpanSize = getSpanSize(position); 894 if (positionSpanSize == spanCount) { 895 return 0; // quick return for full-span items 896 } 897 int span = 0; 898 int startPos = 0; 899 // If caching is enabled, try to jump 900 if (mCacheSpanIndices && mSpanIndexCache.size() > 0) { 901 int prevKey = findReferenceIndexFromCache(position); 902 if (prevKey >= 0) { 903 span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey); 904 startPos = prevKey + 1; 905 } 906 } 907 for (int i = startPos; i < position; i++) { 908 int size = getSpanSize(i); 909 span += size; 910 if (span == spanCount) { 911 span = 0; 912 } else if (span > spanCount) { 913 // did not fit, moving to next row / column 914 span = size; 915 } 916 } 917 if (span + positionSpanSize <= spanCount) { 918 return span; 919 } 920 return 0; 921 } 922 findReferenceIndexFromCache(int position)923 int findReferenceIndexFromCache(int position) { 924 int lo = 0; 925 int hi = mSpanIndexCache.size() - 1; 926 927 while (lo <= hi) { 928 final int mid = (lo + hi) >>> 1; 929 final int midVal = mSpanIndexCache.keyAt(mid); 930 if (midVal < position) { 931 lo = mid + 1; 932 } else { 933 hi = mid - 1; 934 } 935 } 936 int index = lo - 1; 937 if (index >= 0 && index < mSpanIndexCache.size()) { 938 return mSpanIndexCache.keyAt(index); 939 } 940 return -1; 941 } 942 943 /** 944 * Returns the index of the group this position belongs. 945 * <p> 946 * For example, if grid has 3 columns and each item occupies 1 span, span group index 947 * for item 1 will be 0, item 5 will be 1. 948 * 949 * @param adapterPosition The position in adapter 950 * @param spanCount The total number of spans in the grid 951 * @return The index of the span group including the item at the given adapter position 952 */ getSpanGroupIndex(int adapterPosition, int spanCount)953 public int getSpanGroupIndex(int adapterPosition, int spanCount) { 954 int span = 0; 955 int group = 0; 956 int positionSpanSize = getSpanSize(adapterPosition); 957 for (int i = 0; i < adapterPosition; i++) { 958 int size = getSpanSize(i); 959 span += size; 960 if (span == spanCount) { 961 span = 0; 962 group++; 963 } else if (span > spanCount) { 964 // did not fit, moving to next row / column 965 span = size; 966 group++; 967 } 968 } 969 if (span + positionSpanSize > spanCount) { 970 group++; 971 } 972 return group; 973 } 974 } 975 976 @Override onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state)977 public View onFocusSearchFailed(View focused, int focusDirection, 978 RecyclerView.Recycler recycler, RecyclerView.State state) { 979 View prevFocusedChild = findContainingItemView(focused); 980 if (prevFocusedChild == null) { 981 return null; 982 } 983 LayoutParams lp = (LayoutParams) prevFocusedChild.getLayoutParams(); 984 final int prevSpanStart = lp.mSpanIndex; 985 final int prevSpanEnd = lp.mSpanIndex + lp.mSpanSize; 986 View view = super.onFocusSearchFailed(focused, focusDirection, recycler, state); 987 if (view == null) { 988 return null; 989 } 990 // LinearLayoutManager finds the last child. What we want is the child which has the same 991 // spanIndex. 992 final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection); 993 final boolean ascend = (layoutDir == LayoutState.LAYOUT_END) != mShouldReverseLayout; 994 final int start, inc, limit; 995 if (ascend) { 996 start = getChildCount() - 1; 997 inc = -1; 998 limit = -1; 999 } else { 1000 start = 0; 1001 inc = 1; 1002 limit = getChildCount(); 1003 } 1004 final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); 1005 View weakCandidate = null; // somewhat matches but not strong 1006 int weakCandidateSpanIndex = -1; 1007 int weakCandidateOverlap = 0; // how many spans overlap 1008 1009 for (int i = start; i != limit; i += inc) { 1010 View candidate = getChildAt(i); 1011 if (candidate == prevFocusedChild) { 1012 break; 1013 } 1014 if (!candidate.isFocusable()) { 1015 continue; 1016 } 1017 final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams(); 1018 final int candidateStart = candidateLp.mSpanIndex; 1019 final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize; 1020 if (candidateStart == prevSpanStart && candidateEnd == prevSpanEnd) { 1021 return candidate; // perfect match 1022 } 1023 boolean assignAsWeek = false; 1024 if (weakCandidate == null) { 1025 assignAsWeek = true; 1026 } else { 1027 int maxStart = Math.max(candidateStart, prevSpanStart); 1028 int minEnd = Math.min(candidateEnd, prevSpanEnd); 1029 int overlap = minEnd - maxStart; 1030 if (overlap > weakCandidateOverlap) { 1031 assignAsWeek = true; 1032 } else if (overlap == weakCandidateOverlap && 1033 preferLastSpan == (candidateStart > weakCandidateSpanIndex)) { 1034 assignAsWeek = true; 1035 } 1036 } 1037 1038 if (assignAsWeek) { 1039 weakCandidate = candidate; 1040 weakCandidateSpanIndex = candidateLp.mSpanIndex; 1041 weakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) - 1042 Math.max(candidateStart, prevSpanStart); 1043 } 1044 } 1045 return weakCandidate; 1046 } 1047 1048 @Override supportsPredictiveItemAnimations()1049 public boolean supportsPredictiveItemAnimations() { 1050 return mPendingSavedState == null && !mPendingSpanCountChange; 1051 } 1052 1053 /** 1054 * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span. 1055 */ 1056 public static final class DefaultSpanSizeLookup extends SpanSizeLookup { 1057 1058 @Override getSpanSize(int position)1059 public int getSpanSize(int position) { 1060 return 1; 1061 } 1062 1063 @Override getSpanIndex(int position, int spanCount)1064 public int getSpanIndex(int position, int spanCount) { 1065 return position % spanCount; 1066 } 1067 } 1068 1069 /** 1070 * LayoutParams used by GridLayoutManager. 1071 * <p> 1072 * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the 1073 * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is 1074 * expected to fill all of the space given to it. 1075 */ 1076 public static class LayoutParams extends RecyclerView.LayoutParams { 1077 1078 /** 1079 * Span Id for Views that are not laid out yet. 1080 */ 1081 public static final int INVALID_SPAN_ID = -1; 1082 1083 private int mSpanIndex = INVALID_SPAN_ID; 1084 1085 private int mSpanSize = 0; 1086 LayoutParams(Context c, AttributeSet attrs)1087 public LayoutParams(Context c, AttributeSet attrs) { 1088 super(c, attrs); 1089 } 1090 LayoutParams(int width, int height)1091 public LayoutParams(int width, int height) { 1092 super(width, height); 1093 } 1094 LayoutParams(ViewGroup.MarginLayoutParams source)1095 public LayoutParams(ViewGroup.MarginLayoutParams source) { 1096 super(source); 1097 } 1098 LayoutParams(ViewGroup.LayoutParams source)1099 public LayoutParams(ViewGroup.LayoutParams source) { 1100 super(source); 1101 } 1102 LayoutParams(RecyclerView.LayoutParams source)1103 public LayoutParams(RecyclerView.LayoutParams source) { 1104 super(source); 1105 } 1106 1107 /** 1108 * Returns the current span index of this View. If the View is not laid out yet, the return 1109 * value is <code>undefined</code>. 1110 * <p> 1111 * Starting with RecyclerView <b>24.2.0</b>, span indices are always indexed from position 0 1112 * even if the layout is RTL. In a vertical GridLayoutManager, <b>leftmost</b> span is span 1113 * 0 if the layout is <b>LTR</b> and <b>rightmost</b> span is span 0 if the layout is 1114 * <b>RTL</b>. Prior to 24.2.0, it was the opposite which was conflicting with 1115 * {@link SpanSizeLookup#getSpanIndex(int, int)}. 1116 * <p> 1117 * If the View occupies multiple spans, span with the minimum index is returned. 1118 * 1119 * @return The span index of the View. 1120 */ getSpanIndex()1121 public int getSpanIndex() { 1122 return mSpanIndex; 1123 } 1124 1125 /** 1126 * Returns the number of spans occupied by this View. If the View not laid out yet, the 1127 * return value is <code>undefined</code>. 1128 * 1129 * @return The number of spans occupied by this View. 1130 */ getSpanSize()1131 public int getSpanSize() { 1132 return mSpanSize; 1133 } 1134 } 1135 1136 } 1137