StaggeredGridLayoutManager.java revision 9bea36cf2e318e9b729ddc62d855cd0f93bc3866
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.widget; 18 19import android.content.Context; 20import android.graphics.PointF; 21import android.graphics.Rect; 22import android.os.Parcel; 23import android.os.Parcelable; 24import android.support.v4.view.ViewCompat; 25import android.util.AttributeSet; 26import android.util.Log; 27import android.view.View; 28import android.view.ViewGroup; 29 30 31import java.util.ArrayList; 32import java.util.Arrays; 33import java.util.BitSet; 34 35 36import static android.support.v7.widget.LayoutState.LAYOUT_START; 37import static android.support.v7.widget.LayoutState.LAYOUT_END; 38import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD; 39import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL; 40/** 41 * A LayoutManager that lays out children in a staggered grid formation. 42 * It supports horizontal & vertical layout as well as an ability to layout children in reverse. 43 * <p> 44 * Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps, 45 * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can 46 * control this behavior via {@link #setGapStrategy(int)}. 47 */ 48public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { 49 50 public static final String TAG = "StaggeredGridLayoutManager"; 51 52 private static final boolean DEBUG = false; 53 54 /** 55 * Does not do anything to hide gaps 56 */ 57 public static final int GAP_HANDLING_NONE = 0; 58 59 public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; 60 61 public static final int VERTICAL = OrientationHelper.VERTICAL; 62 63 /** 64 * Scroll the shorter span slower to avoid gaps in the UI. 65 * <p> 66 * For example, if LayoutManager ends up with the following layout: 67 * <code> 68 * BXC 69 * DEF 70 * </code> 71 * Where B has two spans height, if user scrolls down it will keep the positions of 2nd and 3rd 72 * columns, 73 * which will result in: 74 * <code> 75 * BXC 76 * BEF 77 * </code> 78 * instead of 79 * <code> 80 * B 81 * BEF 82 * </code> 83 */ 84 public static final int GAP_HANDLING_LAZY = 1; 85 86 /** 87 * On scroll, LayoutManager checks for a view that is assigned to wrong span. 88 * When such a situation is detected, LayoutManager will wait until scroll is complete and then 89 * move children to their correct spans. 90 * <p> 91 * For example, if LayoutManager ends up with the following layout due to adapter changes: 92 * <code> 93 * AAA 94 * _BC 95 * DDD 96 * </code> 97 * It will animate to the following state: 98 * <code> 99 * AAA 100 * BC_ 101 * DDD 102 * </code> 103 */ 104 public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2; 105 106 private static final int INVALID_OFFSET = Integer.MIN_VALUE; 107 108 /** 109 * Number of spans 110 */ 111 private int mSpanCount = -1; 112 113 private Span[] mSpans; 114 115 /** 116 * Primary orientation is the layout's orientation, secondary orientation is the orientation 117 * for spans. Having both makes code much cleaner for calculations. 118 */ 119 OrientationHelper mPrimaryOrientation; 120 OrientationHelper mSecondaryOrientation; 121 122 private int mOrientation; 123 124 /** 125 * The width or height per span, depending on the orientation. 126 */ 127 private int mSizePerSpan; 128 129 private LayoutState mLayoutState; 130 131 private boolean mReverseLayout = false; 132 133 /** 134 * Aggregated reverse layout value that takes RTL into account. 135 */ 136 private boolean mShouldReverseLayout = false; 137 138 /** 139 * Temporary variable used during fill method to check which spans needs to be filled. 140 */ 141 private BitSet mRemainingSpans; 142 143 /** 144 * When LayoutManager needs to scroll to a position, it sets this variable and requests a 145 * layout which will check this variable and re-layout accordingly. 146 */ 147 private int mPendingScrollPosition = RecyclerView.NO_POSITION; 148 149 /** 150 * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is 151 * called. 152 */ 153 private int mPendingScrollPositionOffset = INVALID_OFFSET; 154 155 /** 156 * Keeps the mapping between the adapter positions and spans. This is necessary to provide 157 * a consistent experience when user scrolls the list. 158 */ 159 LazySpanLookup mLazySpanLookup = new LazySpanLookup(); 160 161 /** 162 * how we handle gaps in UI. 163 */ 164 private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 165 166 /** 167 * Saved state needs this information to properly layout on restore. 168 */ 169 private boolean mLastLayoutFromEnd; 170 171 /** 172 * SavedState is not handled until a layout happens. This is where we keep it until next 173 * layout. 174 */ 175 private SavedState mPendingSavedState; 176 177 /** 178 * If LayoutManager detects an unwanted gap in the layout, it sets this flag which will trigger 179 * a runnable after scrolling ends and will re-check. If invalid view state is still present, 180 * it will request a layout to fix it. 181 */ 182 private boolean mHasGaps; 183 184 /** 185 * Creates a StaggeredGridLayoutManager with given parameters. 186 * 187 * @param spanCount If orientation is vertical, spanCount is number of columns. If 188 * orientation is horizontal, spanCount is number of rows. 189 * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL} 190 */ 191 public StaggeredGridLayoutManager(int spanCount, int orientation) { 192 mOrientation = orientation; 193 setSpanCount(spanCount); 194 } 195 196 @Override 197 public void onScrollStateChanged(int state) { 198 if (state == RecyclerView.SCROLL_STATE_IDLE && mHasGaps) { 199 // re-check for gaps 200 View gapView = hasGapsToFix(0, getChildCount()); 201 if (gapView == null) { 202 mHasGaps = false; // yay, gap disappeared :) 203 // We should invalidate positions after the last visible child. No reason to 204 // re-layout. 205 final int lastVisiblePosition = mShouldReverseLayout ? getFirstChildPosition() 206 : getLastChildPosition(); 207 mLazySpanLookup.invalidateAfter(lastVisiblePosition + 1); 208 } else { 209 mLazySpanLookup.invalidateAfter(getPosition(gapView)); 210 requestLayout(); // Trigger a re-layout which will fix the layout assignments. 211 } 212 } 213 } 214 215 /** 216 * Sets the number of spans for the layout. This will invalidate all of the span assignments 217 * for Views. 218 * <p> 219 * Calling this method will automatically result in a new layout request unless the spanCount 220 * parameter is equal to current span count. 221 * 222 * @param spanCount Number of spans to layout 223 */ 224 public void setSpanCount(int spanCount) { 225 if (mPendingSavedState != null && mPendingSavedState.mSpanCount != spanCount) { 226 // invalidate span info in saved state 227 mPendingSavedState.invalidateSpanInfo(); 228 mPendingSavedState.mSpanCount = spanCount; 229 } 230 if (spanCount != mSpanCount) { 231 invalidateSpanAssignments(); 232 mSpanCount = spanCount; 233 mRemainingSpans = new BitSet(mSpanCount); 234 mSpans = new Span[mSpanCount]; 235 for (int i = 0; i < mSpanCount; i++) { 236 mSpans[i] = new Span(i); 237 } 238 requestLayout(); 239 } 240 } 241 242 /** 243 * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep 244 * scroll position. 245 * 246 * @param orientation {@link OrientationHelper#HORIZONTAL} or {@link OrientationHelper#VERTICAL} 247 */ 248 public void setOrientation(int orientation) { 249 if (orientation != HORIZONTAL && orientation != VERTICAL) { 250 throw new IllegalArgumentException("invalid orientation."); 251 } 252 if (mPendingSavedState != null && mPendingSavedState.mOrientation != orientation) { 253 // override pending state 254 mPendingSavedState.mOrientation = orientation; 255 } 256 if (orientation == mOrientation) { 257 return; 258 } 259 mOrientation = orientation; 260 if (mPrimaryOrientation != null && mSecondaryOrientation != null) { 261 // swap 262 OrientationHelper tmp = mPrimaryOrientation; 263 mPrimaryOrientation = mSecondaryOrientation; 264 mSecondaryOrientation = tmp; 265 } 266 requestLayout(); 267 } 268 269 /** 270 * Sets whether LayoutManager should start laying out items from the end of the UI. The order 271 * items are traversed is not affected by this call. 272 * <p> 273 * This behaves similar to the layout change for RTL views. When set to true, first item is 274 * laid out at the end of the ViewGroup, second item is laid out before it etc. 275 * <p> 276 * For horizontal layouts, it depends on the layout direction. 277 * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if 278 * {@link RecyclerView}} is RTL, it will layout from LTR. 279 * 280 * @param reverseLayout Whether layout should be in reverse or not 281 */ 282 public void setReverseLayout(boolean reverseLayout) { 283 if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) { 284 mPendingSavedState.mReverseLayout = reverseLayout; 285 } 286 mReverseLayout = reverseLayout; 287 requestLayout(); 288 } 289 290 /** 291 * Returns the current gap handling strategy for StaggeredGridLayoutManager. 292 * <p> 293 * Staggered grid may have gaps in the layout as items may have different sizes. To avoid gaps, 294 * StaggeredGridLayoutManager provides 3 options. Check {@link #GAP_HANDLING_NONE}, 295 * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}, {@link #GAP_HANDLING_LAZY} for details. 296 * <p> 297 * By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}. 298 * 299 * @return Current gap handling strategy. 300 * @see #setGapStrategy(int) 301 * @see #GAP_HANDLING_NONE 302 * @see #GAP_HANDLING_LAZY 303 * @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS 304 */ 305 public int getGapStrategy() { 306 return mGapStrategy; 307 } 308 309 /** 310 * Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter 311 * is different than the current strategy, calling this method will trigger a layout request. 312 * 313 * @param gapStrategy The new gap handling strategy. Should be {@link #GAP_HANDLING_LAZY} 314 * , {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or 315 * {@link #GAP_HANDLING_NONE} 316 * @see #getGapStrategy() 317 */ 318 public void setGapStrategy(int gapStrategy) { 319 if (mPendingSavedState != null && mPendingSavedState.mGapStrategy != gapStrategy) { 320 mPendingSavedState.mGapStrategy = gapStrategy; 321 } 322 if (gapStrategy == mGapStrategy) { 323 return; 324 } 325 if (gapStrategy != GAP_HANDLING_LAZY && gapStrategy != GAP_HANDLING_NONE && 326 gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) { 327 throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE " 328 + ", GAP_HANDLING_LAZY or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS"); 329 } 330 mGapStrategy = gapStrategy; 331 requestLayout(); 332 } 333 334 /** 335 * Returns the number of spans laid out by StaggeredGridLayoutManager. 336 * 337 * @return Number of spans in the layout 338 */ 339 public int getSpanCount() { 340 return mSpanCount; 341 } 342 343 /** 344 * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items. 345 * <p> 346 * If you need to cancel current assignments, you can call this method which will clear all 347 * assignments and request a new layout. 348 */ 349 public void invalidateSpanAssignments() { 350 mLazySpanLookup.clear(); 351 requestLayout(); 352 } 353 354 private void ensureOrientationHelper() { 355 if (mPrimaryOrientation == null) { 356 mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation); 357 mSecondaryOrientation = OrientationHelper 358 .createOrientationHelper(this, 1 - mOrientation); 359 mLayoutState = new LayoutState(); 360 } 361 } 362 363 /** 364 * Calculates the views' layout order. (e.g. from end to start or start to end) 365 * RTL layout support is applied automatically. So if layout is RTL and 366 * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. 367 */ 368 private void resolveShouldLayoutReverse() { 369 // A == B is the same result, but we rather keep it readable 370 if (mOrientation == VERTICAL || !isLayoutRTL()) { 371 mShouldReverseLayout = mReverseLayout; 372 } else { 373 mShouldReverseLayout = !mReverseLayout; 374 } 375 } 376 377 private boolean isLayoutRTL() { 378 return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; 379 } 380 381 /** 382 * Returns whether views are laid out in reverse order or not. 383 * <p> 384 * Not that this value is not affected by RecyclerView's layout direction. 385 * 386 * @return True if layout is reversed, false otherwise 387 * @see #setReverseLayout(boolean) 388 */ 389 public boolean getReverseLayout() { 390 return mReverseLayout; 391 } 392 393 @Override 394 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 395 ensureOrientationHelper(); 396 // Update adapter size. 397 mLazySpanLookup.mAdapterSize = state.getItemCount(); 398 int anchorItemPosition; 399 int anchorOffset; 400 // This value may change if we are jumping to a position. 401 boolean layoutFromEnd; 402 403 // If set to true, spans will clear their offsets and they'll be laid out from start 404 // depending on the layout direction. Invalidating span offsets is necessary to be able 405 // to jump to a position. 406 boolean invalidateSpanOffsets = false; 407 408 if (mPendingSavedState != null) { 409 if (DEBUG) { 410 Log.d(TAG, "found saved state: " + mPendingSavedState); 411 } 412 setOrientation(mPendingSavedState.mOrientation); 413 setSpanCount(mPendingSavedState.mSpanCount); 414 setGapStrategy(mPendingSavedState.mGapStrategy); 415 setReverseLayout(mPendingSavedState.mReverseLayout); 416 resolveShouldLayoutReverse(); 417 418 if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) { 419 mPendingScrollPosition = mPendingSavedState.mAnchorPosition; 420 layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; 421 } else { 422 layoutFromEnd = mShouldReverseLayout; 423 } 424 if (mPendingSavedState.mHasSpanOffsets) { 425 for (int i = 0; i < mSpanCount; i++) { 426 mSpans[i].clear(); 427 mSpans[i].setLine(mPendingSavedState.mSpanOffsets[i]); 428 } 429 } 430 if (mPendingSavedState.mSpanLookupSize > 1) { 431 mLazySpanLookup.mData = mPendingSavedState.mSpanLookup; 432 } 433 434 } else { 435 resolveShouldLayoutReverse(); 436 layoutFromEnd = mShouldReverseLayout; // get updated value. 437 } 438 439 // Validate scroll position if exists. 440 if (mPendingScrollPosition != RecyclerView.NO_POSITION) { 441 // Validate it. 442 if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { 443 mPendingScrollPosition = RecyclerView.NO_POSITION; 444 mPendingScrollPositionOffset = INVALID_OFFSET; 445 } 446 } 447 448 if (mPendingScrollPosition != RecyclerView.NO_POSITION) { 449 if (mPendingSavedState == null 450 || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION 451 || !mPendingSavedState.mHasSpanOffsets) { 452 // If item is visible, make it fully visible. 453 final View child = findViewByPosition(mPendingScrollPosition); 454 if (child != null) { 455 if (mPendingScrollPositionOffset != INVALID_OFFSET) { 456 // Use regular anchor position. 457 anchorItemPosition = mShouldReverseLayout ? getLastChildPosition() 458 : getFirstChildPosition(); 459 if (layoutFromEnd) { 460 final int target = mPrimaryOrientation.getEndAfterPadding() - 461 mPendingScrollPositionOffset; 462 anchorOffset = target - mPrimaryOrientation.getDecoratedEnd(child); 463 } else { 464 final int target = mPrimaryOrientation.getStartAfterPadding() + 465 mPendingScrollPositionOffset; 466 anchorOffset = target - mPrimaryOrientation.getDecoratedStart(child); 467 } 468 } else { 469 final int startGap = mPrimaryOrientation.getDecoratedStart(child) 470 - mPrimaryOrientation.getStartAfterPadding(); 471 final int endGap = mPrimaryOrientation.getEndAfterPadding() - 472 mPrimaryOrientation.getDecoratedEnd(child); 473 final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child); 474 // Use regular anchor item, just offset the layout. 475 anchorItemPosition = mShouldReverseLayout ? getLastChildPosition() 476 : getFirstChildPosition(); 477 if (childSize > mPrimaryOrientation.getTotalSpace()) { 478 // Item does not fit. Fix depending on layout direction. 479 anchorOffset = layoutFromEnd ? mPrimaryOrientation.getEndAfterPadding() 480 : mPrimaryOrientation.getStartAfterPadding(); 481 } else if (startGap < 0) { 482 anchorOffset = -startGap; 483 } else if (endGap < 0) { 484 anchorOffset = endGap; 485 } else { 486 // Nothing to do, just layout normal. 487 anchorItemPosition = mShouldReverseLayout ? getLastChildPosition() 488 : getFirstChildPosition(); 489 anchorOffset = INVALID_OFFSET; 490 } 491 } 492 } else { 493 // Child is not visible. Set anchor coordinate depending on in which direction 494 // child will be visible. 495 anchorItemPosition = mPendingScrollPosition; 496 if (mPendingScrollPositionOffset == INVALID_OFFSET) { 497 final int position = calculateScrollDirectionForPosition( 498 anchorItemPosition); 499 if (position == LAYOUT_START) { 500 anchorOffset = mPrimaryOrientation.getStartAfterPadding(); 501 layoutFromEnd = false; 502 } else { 503 anchorOffset = mPrimaryOrientation.getEndAfterPadding(); 504 layoutFromEnd = true; 505 } 506 } else { 507 if (layoutFromEnd) { 508 anchorOffset = mPrimaryOrientation.getEndAfterPadding() 509 - mPendingScrollPositionOffset; 510 } else { 511 anchorOffset = mPrimaryOrientation.getStartAfterPadding() 512 + mPendingScrollPositionOffset; 513 } 514 } 515 invalidateSpanOffsets = true; 516 } 517 } else { 518 anchorOffset = INVALID_OFFSET; 519 anchorItemPosition = mPendingScrollPosition; 520 } 521 522 } else { 523 // We don't recycle views out of adapter order. This way, we can rely on the first or 524 // last child as the anchor position. 525 anchorItemPosition = mShouldReverseLayout ? getLastChildPosition() 526 : getFirstChildPosition(); 527 anchorOffset = INVALID_OFFSET; 528 } 529 if (getChildCount() > 0 && (mPendingSavedState == null 530 || !mPendingSavedState.mHasSpanOffsets)) { 531 if (invalidateSpanOffsets) { 532 for (int i = 0; i < mSpanCount; i++) { 533 // Scroll to position is set, clear. 534 mSpans[i].clear(); 535 if (anchorOffset != INVALID_OFFSET) { 536 mSpans[i].setLine(anchorOffset); 537 } 538 } 539 } else { 540 for (int i = 0; i < mSpanCount; i++) { 541 mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout, anchorOffset); 542 } 543 if (DEBUG) { 544 for (int i = 0; i < mSpanCount; i++) { 545 Log.d(TAG, "cached start-end lines for " + i + ":" + 546 mSpans[i].mCachedStart + ":" + mSpans[i].mCachedEnd); 547 } 548 } 549 } 550 } 551 mSizePerSpan = mSecondaryOrientation.getTotalSpace() / mSpanCount; 552 detachAndScrapAttachedViews(recycler); 553 // Layout start. 554 updateLayoutStateToFillStart(anchorItemPosition, state); 555 if (!layoutFromEnd) { 556 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 557 } 558 fill(recycler, mLayoutState, state); 559 560 // Layout end. 561 updateLayoutStateToFillEnd(anchorItemPosition, state); 562 if (layoutFromEnd) { 563 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 564 } 565 fill(recycler, mLayoutState, state); 566 567 if (getChildCount() > 0) { 568 if (mShouldReverseLayout) { 569 fixEndGap(recycler, state, true); 570 fixStartGap(recycler, state, false); 571 } else { 572 fixStartGap(recycler, state, true); 573 fixEndGap(recycler, state, false); 574 } 575 } 576 577 mPendingScrollPosition = RecyclerView.NO_POSITION; 578 mPendingScrollPositionOffset = INVALID_OFFSET; 579 mLastLayoutFromEnd = layoutFromEnd; 580 mPendingSavedState = null; // we don't need this anymore 581 } 582 583 /** 584 * Checks if a child is assigned to the non-optimal span. 585 * 586 * @param startChildIndex Starts checking after this child, inclusive 587 * @param endChildIndex Starts checking until this child, exclusive 588 * @return The first View that is assigned to the wrong span. 589 */ 590 View hasGapsToFix(int startChildIndex, int endChildIndex) { 591 // quick reject 592 if (startChildIndex >= endChildIndex) { 593 return null; 594 } 595 final int firstChildIndex, childLimit; 596 final int nextSpanDiff = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1; 597 598 if (mShouldReverseLayout) { 599 firstChildIndex = endChildIndex - 1; 600 childLimit = startChildIndex - 1; 601 } else { 602 firstChildIndex = startChildIndex; 603 childLimit = endChildIndex; 604 } 605 final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1; 606 for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) { 607 View child = getChildAt(i); 608 final int start = mPrimaryOrientation.getDecoratedStart(child); 609 final int end = mPrimaryOrientation.getDecoratedEnd(child); 610 LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); 611 if (layoutParams.mFullSpan) { 612 continue; // quick reject 613 } 614 int nextSpanIndex = layoutParams.getSpanIndex() + nextSpanDiff; 615 while (nextSpanIndex >= 0 && nextSpanIndex < mSpanCount) { 616 Span nextSpan = mSpans[nextSpanIndex]; 617 if (nextSpan.isEmpty(start, end)) { 618 return child; 619 } 620 nextSpanIndex += nextSpanDiff; 621 } 622 } 623 // everything looks good 624 return null; 625 } 626 627 @Override 628 public boolean supportsPredictiveItemAnimations() { 629 return true; 630 } 631 632 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, 633 int heightSpec) { 634 final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); 635 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 636 widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + insets.left, 637 lp.rightMargin + insets.right); 638 heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + insets.top, 639 lp.bottomMargin + insets.bottom); 640 child.measure(widthSpec, heightSpec); 641 } 642 643 private int updateSpecWithExtra(int spec, int startInset, int endInset) { 644 if (startInset == 0 && endInset == 0) { 645 return spec; 646 } 647 final int mode = View.MeasureSpec.getMode(spec); 648 if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { 649 return View.MeasureSpec.makeMeasureSpec( 650 View.MeasureSpec.getSize(spec) - startInset - endInset, mode); 651 } 652 return spec; 653 } 654 655 @Override 656 public void onRestoreInstanceState(Parcelable state) { 657 if (state instanceof SavedState) { 658 mPendingSavedState = (SavedState) state; 659 requestLayout(); 660 } else if (DEBUG) { 661 Log.d(TAG, "invalid saved state class"); 662 } 663 } 664 665 @Override 666 public Parcelable onSaveInstanceState() { 667 if (mPendingSavedState != null) { 668 return new SavedState(mPendingSavedState); 669 } 670 SavedState state = new SavedState(); 671 state.mOrientation = mOrientation; 672 state.mReverseLayout = mReverseLayout; 673 state.mSpanCount = mSpanCount; 674 state.mAnchorLayoutFromEnd = mLastLayoutFromEnd; 675 state.mGapStrategy = mGapStrategy; 676 677 if (mLazySpanLookup != null && mLazySpanLookup.mData != null) { 678 state.mSpanLookup = mLazySpanLookup.mData; 679 state.mSpanLookupSize = state.mSpanLookup.length; 680 } else { 681 state.mSpanLookupSize = 0; 682 } 683 684 if (getChildCount() > 0) { 685 state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition() 686 : getFirstChildPosition(); 687 state.mHasSpanOffsets = true; 688 state.mSpanOffsets = new int[mSpanCount]; 689 for (int i = 0; i < mSpanCount; i++) { 690 state.mSpanOffsets[i] = mLastLayoutFromEnd ? mSpans[i].getEndLine() 691 : mSpans[i].getStartLine(); 692 } 693 } else { 694 state.mAnchorPosition = RecyclerView.NO_POSITION; 695 state.mHasSpanOffsets = false; 696 } 697 if (DEBUG) { 698 Log.d(TAG, "saved state:\n" + state); 699 } 700 return state; 701 } 702 703 private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state, 704 boolean canOffsetChildren) { 705 final int maxEndLine = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); 706 int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine; 707 int fixOffset; 708 if (gap > 0) { 709 fixOffset = -scrollBy(-gap, recycler, state); 710 } else { 711 return; // nothing to fix 712 } 713 gap -= fixOffset; 714 if (canOffsetChildren && gap > 0) { 715 mPrimaryOrientation.offsetChildren(gap); 716 } 717 } 718 719 private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state, 720 boolean canOffsetChildren) { 721 final int minStartLine = getMinStart(mPrimaryOrientation.getStartAfterPadding()); 722 int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding(); 723 int fixOffset; 724 if (gap > 0) { 725 fixOffset = scrollBy(gap, recycler, state); 726 } else { 727 return; // nothing to fix 728 } 729 gap -= fixOffset; 730 if (canOffsetChildren && gap > 0) { 731 mPrimaryOrientation.offsetChildren(-gap); 732 } 733 } 734 735 private void updateLayoutStateToFillStart(int anchorPosition, RecyclerView.State state) { 736 mLayoutState.mAvailable = 0; 737 mLayoutState.mCurrentPosition = anchorPosition; 738 if (isSmoothScrolling()) { 739 final int targetPos = state.getTargetScrollPosition(); 740 if (mShouldReverseLayout == targetPos < anchorPosition) { 741 mLayoutState.mExtra = 0; 742 } else { 743 mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); 744 } 745 } else { 746 mLayoutState.mExtra = 0; 747 } 748 mLayoutState.mLayoutDirection = LAYOUT_START; 749 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL 750 : ITEM_DIRECTION_HEAD; 751 } 752 753 private void updateLayoutStateToFillEnd(int anchorPosition, RecyclerView.State state) { 754 mLayoutState.mAvailable = 0; 755 mLayoutState.mCurrentPosition = anchorPosition; 756 if (isSmoothScrolling()) { 757 final int targetPos = state.getTargetScrollPosition(); 758 if (mShouldReverseLayout == targetPos > anchorPosition) { 759 mLayoutState.mExtra = 0; 760 } else { 761 mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); 762 } 763 } else { 764 mLayoutState.mExtra = 0; 765 } 766 mLayoutState.mLayoutDirection = LAYOUT_END; 767 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD 768 : ITEM_DIRECTION_TAIL; 769 } 770 771 @Override 772 public void offsetChildrenHorizontal(int dx) { 773 super.offsetChildrenHorizontal(dx); 774 for (int i = 0; i < mSpanCount; i++) { 775 mSpans[i].onOffset(dx); 776 } 777 } 778 779 @Override 780 public void offsetChildrenVertical(int dy) { 781 super.offsetChildrenVertical(dy); 782 for (int i = 0; i < mSpanCount; i++) { 783 mSpans[i].onOffset(dy); 784 } 785 } 786 787 @Override 788 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 789 if (!considerSpanInvalidate(positionStart, itemCount)) { 790 // If positions are not invalidated, move span offsets. 791 mLazySpanLookup.offsetForRemoval(positionStart, itemCount); 792 } 793 } 794 795 @Override 796 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 797 if (!considerSpanInvalidate(positionStart, itemCount)) { 798 // If positions are not invalidated, move span offsets. 799 mLazySpanLookup.offsetForAddition(positionStart, itemCount); 800 } 801 } 802 803 /** 804 * Checks whether it should invalidate span assignments in response to an adapter change. 805 */ 806 private boolean considerSpanInvalidate(int positionStart, int itemCount) { 807 int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition(); 808 if (positionStart + itemCount <= minPosition) { 809 return false;// nothing to update. 810 } 811 int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition(); 812 mLazySpanLookup.invalidateAfter(positionStart); 813 if (positionStart <= maxPosition) { 814 requestLayout(); 815 } 816 return true; 817 } 818 819 private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, 820 RecyclerView.State state) { 821 mRemainingSpans.set(0, mSpanCount, true); 822 // The target position we are trying to reach. 823 final int targetLine; 824 825 /* 826 * The line until which we can recycle, as long as we add views. 827 * Keep in mind, it is still the line in layout direction which means; to calculate the 828 * actual recycle line, we should subtract/add the size in orientation. 829 */ 830 final int recycleLine; 831 // Line of the furthest row. 832 if (layoutState.mLayoutDirection == LAYOUT_END) { 833 recycleLine = mPrimaryOrientation.getEndAfterPadding() + mLayoutState.mAvailable; 834 targetLine = recycleLine + mLayoutState.mExtra; 835 final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); 836 for (int i = 0; i < mSpanCount; i++) { 837 final Span span = mSpans[i]; 838 final int line = span.getEndLine(defaultLine); 839 if (line > targetLine) { 840 mRemainingSpans.set(i, false); 841 } 842 } 843 } else { // LAYOUT_START 844 recycleLine = mPrimaryOrientation.getStartAfterPadding() - mLayoutState.mAvailable; 845 targetLine = recycleLine - mLayoutState.mExtra; 846 for (int i = 0; i < mSpanCount; i++) { 847 final Span span = mSpans[i]; 848 final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); 849 final int line = span.getStartLine(defaultLine); 850 if (line < targetLine) { 851 mRemainingSpans.set(i, false); 852 } 853 } 854 } 855 856 final int widthSpec, heightSpec; 857 if (mOrientation == VERTICAL) { 858 widthSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); 859 heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 860 } else { 861 heightSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); 862 widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 863 } 864 865 while (layoutState.hasMore(state) && !mRemainingSpans.isEmpty()) { 866 View view = layoutState.next(recycler); 867 LayoutParams lp = ((LayoutParams) view.getLayoutParams()); 868 if (layoutState.mLayoutDirection == LAYOUT_END) { 869 addView(view); 870 } else { 871 addView(view, 0); 872 } 873 if (lp.mFullSpan) { 874 final int fullSizeSpec = View.MeasureSpec.makeMeasureSpec( 875 mSecondaryOrientation.getTotalSpace(), View.MeasureSpec.EXACTLY); 876 if (mOrientation == VERTICAL) { 877 measureChildWithDecorationsAndMargin(view, fullSizeSpec, heightSpec); 878 } else { 879 measureChildWithDecorationsAndMargin(view, widthSpec, fullSizeSpec); 880 } 881 } else { 882 measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec); 883 } 884 885 final int position = getPosition(view); 886 final int spanIndex = mLazySpanLookup.getSpan(position); 887 Span currentSpan; 888 if (spanIndex == LayoutParams.INVALID_SPAN_ID) { 889 if (lp.mFullSpan) { 890 // assign full span items to first span 891 currentSpan = mSpans[0]; 892 } else { 893 currentSpan = getNextSpan(layoutState); 894 } 895 mLazySpanLookup.setSpan(position, currentSpan); 896 } else { 897 currentSpan = mSpans[spanIndex]; 898 } 899 final int start; 900 final int end; 901 if (layoutState.mLayoutDirection == LAYOUT_END) { 902 final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding() 903 : mPrimaryOrientation.getStartAfterPadding(); 904 start = lp.mFullSpan ? getMaxEnd(def) : currentSpan.getEndLine(def); 905 end = start + mPrimaryOrientation.getDecoratedMeasurement(view); 906 if (lp.mFullSpan) { 907 for (int i = 0; i < mSpanCount; i++) { 908 mSpans[i].appendToSpan(view); 909 } 910 } else { 911 currentSpan.appendToSpan(view); 912 } 913 } else { 914 final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding() 915 : mPrimaryOrientation.getStartAfterPadding(); 916 end = lp.mFullSpan ? getMinStart(def) : currentSpan.getStartLine(def); 917 start = end - mPrimaryOrientation.getDecoratedMeasurement(view); 918 if (lp.mFullSpan) { 919 for (int i = 0; i < mSpanCount; i++) { 920 mSpans[i].prependToSpan(view); 921 } 922 } else { 923 currentSpan.prependToSpan(view); 924 } 925 926 } 927 lp.mSpan = currentSpan; 928 929 if (DEBUG) { 930 Log.d(TAG, "adding view item " + lp.getViewPosition() + " between " + start + "," 931 + end); 932 } 933 934 final int otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding() 935 : currentSpan.mIndex * mSizePerSpan + mSecondaryOrientation 936 .getStartAfterPadding(); 937 final int otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view); 938 if (mOrientation == VERTICAL) { 939 layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end); 940 } else { 941 layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd); 942 } 943 if (lp.mFullSpan) { 944 for (int i = 0; i < mSpanCount; i++) { 945 updateRemainingSpans(mSpans[i], mLayoutState.mLayoutDirection, targetLine); 946 } 947 } else { 948 updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine); 949 } 950 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 951 // calculate recycle line 952 int maxStart = getMaxStart(currentSpan.getStartLine()); 953 recycleFromEnd(recycler, Math.max(recycleLine, maxStart) 954 + mPrimaryOrientation.getTotalSpace()); 955 } else { 956 // calculate recycle line 957 int minEnd = getMinEnd(currentSpan.getEndLine()); 958 recycleFromStart(recycler, Math.min(recycleLine, minEnd) 959 - mPrimaryOrientation.getTotalSpace()); 960 } 961 } 962 if (DEBUG) { 963 Log.d(TAG, "fill, " + getChildCount()); 964 } 965 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 966 final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding()); 967 return Math.max(0, mLayoutState.mAvailable + (targetLine - minStart)); 968 } else { 969 final int max = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); 970 return Math.max(0, mLayoutState.mAvailable + (max - targetLine)); 971 } 972 } 973 974 private void layoutDecoratedWithMargins(View child, int left, int top, int right, 975 int bottom) { 976 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 977 layoutDecorated(child, left + lp.leftMargin, top + lp.topMargin, right - lp.rightMargin 978 , bottom - lp.bottomMargin); 979 } 980 981 private void updateRemainingSpans(Span span, int layoutDir, int targetLine) { 982 final int deletedSize = span.getDeletedSize(); 983 if (layoutDir == LAYOUT_START) { 984 final int line = span.getStartLine(); 985 if (line + deletedSize < targetLine) { 986 mRemainingSpans.set(span.mIndex, false); 987 } 988 } else { 989 final int line = span.getEndLine(); 990 if (line - deletedSize > targetLine) { 991 mRemainingSpans.set(span.mIndex, false); 992 } 993 } 994 } 995 996 private int getMaxStart(int def) { 997 int maxStart = mSpans[0].getStartLine(def); 998 for (int i = 1; i < mSpanCount; i++) { 999 final int spanStart = mSpans[i].getStartLine(def); 1000 if (spanStart > maxStart) { 1001 maxStart = spanStart; 1002 } 1003 } 1004 return maxStart; 1005 } 1006 1007 private int getMinStart(int def) { 1008 int minStart = mSpans[0].getStartLine(def); 1009 for (int i = 1; i < mSpanCount; i++) { 1010 final int spanStart = mSpans[i].getStartLine(def); 1011 if (spanStart < minStart) { 1012 minStart = spanStart; 1013 } 1014 } 1015 return minStart; 1016 } 1017 1018 private int getMaxEnd(int def) { 1019 int maxEnd = mSpans[0].getEndLine(def); 1020 for (int i = 1; i < mSpanCount; i++) { 1021 final int spanEnd = mSpans[i].getEndLine(def); 1022 if (spanEnd > maxEnd) { 1023 maxEnd = spanEnd; 1024 } 1025 } 1026 return maxEnd; 1027 } 1028 1029 private int getMinEnd(int def) { 1030 int minEnd = mSpans[0].getEndLine(def); 1031 for (int i = 1; i < mSpanCount; i++) { 1032 final int spanEnd = mSpans[i].getEndLine(def); 1033 if (spanEnd < minEnd) { 1034 minEnd = spanEnd; 1035 } 1036 } 1037 return minEnd; 1038 } 1039 1040 private void recycleFromStart(RecyclerView.Recycler recycler, int line) { 1041 if (DEBUG) { 1042 Log.d(TAG, "recycling from start for line " + line); 1043 } 1044 while (getChildCount() > 0) { 1045 View child = getChildAt(0); 1046 if (mPrimaryOrientation.getDecoratedEnd(child) < line) { 1047 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1048 if (lp.mFullSpan) { 1049 for (int j = 0; j < mSpanCount; j++) { 1050 mSpans[j].popStart(); 1051 } 1052 } else { 1053 lp.mSpan.popStart(); 1054 } 1055 removeAndRecycleView(child, recycler); 1056 } else { 1057 return;// done 1058 } 1059 } 1060 } 1061 1062 private void recycleFromEnd(RecyclerView.Recycler recycler, int line) { 1063 final int childCount = getChildCount(); 1064 int i; 1065 for (i = childCount - 1; i >= 0; i--) { 1066 View child = getChildAt(i); 1067 if (mPrimaryOrientation.getDecoratedStart(child) > line) { 1068 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1069 if (lp.mFullSpan) { 1070 for (int j = 0; j < mSpanCount; j++) { 1071 mSpans[j].popEnd(); 1072 } 1073 } else { 1074 lp.mSpan.popEnd(); 1075 } 1076 removeAndRecycleView(child, recycler); 1077 } else { 1078 return;// done 1079 } 1080 } 1081 } 1082 1083 /** 1084 * Finds the span for the next view. 1085 */ 1086 private Span getNextSpan(LayoutState layoutState) { 1087 final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); 1088 if (layoutState.mLayoutDirection == LAYOUT_END) { 1089 Span min = mSpans[0]; 1090 int minLine = min.getEndLine(mPrimaryOrientation.getStartAfterPadding()); 1091 final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); 1092 for (int i = 1; i < mSpanCount; i++) { 1093 final Span other = mSpans[i]; 1094 final int otherLine = other.getEndLine(defaultLine); 1095 if (otherLine < minLine || (otherLine == minLine && preferLastSpan)) { 1096 min = other; 1097 minLine = otherLine; 1098 } 1099 } 1100 return min; 1101 } else { 1102 Span max = mSpans[0]; 1103 int maxLine = max.getStartLine(mPrimaryOrientation.getEndAfterPadding()); 1104 final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); 1105 for (int i = 1; i < mSpanCount; i++) { 1106 final Span other = mSpans[i]; 1107 final int otherLine = other.getStartLine(defaultLine); 1108 if (otherLine > maxLine || (otherLine == maxLine && !preferLastSpan)) { 1109 max = other; 1110 maxLine = otherLine; 1111 } 1112 } 1113 return max; 1114 } 1115 } 1116 1117 @Override 1118 public boolean canScrollVertically() { 1119 return mOrientation == VERTICAL; 1120 } 1121 1122 @Override 1123 public boolean canScrollHorizontally() { 1124 return mOrientation == HORIZONTAL; 1125 } 1126 1127 @Override 1128 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 1129 RecyclerView.State state) { 1130 return scrollBy(dx, recycler, state); 1131 } 1132 1133 @Override 1134 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 1135 RecyclerView.State state) { 1136 return scrollBy(dy, recycler, state); 1137 } 1138 1139 private int calculateScrollDirectionForPosition(int position) { 1140 if (getChildCount() == 0) { 1141 return mShouldReverseLayout ? LAYOUT_END : LAYOUT_START; 1142 } 1143 final int firstChildPos = getFirstChildPosition(); 1144 return position < firstChildPos != mShouldReverseLayout ? LAYOUT_START : LAYOUT_END; 1145 } 1146 1147 @Override 1148 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 1149 int position) { 1150 LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { 1151 @Override 1152 public PointF computeScrollVectorForPosition(int targetPosition) { 1153 final int direction = calculateScrollDirectionForPosition(targetPosition); 1154 if (direction == 0) { 1155 return null; 1156 } 1157 if (mOrientation == HORIZONTAL) { 1158 return new PointF(direction, 0); 1159 } else { 1160 return new PointF(0, direction); 1161 } 1162 } 1163 }; 1164 scroller.setTargetPosition(position); 1165 startSmoothScroll(scroller); 1166 } 1167 1168 @Override 1169 public void scrollToPosition(int position) { 1170 if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) { 1171 mPendingSavedState.invalidateAnchorPositionInfo(); 1172 } 1173 mPendingScrollPosition = position; 1174 mPendingScrollPositionOffset = INVALID_OFFSET; 1175 requestLayout(); 1176 } 1177 1178 /** 1179 * Scroll to the specified adapter position with the given offset from layout start. 1180 * <p> 1181 * Note that scroll position change will not be reflected until the next layout call. 1182 * <p> 1183 * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. 1184 * 1185 * @param position Index (starting at 0) of the reference item. 1186 * @param offset The distance (in pixels) between the start edge of the item view and 1187 * start edge of the RecyclerView. 1188 * @see #setReverseLayout(boolean) 1189 * @see #scrollToPosition(int) 1190 */ 1191 public void scrollToPositionWithOffset(int position, int offset) { 1192 if (mPendingSavedState != null) { 1193 mPendingSavedState.invalidateAnchorPositionInfo(); 1194 } 1195 mPendingScrollPosition = position; 1196 mPendingScrollPositionOffset = offset; 1197 requestLayout(); 1198 } 1199 1200 private int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { 1201 ensureOrientationHelper(); 1202 final int referenceChildPosition; 1203 if (dt > 0) { // layout towards end 1204 mLayoutState.mLayoutDirection = LAYOUT_END; 1205 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD 1206 : ITEM_DIRECTION_TAIL; 1207 referenceChildPosition = getLastChildPosition(); 1208 } else { 1209 mLayoutState.mLayoutDirection = LAYOUT_START; 1210 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL 1211 : ITEM_DIRECTION_HEAD; 1212 referenceChildPosition = getFirstChildPosition(); 1213 } 1214 mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; 1215 final int absDt = Math.abs(dt); 1216 mLayoutState.mAvailable = absDt; 1217 mLayoutState.mExtra = isSmoothScrolling() ? mPrimaryOrientation.getTotalSpace() : 0; 1218 int consumed = fill(recycler, mLayoutState, state); 1219 final int totalScroll; 1220 if (absDt < consumed) { 1221 totalScroll = dt; 1222 } else if (dt < 0) { 1223 totalScroll = -consumed; 1224 } else { // dt > 0 1225 totalScroll = consumed; 1226 } 1227 if (DEBUG) { 1228 Log.d(TAG, "asked " + dt + " scrolled" + totalScroll); 1229 } 1230 1231 if (mGapStrategy == GAP_HANDLING_LAZY 1232 && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD) { 1233 final int targetStart = mPrimaryOrientation.getStartAfterPadding(); 1234 final int targetEnd = mPrimaryOrientation.getEndAfterPadding(); 1235 lazyOffsetSpans(-totalScroll, targetStart, targetEnd); 1236 } else { 1237 mPrimaryOrientation.offsetChildren(-totalScroll); 1238 } 1239 // always reset this if we scroll for a proper save instance state 1240 mLastLayoutFromEnd = mShouldReverseLayout; 1241 1242 if (totalScroll != 0 && mGapStrategy != GAP_HANDLING_NONE 1243 && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD && !mHasGaps) { 1244 final int addedChildCount = Math.abs(mLayoutState.mCurrentPosition 1245 - (referenceChildPosition + mLayoutState.mItemDirection)); 1246 if (addedChildCount > 0) { 1247 // check if any child has been attached to wrong span. If so, trigger a re-layout 1248 // after scroll 1249 final View viewInWrongSpan; 1250 final View referenceView = findViewByPosition(referenceChildPosition); 1251 if (referenceView == null) { 1252 viewInWrongSpan = hasGapsToFix(0, getChildCount()); 1253 } else { 1254 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 1255 viewInWrongSpan = hasGapsToFix(0, addedChildCount); 1256 } else { 1257 viewInWrongSpan = hasGapsToFix(getChildCount() - addedChildCount, 1258 getChildCount()); 1259 } 1260 } 1261 mHasGaps = viewInWrongSpan != null; 1262 } 1263 } 1264 return totalScroll; 1265 } 1266 1267 /** 1268 * The actual method that implements {@link #GAP_HANDLING_LAZY} 1269 */ 1270 private void lazyOffsetSpans(int offset, int targetStart, int targetEnd) { 1271 // For each span offset children one by one. 1272 // When a fullSpan item is reached, stop and wait for other spans to reach to that span. 1273 // When all reach, offset fullSpan to max of others and continue. 1274 int childrenToOffset = getChildCount(); 1275 int[] indexPerSpan = new int[mSpanCount]; 1276 int[] offsetPerSpan = new int[mSpanCount]; 1277 1278 final int childOrder = offset > 0 ? ITEM_DIRECTION_TAIL : ITEM_DIRECTION_HEAD; 1279 if (offset > 0) { 1280 Arrays.fill(indexPerSpan, 0); 1281 } else { 1282 for (int i = 0; i < mSpanCount; i++) { 1283 indexPerSpan[i] = mSpans[i].mViews.size() - 1; 1284 } 1285 } 1286 1287 for (int i = 0; i < mSpanCount; i++) { 1288 offsetPerSpan[i] = mSpans[i].getNormalizedOffset(offset, targetStart, targetEnd); 1289 } 1290 if (DEBUG) { 1291 Log.d(TAG, "lazy offset start. normalized: " + Arrays.toString(offsetPerSpan)); 1292 } 1293 1294 while (childrenToOffset > 0) { 1295 View fullSpanView = null; 1296 for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) { 1297 Span span = mSpans[spanIndex]; 1298 int viewIndex; 1299 for (viewIndex = indexPerSpan[spanIndex]; 1300 viewIndex < span.mViews.size() && viewIndex >= 0; viewIndex += childOrder) { 1301 View view = span.mViews.get(viewIndex); 1302 if (DEBUG) { 1303 Log.d(TAG, "span " + spanIndex + ", view:" + viewIndex + ", pos:" 1304 + getPosition(view)); 1305 } 1306 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1307 if (lp.mFullSpan) { 1308 if (DEBUG) { 1309 Log.d(TAG, "stopping on full span view on index " + viewIndex 1310 + " in span " + spanIndex); 1311 } 1312 fullSpanView = view; 1313 viewIndex += childOrder;// move to next view 1314 break; 1315 } 1316 // offset this child normally 1317 mPrimaryOrientation.offsetChild(view, offsetPerSpan[spanIndex]); 1318 final int nextChildIndex = viewIndex + childOrder; 1319 if (nextChildIndex < span.mViews.size() && nextChildIndex >= 0) { 1320 View nextView = span.mViews.get(nextChildIndex); 1321 // find gap between, before offset 1322 if (childOrder == ITEM_DIRECTION_HEAD) {// negative 1323 offsetPerSpan[spanIndex] = Math 1324 .min(0, mPrimaryOrientation.getDecoratedStart(view) 1325 - mPrimaryOrientation.getDecoratedEnd(nextView)); 1326 } else { 1327 offsetPerSpan[spanIndex] = Math 1328 .max(0, mPrimaryOrientation.getDecoratedEnd(view) - 1329 mPrimaryOrientation.getDecoratedStart(nextView)); 1330 } 1331 if (DEBUG) { 1332 Log.d(TAG, "offset diff:" + offsetPerSpan[spanIndex] + " between " 1333 + getPosition(nextView) + " and " + getPosition(view)); 1334 } 1335 } 1336 childrenToOffset--; 1337 } 1338 indexPerSpan[spanIndex] = viewIndex; 1339 } 1340 if (fullSpanView != null) { 1341 // we have to offset this view. We'll offset it as the biggest amount necessary 1342 int winnerSpan = 0; 1343 int winnerSpanOffset = Math.abs(offsetPerSpan[winnerSpan]); 1344 for (int i = 1; i < mSpanCount; i++) { 1345 final int spanOffset = Math.abs(offsetPerSpan[i]); 1346 if (spanOffset > winnerSpanOffset) { 1347 winnerSpan = i; 1348 winnerSpanOffset = spanOffset; 1349 } 1350 } 1351 if (DEBUG) { 1352 Log.d(TAG, "winner offset:" + offsetPerSpan[winnerSpan] + " of " + winnerSpan); 1353 } 1354 mPrimaryOrientation.offsetChild(fullSpanView, offsetPerSpan[winnerSpan]); 1355 childrenToOffset--; 1356 1357 for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) { 1358 final int nextViewIndex = indexPerSpan[spanIndex]; 1359 final Span span = mSpans[spanIndex]; 1360 if (nextViewIndex < span.mViews.size() && nextViewIndex > 0) { 1361 View nextView = span.mViews.get(nextViewIndex); 1362 // find gap between, before offset 1363 if (childOrder == ITEM_DIRECTION_HEAD) {// negative 1364 offsetPerSpan[spanIndex] = Math 1365 .min(0, mPrimaryOrientation.getDecoratedStart(fullSpanView) 1366 - mPrimaryOrientation.getDecoratedEnd(nextView)); 1367 } else { 1368 offsetPerSpan[spanIndex] = Math 1369 .max(0, mPrimaryOrientation.getDecoratedEnd(fullSpanView) - 1370 mPrimaryOrientation.getDecoratedStart(nextView)); 1371 } 1372 } 1373 } 1374 } 1375 } 1376 for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) { 1377 mSpans[spanIndex].invalidateCache(); 1378 } 1379 } 1380 1381 private int getLastChildPosition() { 1382 final int childCount = getChildCount(); 1383 return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1)); 1384 } 1385 1386 private int getFirstChildPosition() { 1387 final int childCount = getChildCount(); 1388 return childCount == 0 ? 0 : getPosition(getChildAt(0)); 1389 } 1390 1391 @Override 1392 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 1393 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 1394 ViewGroup.LayoutParams.WRAP_CONTENT); 1395 } 1396 1397 @Override 1398 public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 1399 return new LayoutParams(c, attrs); 1400 } 1401 1402 @Override 1403 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 1404 if (lp instanceof ViewGroup.MarginLayoutParams) { 1405 return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 1406 } else { 1407 return new LayoutParams(lp); 1408 } 1409 } 1410 1411 @Override 1412 public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 1413 return lp instanceof LayoutParams; 1414 } 1415 1416 public int getOrientation() { 1417 return mOrientation; 1418 } 1419 1420 1421 /** 1422 * LayoutParams used by StaggeredGridLayoutManager. 1423 */ 1424 public static class LayoutParams extends RecyclerView.LayoutParams { 1425 1426 /** 1427 * Span Id for Views that are not laid out yet. 1428 */ 1429 public static final int INVALID_SPAN_ID = -1; 1430 1431 // Package scope to be able to access from tests. 1432 Span mSpan; 1433 1434 boolean mFullSpan; 1435 1436 public LayoutParams(Context c, AttributeSet attrs) { 1437 super(c, attrs); 1438 } 1439 1440 public LayoutParams(int width, int height) { 1441 super(width, height); 1442 } 1443 1444 public LayoutParams(ViewGroup.MarginLayoutParams source) { 1445 super(source); 1446 } 1447 1448 public LayoutParams(ViewGroup.LayoutParams source) { 1449 super(source); 1450 } 1451 1452 public LayoutParams(RecyclerView.LayoutParams source) { 1453 super(source); 1454 } 1455 1456 /** 1457 * When set to true, the item will layout using all span area. That means, if orientation 1458 * is vertical, the view will have full width; if orientation is horizontal, the view will 1459 * have full height. 1460 * 1461 * @param fullSpan True if this item should traverse all spans. 1462 */ 1463 public void setFullSpan(boolean fullSpan) { 1464 mFullSpan = fullSpan; 1465 } 1466 1467 /** 1468 * Returns the Span index to which this View is assigned. 1469 * 1470 * @return The Span index of the View. If View is not yet assigned to any span, returns 1471 * {@link #INVALID_SPAN_ID}. 1472 */ 1473 public final int getSpanIndex() { 1474 if (mSpan == null) { 1475 return INVALID_SPAN_ID; 1476 } 1477 return mSpan.mIndex; 1478 } 1479 } 1480 1481 // Package scoped to access from tests. 1482 class Span { 1483 1484 final int INVALID_LINE = Integer.MIN_VALUE; 1485 1486 private ArrayList<View> mViews = new ArrayList<View>(); 1487 1488 int mCachedStart = INVALID_LINE; 1489 1490 int mCachedEnd = INVALID_LINE; 1491 1492 int mDeletedSize = 0; 1493 1494 final int mIndex; 1495 1496 private Span(int index) { 1497 mIndex = index; 1498 } 1499 1500 int getStartLine(int def) { 1501 if (mCachedStart != INVALID_LINE) { 1502 return mCachedStart; 1503 } 1504 if (mViews.size() == 0) { 1505 return def; 1506 } 1507 mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0)); 1508 return mCachedStart; 1509 } 1510 1511 // Use this one when default value does not make sense and not having a value means a bug. 1512 int getStartLine() { 1513 if (mCachedStart != INVALID_LINE) { 1514 return mCachedStart; 1515 } 1516 mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0)); 1517 return mCachedStart; 1518 } 1519 1520 int getEndLine(int def) { 1521 if (mCachedEnd != INVALID_LINE) { 1522 return mCachedEnd; 1523 } 1524 final int size = mViews.size(); 1525 if (size == 0) { 1526 return def; 1527 } 1528 mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(size - 1)); 1529 return mCachedEnd; 1530 } 1531 1532 // Use this one when default value does not make sense and not having a value means a bug. 1533 int getEndLine() { 1534 if (mCachedEnd != INVALID_LINE) { 1535 return mCachedEnd; 1536 } 1537 mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(mViews.size() - 1)); 1538 return mCachedEnd; 1539 } 1540 1541 void prependToSpan(View view) { 1542 LayoutParams lp = getLayoutParams(view); 1543 lp.mSpan = this; 1544 mViews.add(0, view); 1545 mCachedStart = INVALID_LINE; 1546 if (mViews.size() == 1) { 1547 mCachedEnd = INVALID_LINE; 1548 } 1549 if (lp.isItemRemoved()) { 1550 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 1551 } 1552 } 1553 1554 void appendToSpan(View view) { 1555 LayoutParams lp = getLayoutParams(view); 1556 lp.mSpan = this; 1557 mViews.add(view); 1558 mCachedEnd = INVALID_LINE; 1559 if (mViews.size() == 1) { 1560 mCachedStart = INVALID_LINE; 1561 } 1562 if (lp.isItemRemoved()) { 1563 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 1564 } 1565 } 1566 1567 // Useful method to preserve positions on a re-layout. 1568 void cacheReferenceLineAndClear(boolean reverseLayout, int offset) { 1569 int reference; 1570 if (reverseLayout) { 1571 reference = getEndLine(INVALID_LINE); 1572 } else { 1573 reference = getStartLine(INVALID_LINE); 1574 } 1575 clear(); 1576 if (reference == INVALID_LINE) { 1577 return; 1578 } 1579 if (offset != INVALID_OFFSET) { 1580 reference += offset; 1581 } 1582 mCachedStart = mCachedEnd = reference; 1583 } 1584 1585 void clear() { 1586 mViews.clear(); 1587 invalidateCache(); 1588 mDeletedSize = 0; 1589 } 1590 1591 void invalidateCache() { 1592 mCachedStart = INVALID_LINE; 1593 mCachedEnd = INVALID_LINE; 1594 } 1595 1596 void setLine(int line) { 1597 mCachedEnd = mCachedStart = line; 1598 } 1599 1600 void popEnd() { 1601 final int size = mViews.size(); 1602 View end = mViews.remove(size - 1); 1603 final LayoutParams lp = getLayoutParams(end); 1604 lp.mSpan = null; 1605 if (lp.isItemRemoved()) { 1606 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end); 1607 } 1608 if (size == 1) { 1609 mCachedStart = INVALID_LINE; 1610 } 1611 mCachedEnd = INVALID_LINE; 1612 } 1613 1614 void popStart() { 1615 View start = mViews.remove(0); 1616 final LayoutParams lp = getLayoutParams(start); 1617 lp.mSpan = null; 1618 if (mViews.size() == 0) { 1619 mCachedEnd = INVALID_LINE; 1620 } 1621 if (lp.isItemRemoved()) { 1622 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start); 1623 } 1624 mCachedStart = INVALID_LINE; 1625 } 1626 1627 // TODO cache this. 1628 public int getDeletedSize() { 1629 return mDeletedSize; 1630 } 1631 1632 LayoutParams getLayoutParams(View view) { 1633 return (LayoutParams) view.getLayoutParams(); 1634 } 1635 1636 void onOffset(int dt) { 1637 if (mCachedStart != INVALID_LINE) { 1638 mCachedStart += dt; 1639 } 1640 if (mCachedEnd != INVALID_LINE) { 1641 mCachedEnd += dt; 1642 } 1643 } 1644 1645 // normalized offset is how much this span can scroll 1646 int getNormalizedOffset(int dt, int targetStart, int targetEnd) { 1647 if (mViews.size() == 0) { 1648 return 0; 1649 } 1650 if (dt < 0) { 1651 final int endSpace = getEndLine() - targetEnd; 1652 if (endSpace <= 0) { 1653 return 0; 1654 } 1655 return -dt > endSpace ? -endSpace : dt; 1656 } else { 1657 final int startSpace = targetStart - getStartLine(); 1658 if (startSpace <= 0) { 1659 return 0; 1660 } 1661 return startSpace < dt ? startSpace : dt; 1662 } 1663 } 1664 1665 /** 1666 * Returns if there is no child between start-end lines 1667 * 1668 * @param start The start line 1669 * @param end The end line 1670 * @return true if a new child can be added between start and end 1671 */ 1672 boolean isEmpty(int start, int end) { 1673 final int count = mViews.size(); 1674 for (int i = 0; i < count; i++) { 1675 final View view = mViews.get(i); 1676 if (mPrimaryOrientation.getDecoratedStart(view) < end && 1677 mPrimaryOrientation.getDecoratedEnd(view) > start) { 1678 return false; 1679 } 1680 } 1681 return true; 1682 } 1683 } 1684 1685 /** 1686 * An array of mappings from adapter position to span. 1687 * This only grows when a write happens and it grows up to the size of the adapter. 1688 */ 1689 static class LazySpanLookup { 1690 1691 private static final int MIN_SIZE = 10; 1692 1693 int[] mData; 1694 1695 int mAdapterSize; // we don't want to grow beyond that, unless it grows 1696 1697 void invalidateAfter(int position) { 1698 if (mData == null) { 1699 return; 1700 } 1701 if (position >= mData.length) { 1702 return; 1703 } 1704 Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID); 1705 } 1706 1707 int getSpan(int position) { 1708 if (mData == null || position >= mData.length) { 1709 return LayoutParams.INVALID_SPAN_ID; 1710 } else { 1711 return mData[position]; 1712 } 1713 } 1714 1715 void setSpan(int position, Span span) { 1716 ensureSize(position); 1717 mData[position] = span.mIndex; 1718 } 1719 1720 int sizeForPosition(int position) { 1721 int len = mData.length; 1722 while (len <= position) { 1723 len *= 2; 1724 } 1725 if (len > mAdapterSize) { 1726 len = mAdapterSize; 1727 } 1728 return len; 1729 } 1730 1731 void ensureSize(int position) { 1732 if (mData == null) { 1733 mData = new int[Math.max(position, MIN_SIZE) + 1]; 1734 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 1735 } else if (position >= mData.length) { 1736 int[] old = mData; 1737 mData = new int[sizeForPosition(position)]; 1738 System.arraycopy(old, 0, mData, 0, old.length); 1739 Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID); 1740 } 1741 } 1742 1743 void clear() { 1744 if (mData != null) { 1745 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 1746 } 1747 } 1748 1749 void offsetForRemoval(int positionStart, int itemCount) { 1750 ensureSize(positionStart + itemCount); 1751 System.arraycopy(mData, positionStart + itemCount, mData, positionStart, 1752 mData.length - positionStart - itemCount); 1753 Arrays.fill(mData, mData.length - itemCount, mData.length, 1754 LayoutParams.INVALID_SPAN_ID); 1755 } 1756 1757 void offsetForAddition(int positionStart, int itemCount) { 1758 ensureSize(positionStart + itemCount); 1759 System.arraycopy(mData, positionStart, mData, positionStart + itemCount, 1760 mData.length - positionStart - itemCount); 1761 Arrays.fill(mData, positionStart, positionStart + itemCount, 1762 LayoutParams.INVALID_SPAN_ID); 1763 } 1764 } 1765 1766 static class SavedState implements Parcelable { 1767 1768 int mOrientation; 1769 1770 int mSpanCount; 1771 1772 int mGapStrategy; 1773 1774 int mAnchorPosition; 1775 1776 int[] mSpanOffsets; 1777 1778 int mSpanLookupSize; 1779 1780 int[] mSpanLookup; 1781 1782 boolean mReverseLayout; 1783 1784 boolean mAnchorLayoutFromEnd; 1785 1786 boolean mHasSpanOffsets; 1787 1788 public SavedState() { 1789 } 1790 1791 SavedState(Parcel in) { 1792 mOrientation = in.readInt(); 1793 mSpanCount = in.readInt(); 1794 mGapStrategy = in.readInt(); 1795 mAnchorPosition = in.readInt(); 1796 mHasSpanOffsets = in.readInt() == 1; 1797 if (mHasSpanOffsets) { 1798 mSpanOffsets = new int[mSpanCount]; 1799 in.readIntArray(mSpanOffsets); 1800 } 1801 1802 mSpanLookupSize = in.readInt(); 1803 if (mSpanLookupSize > 0) { 1804 mSpanLookup = new int[mSpanLookupSize]; 1805 in.readIntArray(mSpanLookup); 1806 } 1807 mReverseLayout = in.readInt() == 1; 1808 mAnchorLayoutFromEnd = in.readInt() == 1; 1809 } 1810 1811 public SavedState(SavedState other) { 1812 mOrientation = other.mOrientation; 1813 mSpanCount = other.mSpanCount; 1814 mGapStrategy = other.mGapStrategy; 1815 mAnchorPosition = other.mAnchorPosition; 1816 mHasSpanOffsets = other.mHasSpanOffsets; 1817 mSpanOffsets = other.mSpanOffsets; 1818 mSpanLookupSize = other.mSpanLookupSize; 1819 mSpanLookup = other.mSpanLookup; 1820 mReverseLayout = other.mReverseLayout; 1821 mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; 1822 } 1823 1824 void invalidateSpanInfo() { 1825 mSpanOffsets = null; 1826 mHasSpanOffsets = false; 1827 mSpanCount = -1; 1828 mSpanLookupSize = 0; 1829 mSpanLookup = null; 1830 } 1831 1832 void invalidateAnchorPositionInfo() { 1833 mSpanOffsets = null; 1834 mHasSpanOffsets = false; 1835 mAnchorPosition = RecyclerView.NO_POSITION; 1836 } 1837 1838 @Override 1839 public int describeContents() { 1840 return 0; 1841 } 1842 1843 @Override 1844 public void writeToParcel(Parcel dest, int flags) { 1845 dest.writeInt(mOrientation); 1846 dest.writeInt(mSpanCount); 1847 dest.writeInt(mGapStrategy); 1848 dest.writeInt(mAnchorPosition); 1849 dest.writeInt(mHasSpanOffsets ? 1 : 0); 1850 if (mHasSpanOffsets) { 1851 dest.writeIntArray(mSpanOffsets); 1852 } 1853 dest.writeInt(mSpanLookupSize); 1854 if (mSpanLookupSize > 0) { 1855 dest.writeIntArray(mSpanLookup); 1856 } 1857 dest.writeInt(mReverseLayout ? 1 : 0); 1858 dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); 1859 } 1860 1861 @Override 1862 public String toString() { 1863 return "SavedState{" + 1864 "mOrientation=" + mOrientation + 1865 ", mSpanCount=" + mSpanCount + 1866 ", mGapStrategy=" + mGapStrategy + 1867 ", mAnchorPosition=" + mAnchorPosition + 1868 ", mSpanOffsets=" + Arrays.toString(mSpanOffsets) + 1869 ", mSpanLookupSize=" + mSpanLookupSize + 1870 ", mSpanLookup=" + Arrays.toString(mSpanLookup) + 1871 ", mReverseLayout=" + mReverseLayout + 1872 ", mAnchorLayoutFromEnd=" + mAnchorLayoutFromEnd + 1873 ", mHasSpanOffsets=" + mHasSpanOffsets + 1874 '}'; 1875 } 1876 1877 public static final Parcelable.Creator<SavedState> CREATOR 1878 = new Parcelable.Creator<SavedState>() { 1879 @Override 1880 public SavedState createFromParcel(Parcel in) { 1881 return new SavedState(in); 1882 } 1883 1884 @Override 1885 public SavedState[] newArray(int size) { 1886 return new SavedState[size]; 1887 } 1888 }; 1889 } 1890} 1891