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