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