GridLayoutManager.java revision b8403301bbec29129730f6cce3fe2fa3ee8e1e0b
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 */ 16package android.support.v7.widget; 17 18import android.content.Context; 19import android.graphics.Rect; 20import android.os.Parcel; 21import android.os.Parcelable; 22import android.util.AttributeSet; 23import android.util.Log; 24import android.view.View; 25import android.view.ViewGroup; 26 27import 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 */ 35public 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 int mSpanCount = DEFAULT_SPAN_COUNT; 41 /** 42 * The size of each span 43 */ 44 int mSizePerSpan; 45 /** 46 * Temporary array to keep views in layoutChunk method 47 */ 48 View[] mSet; 49 50 /** 51 * The measure spec for the perpendicular orientation to {@link #getOrientation()}. 52 */ 53 static int OTHER_DIM_SPEC = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 54 55 SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); 56 57 public GridLayoutManager(Context context, int spanCount) { 58 super(context); 59 setSpanCount(spanCount); 60 } 61 62 public GridLayoutManager(Context context, int spanCount, int orientation, 63 boolean reverseLayout) { 64 super(context, orientation, reverseLayout); 65 setSpanCount(spanCount); 66 } 67 68 @Override 69 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 70 super.onLayoutChildren(recycler, state); 71 if (DEBUG) { 72 validateChildOrder(); 73 } 74 } 75 76 @Override 77 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 78 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 79 ViewGroup.LayoutParams.WRAP_CONTENT); 80 } 81 82 @Override 83 public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 84 return new LayoutParams(c, attrs); 85 } 86 87 @Override 88 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 89 if (lp instanceof ViewGroup.MarginLayoutParams) { 90 return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 91 } else { 92 return new LayoutParams(lp); 93 } 94 } 95 96 @Override 97 public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 98 return lp instanceof LayoutParams; 99 } 100 101 /** 102 * Sets the source to get the number of spans occupied by each item in the adapter. 103 * 104 * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans 105 * occupied by each item 106 */ 107 public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) { 108 mSpanSizeLookup = spanSizeLookup; 109 } 110 111 /** 112 * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager. 113 * 114 * @return The current {@link SpanSizeLookup} used by the GridLayoutManager. 115 */ 116 public SpanSizeLookup getSpanSizeLookup() { 117 return mSpanSizeLookup; 118 } 119 120 private void updateMeasurements() { 121 int totalSpace; 122 if (getOrientation() == VERTICAL) { 123 totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); 124 } else { 125 totalSpace = getHeight() - getPaddingBottom() - getPaddingTop(); 126 } 127 mSizePerSpan = totalSpace / mSpanCount; 128 } 129 130 @Override 131 void onAnchorReady(LinearLayoutManager.AnchorInfo anchorInfo) { 132 super.onAnchorReady(anchorInfo); 133 updateMeasurements(); 134 int span = mSpanSizeLookup.getSpanIndex(anchorInfo.mPosition, mSpanCount); 135 if (span != 0) { //this is not affected by RTL 136 if (DEBUG) { 137 Log.d(TAG, "Correcting span for position " + anchorInfo.mPosition); 138 } 139 int prev = anchorInfo.mPosition - 1; 140 while (span > 0) { 141 span -= mSpanSizeLookup.getSpanSize(prev); 142 } 143 anchorInfo.mPosition = prev; 144 if (DEBUG) { 145 Log.d(TAG, "corrected anchor position to " + anchorInfo.mPosition); 146 } 147 } 148 if (mSet == null || mSet.length != mSpanCount) { 149 mSet = new View[mSpanCount]; 150 } 151 } 152 153 @Override 154 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, 155 LayoutState layoutState, LayoutChunkResult result) { 156 int count = 0; 157 int remainingSpan = mSpanCount; 158 while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { 159 int pos = layoutState.mCurrentPosition; 160 final int spanSize = mSpanSizeLookup.getSpanSize(pos); 161 remainingSpan -= spanSize; 162 if (remainingSpan < 0) { 163 break; // item did not fit into this row or column 164 } 165 View view = layoutState.next(recycler); 166 if (view == null) { 167 break; 168 } 169 mSet[count] = view; 170 count ++; 171 } 172 173 if (count == 0) { 174 result.mFinished = true; 175 return; 176 } 177 178 int maxSize = 0; 179 final boolean layingOutInPrimaryDirection = mShouldReverseLayout == 180 (layoutState.mLayoutDirection == LayoutState.LAYOUT_START); 181 182 for (int i = 0; i < count; i ++) { 183 View view = mSet[i]; 184 if (layingOutInPrimaryDirection) { 185 addView(view); 186 } else { 187 addView(view, 0); 188 } 189 int spanSize = mSpanSizeLookup.getSpanSize(getPosition(view)); 190 final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize, 191 View.MeasureSpec.EXACTLY); 192 if (mOrientation == VERTICAL) { 193 measureChildWithDecorationsAndMargin(view, spec, OTHER_DIM_SPEC); 194 } else { 195 measureChildWithDecorationsAndMargin(view, OTHER_DIM_SPEC, spec); 196 } 197 final int size = mOrientationHelper.getDecoratedMeasurement(view); 198 if (size > maxSize) { 199 maxSize = size; 200 } 201 } 202 result.mConsumed = maxSize; 203 204 int left = 0, top = 0, right = 0, bottom = 0; 205 if (mOrientation == VERTICAL) { 206 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 207 bottom = layoutState.mOffset; 208 top = bottom - maxSize; 209 } else { 210 top = layoutState.mOffset; 211 bottom = top + maxSize; 212 } 213 } else { 214 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 215 right = layoutState.mOffset; 216 left = right - maxSize; 217 } else { 218 left = layoutState.mOffset; 219 right = left + maxSize; 220 } 221 } 222 int span, spanDiff, start, end, diff; 223 // make sure we traverse from min position to max position 224 if (layingOutInPrimaryDirection) { 225 start = 0; 226 end = count; 227 diff = 1; 228 } else { 229 start = count - 1; 230 end = -1; 231 diff = -1; 232 } 233 if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span 234 span = mSpanCount - 1; 235 spanDiff = -1; 236 } else { 237 span = 0; 238 spanDiff = 1; 239 } 240 for (int i = start; i != end; i += diff) { 241 View view = mSet[i]; 242 LayoutParams params = (LayoutParams) view.getLayoutParams(); 243 int spanSize = mSpanSizeLookup.getSpanSize(getPosition(view)); 244 final int startSpan; 245 if (spanDiff == -1 && spanSize > 1) { 246 startSpan = span - (spanSize - 1); 247 } else { 248 startSpan = span; 249 } 250 if (mOrientation == VERTICAL) { 251 left = getPaddingLeft() + mSizePerSpan * startSpan; 252 right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); 253 } else { 254 top = getPaddingTop() + mSizePerSpan * startSpan; 255 bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); 256 } 257 params.mSpanIndex = startSpan; 258 params.mSpanSize = spanSize; 259 // We calculate everything with View's bounding box (which includes decor and margins) 260 // To calculate correct layout position, we subtract margins. 261 layoutDecorated(view, left + params.leftMargin, top + params.topMargin, 262 right - params.rightMargin, bottom - params.bottomMargin); 263 if (DEBUG) { 264 Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" 265 + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" 266 + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin) 267 + ", span:" + span + ", spanSize:" + spanSize); 268 } 269 // Consume the available space if the view is not removed OR changed 270 if (params.isItemRemoved() || params.isItemChanged()) { 271 result.mIgnoreConsumed = true; 272 } 273 result.mFocusable |= view.isFocusable(); 274 span += spanDiff * spanSize; 275 } 276 Arrays.fill(mSet, null); 277 } 278 279 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) { 280 final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); 281 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); 282 widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + insets.left, 283 lp.rightMargin + insets.right); 284 heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + insets.top, 285 lp.bottomMargin + insets.bottom); 286 child.measure(widthSpec, heightSpec); 287 } 288 289 private int updateSpecWithExtra(int spec, int startInset, int endInset) { 290 if (startInset == 0 && endInset == 0) { 291 return spec; 292 } 293 final int mode = View.MeasureSpec.getMode(spec); 294 if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { 295 return View.MeasureSpec.makeMeasureSpec( 296 View.MeasureSpec.getSize(spec) - startInset - endInset, mode); 297 } 298 return spec; 299 } 300 301 /** 302 * Returns the number of spans laid out by this grid. 303 * 304 * @return The number of spans 305 * @see #setSpanCount(int) 306 */ 307 public int getSpanCount() { 308 return mSpanCount; 309 } 310 311 /** 312 * Sets the number of spans to be laid out. 313 * <p> 314 * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns. 315 * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows. 316 * 317 * @param spanCount The total number of spans in the grid 318 * @see #getSpanCount() 319 */ 320 public void setSpanCount(int spanCount) { 321 if (spanCount == mSpanCount) { 322 return; 323 } 324 if (spanCount < 1) { 325 throw new IllegalArgumentException("Span count should be at least 1. Provided " 326 + spanCount); 327 } 328 mSpanCount = spanCount; 329 } 330 331 /** 332 * A helper class to provide the number of spans each item occupies. 333 * <p> 334 * Default implementation sets each item to occupy exactly 1 span. 335 * 336 * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup) 337 */ 338 public static abstract class SpanSizeLookup { 339 /** 340 * Returns the number of span occupied by the item at <code>position</code>. 341 * 342 * @param position The adapter position of the item 343 * @return The number of spans occupied by the item at the provided position 344 */ 345 abstract public int getSpanSize(int position); 346 347 /** 348 * Returns the final span index of the provided position. 349 * <p> 350 * Default implementation traverses all items before the current position to decide which 351 * span offset this item should be positioned at. You can override this method if you have 352 * a faster way to calculate it based on your data set. 353 * <p> 354 * Note that span offsets always start with 0 and is not affected by RTL. 355 * 356 * @param position The position of the item 357 * @param spanCount The total number of spans in the grid 358 * @return The final span position of the item. Should be between 0 (inclusive) and 359 * <code>spanCount</code>(exclusive) 360 */ 361 public int getSpanIndex(int position, int spanCount) { 362 int span = 0; 363 int positionSpanSize = getSpanSize(position); 364 if (positionSpanSize == spanCount) { 365 return 0; // quick return for full-span items 366 } 367 for (int i = 0; i < position; i++) { 368 int size = getSpanSize(position); 369 span += size; 370 if (span == spanCount) { 371 span = 0; 372 } else if (span > spanCount) { 373 // did not fit, moving to next row / column 374 span = size; 375 } 376 } 377 if (span + positionSpanSize <= spanCount) { 378 return span; 379 } 380 return 0; 381 } 382 } 383 384 @Override 385 public boolean supportsPredictiveItemAnimations() { 386 return false; 387 } 388 389 /** 390 * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span. 391 */ 392 public static final class DefaultSpanSizeLookup extends SpanSizeLookup { 393 @Override 394 public int getSpanSize(int position) { 395 return 1; 396 } 397 398 @Override 399 public int getSpanIndex(int position, int spanCount) { 400 return position % spanCount; 401 } 402 } 403 404 /** 405 * LayoutParams used by GridLayoutManager. 406 */ 407 public static class LayoutParams extends RecyclerView.LayoutParams { 408 409 /** 410 * Span Id for Views that are not laid out yet. 411 */ 412 public static final int INVALID_SPAN_ID = -1; 413 414 private int mSpanIndex = INVALID_SPAN_ID; 415 416 private int mSpanSize = 0; 417 418 public LayoutParams(Context c, AttributeSet attrs) { 419 super(c, attrs); 420 } 421 422 public LayoutParams(int width, int height) { 423 super(width, height); 424 } 425 426 public LayoutParams(ViewGroup.MarginLayoutParams source) { 427 super(source); 428 } 429 430 public LayoutParams(ViewGroup.LayoutParams source) { 431 super(source); 432 } 433 434 public LayoutParams(RecyclerView.LayoutParams source) { 435 super(source); 436 } 437 438 /** 439 * Returns the current span index of this View. If the View is not laid out yet, the return 440 * value is <code>undefined</code>. 441 * <p> 442 * Note that span index may change by whether the RecyclerView is RTL or not. For 443 * example, if the number of spans is 3 and layout is RTL, the rightmost item will have 444 * span index of 2. If the layout changes back to LTR, span index for this view will be 0. 445 * If the item was occupying 2 spans, span indices would be 1 and 0 respectively. 446 * <p> 447 * If the View occupies multiple spans, span with the minimum index is returned. 448 * 449 * @return The span index of the View. 450 */ 451 public int getSpanIndex() { 452 return mSpanIndex; 453 } 454 455 /** 456 * Returns the number of spans occupied by this View. If the View not laid out yet, the 457 * return value is <code>undefined</code>. 458 * 459 * @return The number of spans occupied by this View. 460 */ 461 public int getSpanSize() { 462 return mSpanSize; 463 } 464 } 465 466} 467