StaggeredGridLayoutManager.java revision a910619e83d0052e1d81aa5fe532821a2f99d76c
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.support.v4.view.accessibility.AccessibilityEventCompat; 26import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 27import android.support.v4.view.accessibility.AccessibilityRecordCompat; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.view.View; 31import android.view.ViewGroup; 32import android.view.accessibility.AccessibilityEvent; 33 34import java.util.ArrayList; 35import java.util.Arrays; 36import java.util.BitSet; 37import java.util.List; 38 39import static android.support.v7.widget.LayoutState.LAYOUT_START; 40import static android.support.v7.widget.LayoutState.LAYOUT_END; 41import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD; 42import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL; 43import static android.support.v7.widget.RecyclerView.NO_POSITION; 44 45/** 46 * A LayoutManager that lays out children in a staggered grid formation. 47 * It supports horizontal & vertical layout as well as an ability to layout children in reverse. 48 * <p> 49 * Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps, 50 * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can 51 * control this behavior via {@link #setGapStrategy(int)}. 52 */ 53public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { 54 55 public static final String TAG = "StaggeredGridLayoutManager"; 56 57 private static final boolean DEBUG = false; 58 59 public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; 60 61 public static final int VERTICAL = OrientationHelper.VERTICAL; 62 63 /** 64 * Does not do anything to hide gaps. 65 */ 66 public static final int GAP_HANDLING_NONE = 0; 67 68 @Deprecated 69 public static final int GAP_HANDLING_LAZY = 1; 70 71 /** 72 * When scroll state is changed to {@link RecyclerView#SCROLL_STATE_IDLE}, StaggeredGrid will 73 * check if there are gaps in the because of full span items. If it finds, it will re-layout 74 * and move items to correct positions with animations. 75 * <p> 76 * For example, if LayoutManager ends up with the following layout due to adapter changes: 77 * <code> 78 * AAA 79 * _BC 80 * DDD 81 * </code> 82 * It will animate to the following state: 83 * <code> 84 * AAA 85 * BC_ 86 * DDD 87 * </code> 88 */ 89 public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2; 90 91 private static final int INVALID_OFFSET = Integer.MIN_VALUE; 92 93 /** 94 * Number of spans 95 */ 96 private int mSpanCount = -1; 97 98 private Span[] mSpans; 99 100 /** 101 * Primary orientation is the layout's orientation, secondary orientation is the orientation 102 * for spans. Having both makes code much cleaner for calculations. 103 */ 104 OrientationHelper mPrimaryOrientation; 105 OrientationHelper mSecondaryOrientation; 106 107 private int mOrientation; 108 109 /** 110 * The width or height per span, depending on the orientation. 111 */ 112 private int mSizePerSpan; 113 114 private LayoutState mLayoutState; 115 116 private boolean mReverseLayout = false; 117 118 /** 119 * Aggregated reverse layout value that takes RTL into account. 120 */ 121 boolean mShouldReverseLayout = false; 122 123 /** 124 * Temporary variable used during fill method to check which spans needs to be filled. 125 */ 126 private BitSet mRemainingSpans; 127 128 /** 129 * When LayoutManager needs to scroll to a position, it sets this variable and requests a 130 * layout which will check this variable and re-layout accordingly. 131 */ 132 int mPendingScrollPosition = NO_POSITION; 133 134 /** 135 * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is 136 * called. 137 */ 138 int mPendingScrollPositionOffset = INVALID_OFFSET; 139 140 /** 141 * Keeps the mapping between the adapter positions and spans. This is necessary to provide 142 * a consistent experience when user scrolls the list. 143 */ 144 LazySpanLookup mLazySpanLookup = new LazySpanLookup(); 145 146 /** 147 * how we handle gaps in UI. 148 */ 149 private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 150 151 /** 152 * Saved state needs this information to properly layout on restore. 153 */ 154 private boolean mLastLayoutFromEnd; 155 156 /** 157 * Saved state and onLayout needs this information to re-layout properly 158 */ 159 private boolean mLastLayoutRTL; 160 161 /** 162 * SavedState is not handled until a layout happens. This is where we keep it until next 163 * layout. 164 */ 165 private SavedState mPendingSavedState; 166 167 /** 168 * Re-used measurement specs. updated by onLayout. 169 */ 170 private int mFullSizeSpec, mWidthSpec, mHeightSpec; 171 172 /** 173 * Re-used anchor info. 174 */ 175 private final AnchorInfo mAnchorInfo = new AnchorInfo(); 176 177 /** 178 * If a full span item is invalid / or created in reverse direction; it may create gaps in 179 * the UI. While laying out, if such case is detected, we set this flag. 180 * <p> 181 * After scrolling stops, we check this flag and if it is set, re-layout. 182 */ 183 private boolean mLaidOutInvalidFullSpan = false; 184 185 /** 186 * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. 187 * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} 188 */ 189 private boolean mSmoothScrollbarEnabled = true; 190 191 private final Runnable checkForGapsRunnable = new Runnable() { 192 @Override 193 public void run() { 194 checkForGaps(); 195 } 196 }; 197 198 /** 199 * Creates a StaggeredGridLayoutManager with given parameters. 200 * 201 * @param spanCount If orientation is vertical, spanCount is number of columns. If 202 * orientation is horizontal, spanCount is number of rows. 203 * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL} 204 */ 205 public StaggeredGridLayoutManager(int spanCount, int orientation) { 206 mOrientation = orientation; 207 setSpanCount(spanCount); 208 } 209 210 /** 211 * Checks for gaps in the UI that may be caused by adapter changes. 212 * <p> 213 * When a full span item is laid out in reverse direction, it sets a flag which we check when 214 * scroll is stopped (or re-layout happens) and re-layout after first valid item. 215 */ 216 private void checkForGaps() { 217 if (getChildCount() == 0 || mGapStrategy == GAP_HANDLING_NONE) { 218 return; 219 } 220 final int minPos, maxPos; 221 if (mShouldReverseLayout) { 222 minPos = getLastChildPosition(); 223 maxPos = getFirstChildPosition(); 224 } else { 225 minPos = getFirstChildPosition(); 226 maxPos = getLastChildPosition(); 227 } 228 if (minPos == 0) { 229 View gapView = hasGapsToFix(); 230 if (gapView != null) { 231 mLazySpanLookup.clear(); 232 requestSimpleAnimationsInNextLayout(); 233 requestLayout(); 234 return; 235 } 236 } 237 if (!mLaidOutInvalidFullSpan) { 238 return; 239 } 240 int invalidGapDir = mShouldReverseLayout ? LAYOUT_START : LAYOUT_END; 241 final LazySpanLookup.FullSpanItem invalidFsi = mLazySpanLookup 242 .getFirstFullSpanItemInRange(minPos, maxPos + 1, invalidGapDir); 243 if (invalidFsi == null) { 244 mLaidOutInvalidFullSpan = false; 245 mLazySpanLookup.forceInvalidateAfter(maxPos + 1); 246 return; 247 } 248 final LazySpanLookup.FullSpanItem validFsi = mLazySpanLookup 249 .getFirstFullSpanItemInRange(minPos, invalidFsi.mPosition, 250 invalidGapDir * -1); 251 if (validFsi == null) { 252 mLazySpanLookup.forceInvalidateAfter(invalidFsi.mPosition); 253 } else { 254 mLazySpanLookup.forceInvalidateAfter(validFsi.mPosition + 1); 255 } 256 requestSimpleAnimationsInNextLayout(); 257 requestLayout(); 258 } 259 260 @Override 261 public void onScrollStateChanged(int state) { 262 if (state == RecyclerView.SCROLL_STATE_IDLE) { 263 checkForGaps(); 264 } 265 } 266 267 @Override 268 public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 269 for (int i = 0; i < mSpanCount; i++) { 270 mSpans[i].clear(); 271 } 272 } 273 274 /** 275 * Checks for gaps if we've reached to the top of the list. 276 * <p> 277 * Intermediate gaps created by full span items are tracked via mLaidOutInvalidFullSpan field. 278 */ 279 View hasGapsToFix() { 280 int startChildIndex = 0; 281 int endChildIndex = getChildCount() - 1; 282 BitSet mSpansToCheck = new BitSet(mSpanCount); 283 mSpansToCheck.set(0, mSpanCount, true); 284 285 final int firstChildIndex, childLimit; 286 final int preferredSpanDir = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1; 287 288 if (mShouldReverseLayout) { 289 firstChildIndex = endChildIndex - 1; 290 childLimit = startChildIndex - 1; 291 } else { 292 firstChildIndex = startChildIndex; 293 childLimit = endChildIndex; 294 } 295 final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1; 296 for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) { 297 View child = getChildAt(i); 298 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 299 if (mSpansToCheck.get(lp.mSpan.mIndex)) { 300 if (checkSpanForGap(lp.mSpan)) { 301 return child; 302 } 303 mSpansToCheck.clear(lp.mSpan.mIndex); 304 } 305 if (lp.mFullSpan) { 306 continue; // quick reject 307 } 308 309 if (i + nextChildDiff != childLimit) { 310 View nextChild = getChildAt(i + nextChildDiff); 311 boolean compareSpans = false; 312 if (mShouldReverseLayout) { 313 // ensure child's end is below nextChild's end 314 int myEnd = mPrimaryOrientation.getDecoratedEnd(child); 315 int nextEnd = mPrimaryOrientation.getDecoratedEnd(nextChild); 316 if (myEnd < nextEnd) { 317 return child;//i should have a better position 318 } else if (myEnd == nextEnd) { 319 compareSpans = true; 320 } 321 } else { 322 int myStart = mPrimaryOrientation.getDecoratedStart(child); 323 int nextStart = mPrimaryOrientation.getDecoratedStart(nextChild); 324 if (myStart > nextStart) { 325 return child;//i should have a better position 326 } else if (myStart == nextStart) { 327 compareSpans = true; 328 } 329 } 330 if (compareSpans) { 331 // equal, check span indices. 332 LayoutParams nextLp = (LayoutParams) nextChild.getLayoutParams(); 333 if (lp.mSpan.mIndex - nextLp.mSpan.mIndex < 0 != preferredSpanDir < 0) { 334 return child; 335 } 336 } 337 } 338 } 339 // everything looks good 340 return null; 341 } 342 343 private boolean checkSpanForGap(Span span) { 344 if (mShouldReverseLayout) { 345 if (span.getEndLine() < mPrimaryOrientation.getEndAfterPadding()) { 346 return true; 347 } 348 } else if (span.getStartLine() > mPrimaryOrientation.getStartAfterPadding()) { 349 return true; 350 } 351 return false; 352 } 353 354 /** 355 * Sets the number of spans for the layout. This will invalidate all of the span assignments 356 * for Views. 357 * <p> 358 * Calling this method will automatically result in a new layout request unless the spanCount 359 * parameter is equal to current span count. 360 * 361 * @param spanCount Number of spans to layout 362 */ 363 public void setSpanCount(int spanCount) { 364 assertNotInLayoutOrScroll(null); 365 if (spanCount != mSpanCount) { 366 invalidateSpanAssignments(); 367 mSpanCount = spanCount; 368 mRemainingSpans = new BitSet(mSpanCount); 369 mSpans = new Span[mSpanCount]; 370 for (int i = 0; i < mSpanCount; i++) { 371 mSpans[i] = new Span(i); 372 } 373 requestLayout(); 374 } 375 } 376 377 /** 378 * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep 379 * scroll position if this method is called after views are laid out. 380 * 381 * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} 382 */ 383 public void setOrientation(int orientation) { 384 if (orientation != HORIZONTAL && orientation != VERTICAL) { 385 throw new IllegalArgumentException("invalid orientation."); 386 } 387 assertNotInLayoutOrScroll(null); 388 if (orientation == mOrientation) { 389 return; 390 } 391 mOrientation = orientation; 392 if (mPrimaryOrientation != null && mSecondaryOrientation != null) { 393 // swap 394 OrientationHelper tmp = mPrimaryOrientation; 395 mPrimaryOrientation = mSecondaryOrientation; 396 mSecondaryOrientation = tmp; 397 } 398 requestLayout(); 399 } 400 401 /** 402 * Sets whether LayoutManager should start laying out items from the end of the UI. The order 403 * items are traversed is not affected by this call. 404 * <p> 405 * For vertical layout, if it is set to <code>true</code>, first item will be at the bottom of 406 * the list. 407 * <p> 408 * For horizontal layouts, it depends on the layout direction. 409 * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if 410 * {@link RecyclerView}} is RTL, it will layout from LTR. 411 * 412 * @param reverseLayout Whether layout should be in reverse or not 413 */ 414 public void setReverseLayout(boolean reverseLayout) { 415 assertNotInLayoutOrScroll(null); 416 if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) { 417 mPendingSavedState.mReverseLayout = reverseLayout; 418 } 419 mReverseLayout = reverseLayout; 420 requestLayout(); 421 } 422 423 /** 424 * Returns the current gap handling strategy for StaggeredGridLayoutManager. 425 * <p> 426 * Staggered grid may have gaps in the layout due to changes in the adapter. To avoid gaps, 427 * StaggeredGridLayoutManager provides 2 options. Check {@link #GAP_HANDLING_NONE} and 428 * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} for details. 429 * <p> 430 * By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}. 431 * 432 * @return Current gap handling strategy. 433 * @see #setGapStrategy(int) 434 * @see #GAP_HANDLING_NONE 435 * @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS 436 */ 437 public int getGapStrategy() { 438 return mGapStrategy; 439 } 440 441 /** 442 * Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter 443 * is different than the current strategy, calling this method will trigger a layout request. 444 * 445 * @param gapStrategy The new gap handling strategy. Should be 446 * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or {@link 447 * #GAP_HANDLING_NONE}. 448 * @see #getGapStrategy() 449 */ 450 public void setGapStrategy(int gapStrategy) { 451 assertNotInLayoutOrScroll(null); 452 if (gapStrategy == mGapStrategy) { 453 return; 454 } 455 if (gapStrategy != GAP_HANDLING_NONE && 456 gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) { 457 throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE " 458 + "or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS"); 459 } 460 mGapStrategy = gapStrategy; 461 requestLayout(); 462 } 463 464 @Override 465 public void assertNotInLayoutOrScroll(String message) { 466 if (mPendingSavedState == null) { 467 super.assertNotInLayoutOrScroll(message); 468 } 469 } 470 471 /** 472 * Returns the number of spans laid out by StaggeredGridLayoutManager. 473 * 474 * @return Number of spans in the layout 475 */ 476 public int getSpanCount() { 477 return mSpanCount; 478 } 479 480 /** 481 * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items. 482 * <p> 483 * If you need to cancel current assignments, you can call this method which will clear all 484 * assignments and request a new layout. 485 */ 486 public void invalidateSpanAssignments() { 487 mLazySpanLookup.clear(); 488 requestLayout(); 489 } 490 491 private void ensureOrientationHelper() { 492 if (mPrimaryOrientation == null) { 493 mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation); 494 mSecondaryOrientation = OrientationHelper 495 .createOrientationHelper(this, 1 - mOrientation); 496 mLayoutState = new LayoutState(); 497 } 498 } 499 500 /** 501 * Calculates the views' layout order. (e.g. from end to start or start to end) 502 * RTL layout support is applied automatically. So if layout is RTL and 503 * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. 504 */ 505 private void resolveShouldLayoutReverse() { 506 // A == B is the same result, but we rather keep it readable 507 if (mOrientation == VERTICAL || !isLayoutRTL()) { 508 mShouldReverseLayout = mReverseLayout; 509 } else { 510 mShouldReverseLayout = !mReverseLayout; 511 } 512 } 513 514 boolean isLayoutRTL() { 515 return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; 516 } 517 518 /** 519 * Returns whether views are laid out in reverse order or not. 520 * <p> 521 * Not that this value is not affected by RecyclerView's layout direction. 522 * 523 * @return True if layout is reversed, false otherwise 524 * @see #setReverseLayout(boolean) 525 */ 526 public boolean getReverseLayout() { 527 return mReverseLayout; 528 } 529 530 @Override 531 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 532 ensureOrientationHelper(); 533 // Update adapter size. 534 mLazySpanLookup.mAdapterSize = state.getItemCount(); 535 536 final AnchorInfo anchorInfo = mAnchorInfo; 537 anchorInfo.reset(); 538 539 if (mPendingSavedState != null) { 540 applyPendingSavedState(anchorInfo); 541 } else { 542 resolveShouldLayoutReverse(); 543 anchorInfo.mLayoutFromEnd = mShouldReverseLayout; 544 } 545 546 updateAnchorInfoForLayout(state, anchorInfo); 547 548 if (mPendingSavedState == null) { 549 if (anchorInfo.mLayoutFromEnd != mLastLayoutFromEnd || 550 isLayoutRTL() != mLastLayoutRTL) { 551 mLazySpanLookup.clear(); 552 anchorInfo.mInvalidateOffsets = true; 553 } 554 } 555 556 if (getChildCount() > 0 && (mPendingSavedState == null || 557 mPendingSavedState.mSpanOffsetsSize < 1)) { 558 if (anchorInfo.mInvalidateOffsets) { 559 for (int i = 0; i < mSpanCount; i++) { 560 // Scroll to position is set, clear. 561 mSpans[i].clear(); 562 if (anchorInfo.mOffset != INVALID_OFFSET) { 563 mSpans[i].setLine(anchorInfo.mOffset); 564 } 565 } 566 } else { 567 for (int i = 0; i < mSpanCount; i++) { 568 mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout, anchorInfo.mOffset); 569 } 570 } 571 } 572 detachAndScrapAttachedViews(recycler); 573 mLaidOutInvalidFullSpan = false; 574 updateMeasureSpecs(); 575 if (anchorInfo.mLayoutFromEnd) { 576 // Layout start. 577 updateLayoutStateToFillStart(anchorInfo.mPosition, state); 578 fill(recycler, mLayoutState, state); 579 // Layout end. 580 updateLayoutStateToFillEnd(anchorInfo.mPosition, state); 581 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 582 fill(recycler, mLayoutState, state); 583 } else { 584 // Layout end. 585 updateLayoutStateToFillEnd(anchorInfo.mPosition, state); 586 fill(recycler, mLayoutState, state); 587 // Layout start. 588 updateLayoutStateToFillStart(anchorInfo.mPosition, state); 589 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 590 fill(recycler, mLayoutState, state); 591 } 592 593 if (getChildCount() > 0) { 594 if (mShouldReverseLayout) { 595 fixEndGap(recycler, state, true); 596 fixStartGap(recycler, state, false); 597 } else { 598 fixStartGap(recycler, state, true); 599 fixEndGap(recycler, state, false); 600 } 601 } 602 603 if (!state.isPreLayout()) { 604 if (getChildCount() > 0 && mPendingScrollPosition != NO_POSITION && 605 mLaidOutInvalidFullSpan) { 606 ViewCompat.postOnAnimation(getChildAt(0), checkForGapsRunnable); 607 } 608 mPendingScrollPosition = NO_POSITION; 609 mPendingScrollPositionOffset = INVALID_OFFSET; 610 } 611 mLastLayoutFromEnd = anchorInfo.mLayoutFromEnd; 612 mLastLayoutRTL = isLayoutRTL(); 613 mPendingSavedState = null; // we don't need this anymore 614 } 615 616 private void applyPendingSavedState(AnchorInfo anchorInfo) { 617 if (DEBUG) { 618 Log.d(TAG, "found saved state: " + mPendingSavedState); 619 } 620 if (mPendingSavedState.mSpanOffsetsSize > 0) { 621 if (mPendingSavedState.mSpanOffsetsSize == mSpanCount) { 622 for (int i = 0; i < mSpanCount; i++) { 623 mSpans[i].clear(); 624 int line = mPendingSavedState.mSpanOffsets[i]; 625 if (line != Span.INVALID_LINE) { 626 if (mPendingSavedState.mAnchorLayoutFromEnd) { 627 line += mPrimaryOrientation.getEndAfterPadding(); 628 } else { 629 line += mPrimaryOrientation.getStartAfterPadding(); 630 } 631 } 632 mSpans[i].setLine(line); 633 } 634 } else { 635 mPendingSavedState.invalidateSpanInfo(); 636 mPendingSavedState.mAnchorPosition = mPendingSavedState.mVisibleAnchorPosition; 637 } 638 } 639 mLastLayoutRTL = mPendingSavedState.mLastLayoutRTL; 640 setReverseLayout(mPendingSavedState.mReverseLayout); 641 resolveShouldLayoutReverse(); 642 643 if (mPendingSavedState.mAnchorPosition != NO_POSITION) { 644 mPendingScrollPosition = mPendingSavedState.mAnchorPosition; 645 anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; 646 } else { 647 anchorInfo.mLayoutFromEnd = mShouldReverseLayout; 648 } 649 if (mPendingSavedState.mSpanLookupSize > 1) { 650 mLazySpanLookup.mData = mPendingSavedState.mSpanLookup; 651 mLazySpanLookup.mFullSpanItems = mPendingSavedState.mFullSpanItems; 652 } 653 } 654 655 void updateAnchorInfoForLayout(RecyclerView.State state, AnchorInfo anchorInfo) { 656 if (updateAnchorFromPendingData(state, anchorInfo)) { 657 return; 658 } 659 if (updateAnchorFromChildren(state, anchorInfo)) { 660 return; 661 } 662 if (DEBUG) { 663 Log.d(TAG, "Deciding anchor info from fresh state"); 664 } 665 anchorInfo.assignCoordinateFromPadding(); 666 anchorInfo.mPosition = 0; 667 } 668 669 private boolean updateAnchorFromChildren(RecyclerView.State state, AnchorInfo anchorInfo) { 670 // We don't recycle views out of adapter order. This way, we can rely on the first or 671 // last child as the anchor position. 672 // Layout direction may change but we should select the child depending on the latest 673 // layout direction. Otherwise, we'll choose the wrong child. 674 anchorInfo.mPosition = mLastLayoutFromEnd 675 ? findLastReferenceChildPosition(state.getItemCount()) 676 : findFirstReferenceChildPosition(state.getItemCount()); 677 anchorInfo.mOffset = INVALID_OFFSET; 678 return true; 679 } 680 681 boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { 682 // Validate scroll position if exists. 683 if (state.isPreLayout() || mPendingScrollPosition == NO_POSITION) { 684 return false; 685 } 686 // Validate it. 687 if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { 688 mPendingScrollPosition = NO_POSITION; 689 mPendingScrollPositionOffset = INVALID_OFFSET; 690 return false; 691 } 692 693 if (mPendingSavedState == null || mPendingSavedState.mAnchorPosition == NO_POSITION 694 || mPendingSavedState.mSpanOffsetsSize < 1) { 695 // If item is visible, make it fully visible. 696 final View child = findViewByPosition(mPendingScrollPosition); 697 if (child != null) { 698 // Use regular anchor position, offset according to pending offset and target 699 // child 700 anchorInfo.mPosition = mShouldReverseLayout ? getLastChildPosition() 701 : getFirstChildPosition(); 702 703 if (mPendingScrollPositionOffset != INVALID_OFFSET) { 704 if (anchorInfo.mLayoutFromEnd) { 705 final int target = mPrimaryOrientation.getEndAfterPadding() - 706 mPendingScrollPositionOffset; 707 anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedEnd(child); 708 } else { 709 final int target = mPrimaryOrientation.getStartAfterPadding() + 710 mPendingScrollPositionOffset; 711 anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedStart(child); 712 } 713 return true; 714 } 715 716 // no offset provided. Decide according to the child location 717 final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child); 718 if (childSize > mPrimaryOrientation.getTotalSpace()) { 719 // Item does not fit. Fix depending on layout direction. 720 anchorInfo.mOffset = anchorInfo.mLayoutFromEnd 721 ? mPrimaryOrientation.getEndAfterPadding() 722 : mPrimaryOrientation.getStartAfterPadding(); 723 return true; 724 } 725 726 final int startGap = mPrimaryOrientation.getDecoratedStart(child) 727 - mPrimaryOrientation.getStartAfterPadding(); 728 if (startGap < 0) { 729 anchorInfo.mOffset = -startGap; 730 return true; 731 } 732 final int endGap = mPrimaryOrientation.getEndAfterPadding() - 733 mPrimaryOrientation.getDecoratedEnd(child); 734 if (endGap < 0) { 735 anchorInfo.mOffset = endGap; 736 return true; 737 } 738 // child already visible. just layout as usual 739 anchorInfo.mOffset = INVALID_OFFSET; 740 } else { 741 // Child is not visible. Set anchor coordinate depending on in which direction 742 // child will be visible. 743 anchorInfo.mPosition = mPendingScrollPosition; 744 if (mPendingScrollPositionOffset == INVALID_OFFSET) { 745 final int position = calculateScrollDirectionForPosition( 746 anchorInfo.mPosition); 747 anchorInfo.mLayoutFromEnd = position == LAYOUT_END; 748 anchorInfo.assignCoordinateFromPadding(); 749 } else { 750 anchorInfo.assignCoordinateFromPadding(mPendingScrollPositionOffset); 751 } 752 anchorInfo.mInvalidateOffsets = true; 753 } 754 } else { 755 anchorInfo.mOffset = INVALID_OFFSET; 756 anchorInfo.mPosition = mPendingScrollPosition; 757 } 758 return true; 759 } 760 761 void updateMeasureSpecs() { 762 mSizePerSpan = mSecondaryOrientation.getTotalSpace() / mSpanCount; 763 mFullSizeSpec = View.MeasureSpec.makeMeasureSpec( 764 mSecondaryOrientation.getTotalSpace(), View.MeasureSpec.EXACTLY); 765 if (mOrientation == VERTICAL) { 766 mWidthSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); 767 mHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 768 } else { 769 mHeightSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); 770 mWidthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 771 } 772 } 773 774 @Override 775 public boolean supportsPredictiveItemAnimations() { 776 return mPendingSavedState == null; 777 } 778 779 /** 780 * Returns the adapter position of the first visible view for each span. 781 * <p> 782 * Note that, this value is not affected by layout orientation or item order traversal. 783 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 784 * not in the layout. 785 * <p> 786 * If RecyclerView has item decorators, they will be considered in calculations as well. 787 * <p> 788 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 789 * views are ignored in this method. 790 * 791 * @param into An array to put the results into. If you don't provide any, LayoutManager will 792 * create a new one. 793 * @return The adapter position of the first visible item in each span. If a span does not have 794 * any items, {@link RecyclerView#NO_POSITION} is returned for that span. 795 * @see #findFirstCompletelyVisibleItemPositions(int[]) 796 * @see #findLastVisibleItemPositions(int[]) 797 */ 798 public int[] findFirstVisibleItemPositions(int[] into) { 799 if (into == null) { 800 into = new int[mSpanCount]; 801 } else if (into.length < mSpanCount) { 802 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 803 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 804 } 805 for (int i = 0; i < mSpanCount; i++) { 806 into[i] = mSpans[i].findFirstVisibleItemPosition(); 807 } 808 return into; 809 } 810 811 /** 812 * Returns the adapter position of the first completely visible view for each span. 813 * <p> 814 * Note that, this value is not affected by layout orientation or item order traversal. 815 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 816 * not in the layout. 817 * <p> 818 * If RecyclerView has item decorators, they will be considered in calculations as well. 819 * <p> 820 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 821 * views are ignored in this method. 822 * 823 * @param into An array to put the results into. If you don't provide any, LayoutManager will 824 * create a new one. 825 * @return The adapter position of the first fully visible item in each span. If a span does 826 * not have any items, {@link RecyclerView#NO_POSITION} is returned for that span. 827 * @see #findFirstVisibleItemPositions(int[]) 828 * @see #findLastCompletelyVisibleItemPositions(int[]) 829 */ 830 public int[] findFirstCompletelyVisibleItemPositions(int[] into) { 831 if (into == null) { 832 into = new int[mSpanCount]; 833 } else if (into.length < mSpanCount) { 834 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 835 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 836 } 837 for (int i = 0; i < mSpanCount; i++) { 838 into[i] = mSpans[i].findFirstCompletelyVisibleItemPosition(); 839 } 840 return into; 841 } 842 843 /** 844 * Returns the adapter position of the last visible view for each span. 845 * <p> 846 * Note that, this value is not affected by layout orientation or item order traversal. 847 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 848 * not in the layout. 849 * <p> 850 * If RecyclerView has item decorators, they will be considered in calculations as well. 851 * <p> 852 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 853 * views are ignored in this method. 854 * 855 * @param into An array to put the results into. If you don't provide any, LayoutManager will 856 * create a new one. 857 * @return The adapter position of the last visible item in each span. If a span does not have 858 * any items, {@link RecyclerView#NO_POSITION} is returned for that span. 859 * @see #findLastCompletelyVisibleItemPositions(int[]) 860 * @see #findFirstVisibleItemPositions(int[]) 861 */ 862 public int[] findLastVisibleItemPositions(int[] into) { 863 if (into == null) { 864 into = new int[mSpanCount]; 865 } else if (into.length < mSpanCount) { 866 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 867 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 868 } 869 for (int i = 0; i < mSpanCount; i++) { 870 into[i] = mSpans[i].findLastVisibleItemPosition(); 871 } 872 return into; 873 } 874 875 /** 876 * Returns the adapter position of the last completely visible view for each span. 877 * <p> 878 * Note that, this value is not affected by layout orientation or item order traversal. 879 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 880 * not in the layout. 881 * <p> 882 * If RecyclerView has item decorators, they will be considered in calculations as well. 883 * <p> 884 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 885 * views are ignored in this method. 886 * 887 * @param into An array to put the results into. If you don't provide any, LayoutManager will 888 * create a new one. 889 * @return The adapter position of the last fully visible item in each span. If a span does not 890 * have any items, {@link RecyclerView#NO_POSITION} is returned for that span. 891 * @see #findFirstCompletelyVisibleItemPositions(int[]) 892 * @see #findLastVisibleItemPositions(int[]) 893 */ 894 public int[] findLastCompletelyVisibleItemPositions(int[] into) { 895 if (into == null) { 896 into = new int[mSpanCount]; 897 } else if (into.length < mSpanCount) { 898 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 899 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 900 } 901 for (int i = 0; i < mSpanCount; i++) { 902 into[i] = mSpans[i].findLastCompletelyVisibleItemPosition(); 903 } 904 return into; 905 } 906 907 @Override 908 public int computeHorizontalScrollOffset(RecyclerView.State state) { 909 return computeScrollOffset(state); 910 } 911 912 private int computeScrollOffset(RecyclerView.State state) { 913 if (getChildCount() == 0) { 914 return 0; 915 } 916 return ScrollbarHelper.computeScrollOffset(state, mPrimaryOrientation, 917 findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled) 918 , findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled), 919 this, mSmoothScrollbarEnabled, mShouldReverseLayout); 920 } 921 922 @Override 923 public int computeVerticalScrollOffset(RecyclerView.State state) { 924 return computeScrollOffset(state); 925 } 926 927 @Override 928 public int computeHorizontalScrollExtent(RecyclerView.State state) { 929 return computeScrollExtent(state); 930 } 931 932 private int computeScrollExtent(RecyclerView.State state) { 933 if (getChildCount() == 0) { 934 return 0; 935 } 936 return ScrollbarHelper.computeScrollExtent(state, mPrimaryOrientation, 937 findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled) 938 , findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled), 939 this, mSmoothScrollbarEnabled); 940 } 941 942 @Override 943 public int computeVerticalScrollExtent(RecyclerView.State state) { 944 return computeScrollExtent(state); 945 } 946 947 @Override 948 public int computeHorizontalScrollRange(RecyclerView.State state) { 949 return computeScrollRange(state); 950 } 951 952 private int computeScrollRange(RecyclerView.State state) { 953 if (getChildCount() == 0) { 954 return 0; 955 } 956 return ScrollbarHelper.computeScrollRange(state, mPrimaryOrientation, 957 findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled) 958 , findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled), 959 this, mSmoothScrollbarEnabled); 960 } 961 962 @Override 963 public int computeVerticalScrollRange(RecyclerView.State state) { 964 return computeScrollRange(state); 965 } 966 967 private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp) { 968 if (lp.mFullSpan) { 969 if (mOrientation == VERTICAL) { 970 measureChildWithDecorationsAndMargin(child, mFullSizeSpec, mHeightSpec); 971 } else { 972 measureChildWithDecorationsAndMargin(child, mWidthSpec, mFullSizeSpec); 973 } 974 } else { 975 measureChildWithDecorationsAndMargin(child, mWidthSpec, mHeightSpec); 976 } 977 } 978 979 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, 980 int heightSpec) { 981 final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); 982 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 983 widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + insets.left, 984 lp.rightMargin + insets.right); 985 heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + insets.top, 986 lp.bottomMargin + insets.bottom); 987 child.measure(widthSpec, heightSpec); 988 } 989 990 private int updateSpecWithExtra(int spec, int startInset, int endInset) { 991 if (startInset == 0 && endInset == 0) { 992 return spec; 993 } 994 final int mode = View.MeasureSpec.getMode(spec); 995 if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { 996 return View.MeasureSpec.makeMeasureSpec( 997 View.MeasureSpec.getSize(spec) - startInset - endInset, mode); 998 } 999 return spec; 1000 } 1001 1002 @Override 1003 public void onRestoreInstanceState(Parcelable state) { 1004 if (state instanceof SavedState) { 1005 mPendingSavedState = (SavedState) state; 1006 requestLayout(); 1007 } else if (DEBUG) { 1008 Log.d(TAG, "invalid saved state class"); 1009 } 1010 } 1011 1012 @Override 1013 public Parcelable onSaveInstanceState() { 1014 if (mPendingSavedState != null) { 1015 return new SavedState(mPendingSavedState); 1016 } 1017 SavedState state = new SavedState(); 1018 state.mReverseLayout = mReverseLayout; 1019 state.mAnchorLayoutFromEnd = mLastLayoutFromEnd; 1020 state.mLastLayoutRTL = mLastLayoutRTL; 1021 1022 if (mLazySpanLookup != null && mLazySpanLookup.mData != null) { 1023 state.mSpanLookup = mLazySpanLookup.mData; 1024 state.mSpanLookupSize = state.mSpanLookup.length; 1025 state.mFullSpanItems = mLazySpanLookup.mFullSpanItems; 1026 } else { 1027 state.mSpanLookupSize = 0; 1028 } 1029 1030 if (getChildCount() > 0) { 1031 state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition() 1032 : getFirstChildPosition(); 1033 state.mVisibleAnchorPosition = findFirstVisibleItemPositionInt(); 1034 state.mSpanOffsetsSize = mSpanCount; 1035 state.mSpanOffsets = new int[mSpanCount]; 1036 for (int i = 0; i < mSpanCount; i++) { 1037 int line; 1038 if (mLastLayoutFromEnd) { 1039 line = mSpans[i].getEndLine(Span.INVALID_LINE); 1040 if (line != Span.INVALID_LINE) { 1041 line -= mPrimaryOrientation.getEndAfterPadding(); 1042 } 1043 } else { 1044 line = mSpans[i].getStartLine(Span.INVALID_LINE); 1045 if (line != Span.INVALID_LINE) { 1046 line -= mPrimaryOrientation.getStartAfterPadding(); 1047 } 1048 } 1049 state.mSpanOffsets[i] = line; 1050 } 1051 } else { 1052 state.mAnchorPosition = NO_POSITION; 1053 state.mVisibleAnchorPosition = NO_POSITION; 1054 state.mSpanOffsetsSize = 0; 1055 } 1056 if (DEBUG) { 1057 Log.d(TAG, "saved state:\n" + state); 1058 } 1059 return state; 1060 } 1061 1062 @Override 1063 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, 1064 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { 1065 ViewGroup.LayoutParams lp = host.getLayoutParams(); 1066 if (!(lp instanceof LayoutParams)) { 1067 super.onInitializeAccessibilityNodeInfoForItem(host, info); 1068 return; 1069 } 1070 LayoutParams sglp = (LayoutParams) lp; 1071 if (mOrientation == HORIZONTAL) { 1072 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 1073 sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1, 1074 -1, -1, 1075 sglp.mFullSpan, false)); 1076 } else { // VERTICAL 1077 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 1078 -1, -1, 1079 sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1, 1080 sglp.mFullSpan, false)); 1081 } 1082 } 1083 1084 @Override 1085 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1086 super.onInitializeAccessibilityEvent(event); 1087 if (getChildCount() > 0) { 1088 final AccessibilityRecordCompat record = AccessibilityEventCompat 1089 .asRecord(event); 1090 final View start = findFirstVisibleItemClosestToStart(false); 1091 final View end = findFirstVisibleItemClosestToEnd(false); 1092 if (start == null || end == null) { 1093 return; 1094 } 1095 final int startPos = getPosition(start); 1096 final int endPos = getPosition(end); 1097 if (startPos < endPos) { 1098 record.setFromIndex(startPos); 1099 record.setToIndex(endPos); 1100 } else { 1101 record.setFromIndex(endPos); 1102 record.setToIndex(startPos); 1103 } 1104 } 1105 } 1106 1107 /** 1108 * Finds the first fully visible child to be used as an anchor child if span count changes when 1109 * state is restored. 1110 */ 1111 int findFirstVisibleItemPositionInt() { 1112 final View first = mShouldReverseLayout ? findFirstVisibleItemClosestToEnd(true) : 1113 findFirstVisibleItemClosestToStart(true); 1114 return first == null ? NO_POSITION : getPosition(first); 1115 } 1116 1117 @Override 1118 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 1119 RecyclerView.State state) { 1120 if (mOrientation == HORIZONTAL) { 1121 return mSpanCount; 1122 } 1123 return super.getRowCountForAccessibility(recycler, state); 1124 } 1125 1126 @Override 1127 public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, 1128 RecyclerView.State state) { 1129 if (mOrientation == VERTICAL) { 1130 return mSpanCount; 1131 } 1132 return super.getColumnCountForAccessibility(recycler, state); 1133 } 1134 1135 View findFirstVisibleItemClosestToStart(boolean fullyVisible) { 1136 final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); 1137 final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); 1138 final int limit = getChildCount(); 1139 for (int i = 0; i < limit; i ++) { 1140 final View child = getChildAt(i); 1141 if ((!fullyVisible || mPrimaryOrientation.getDecoratedStart(child) >= boundsStart) 1142 && mPrimaryOrientation.getDecoratedEnd(child) <= boundsEnd) { 1143 return child; 1144 } 1145 } 1146 return null; 1147 } 1148 1149 View findFirstVisibleItemClosestToEnd(boolean fullyVisible) { 1150 final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); 1151 final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); 1152 for (int i = getChildCount() - 1; i >= 0; i --) { 1153 final View child = getChildAt(i); 1154 if (mPrimaryOrientation.getDecoratedStart(child) >= boundsStart && (!fullyVisible 1155 || mPrimaryOrientation.getDecoratedEnd(child) <= boundsEnd)) { 1156 return child; 1157 } 1158 } 1159 return null; 1160 } 1161 1162 private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state, 1163 boolean canOffsetChildren) { 1164 final int maxEndLine = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); 1165 int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine; 1166 int fixOffset; 1167 if (gap > 0) { 1168 fixOffset = -scrollBy(-gap, recycler, state); 1169 } else { 1170 return; // nothing to fix 1171 } 1172 gap -= fixOffset; 1173 if (canOffsetChildren && gap > 0) { 1174 mPrimaryOrientation.offsetChildren(gap); 1175 } 1176 } 1177 1178 private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state, 1179 boolean canOffsetChildren) { 1180 final int minStartLine = getMinStart(mPrimaryOrientation.getStartAfterPadding()); 1181 int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding(); 1182 int fixOffset; 1183 if (gap > 0) { 1184 fixOffset = scrollBy(gap, recycler, state); 1185 } else { 1186 return; // nothing to fix 1187 } 1188 gap -= fixOffset; 1189 if (canOffsetChildren && gap > 0) { 1190 mPrimaryOrientation.offsetChildren(-gap); 1191 } 1192 } 1193 1194 private void updateLayoutStateToFillStart(int anchorPosition, RecyclerView.State state) { 1195 mLayoutState.mAvailable = 0; 1196 mLayoutState.mCurrentPosition = anchorPosition; 1197 if (isSmoothScrolling()) { 1198 final int targetPos = state.getTargetScrollPosition(); 1199 if (mShouldReverseLayout == targetPos < anchorPosition) { 1200 mLayoutState.mExtra = 0; 1201 } else { 1202 mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); 1203 } 1204 } else { 1205 mLayoutState.mExtra = 0; 1206 } 1207 mLayoutState.mLayoutDirection = LAYOUT_START; 1208 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL 1209 : ITEM_DIRECTION_HEAD; 1210 } 1211 1212 private void updateLayoutStateToFillEnd(int anchorPosition, RecyclerView.State state) { 1213 mLayoutState.mAvailable = 0; 1214 mLayoutState.mCurrentPosition = anchorPosition; 1215 if (isSmoothScrolling()) { 1216 final int targetPos = state.getTargetScrollPosition(); 1217 if (mShouldReverseLayout == targetPos > anchorPosition) { 1218 mLayoutState.mExtra = 0; 1219 } else { 1220 mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); 1221 } 1222 } else { 1223 mLayoutState.mExtra = 0; 1224 } 1225 mLayoutState.mLayoutDirection = LAYOUT_END; 1226 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD 1227 : ITEM_DIRECTION_TAIL; 1228 } 1229 1230 @Override 1231 public void offsetChildrenHorizontal(int dx) { 1232 super.offsetChildrenHorizontal(dx); 1233 for (int i = 0; i < mSpanCount; i++) { 1234 mSpans[i].onOffset(dx); 1235 } 1236 } 1237 1238 @Override 1239 public void offsetChildrenVertical(int dy) { 1240 super.offsetChildrenVertical(dy); 1241 for (int i = 0; i < mSpanCount; i++) { 1242 mSpans[i].onOffset(dy); 1243 } 1244 } 1245 1246 @Override 1247 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 1248 handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.REMOVE); 1249 } 1250 1251 @Override 1252 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 1253 handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.ADD); 1254 } 1255 1256 @Override 1257 public void onItemsChanged(RecyclerView recyclerView) { 1258 mLazySpanLookup.clear(); 1259 requestLayout(); 1260 } 1261 1262 @Override 1263 public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { 1264 handleUpdate(from, to, AdapterHelper.UpdateOp.MOVE); 1265 } 1266 1267 @Override 1268 public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) { 1269 handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.UPDATE); 1270 } 1271 1272 /** 1273 * Checks whether it should invalidate span assignments in response to an adapter change. 1274 */ 1275 private void handleUpdate(int positionStart, int itemCountOrToPosition, int cmd) { 1276 int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition(); 1277 mLazySpanLookup.invalidateAfter(positionStart); 1278 switch (cmd) { 1279 case AdapterHelper.UpdateOp.ADD: 1280 mLazySpanLookup.offsetForAddition(positionStart, itemCountOrToPosition); 1281 break; 1282 case AdapterHelper.UpdateOp.REMOVE: 1283 mLazySpanLookup.offsetForRemoval(positionStart, itemCountOrToPosition); 1284 break; 1285 case AdapterHelper.UpdateOp.MOVE: 1286 // TODO optimize 1287 mLazySpanLookup.offsetForRemoval(positionStart, 1); 1288 mLazySpanLookup.offsetForAddition(itemCountOrToPosition, 1); 1289 break; 1290 } 1291 1292 if (positionStart + itemCountOrToPosition <= minPosition) { 1293 return; 1294 1295 } 1296 int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition(); 1297 if (positionStart <= maxPosition) { 1298 requestLayout(); 1299 } 1300 } 1301 1302 private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, 1303 RecyclerView.State state) { 1304 mRemainingSpans.set(0, mSpanCount, true); 1305 // The target position we are trying to reach. 1306 final int targetLine; 1307 /* 1308 * The line until which we can recycle, as long as we add views. 1309 * Keep in mind, it is still the line in layout direction which means; to calculate the 1310 * actual recycle line, we should subtract/add the size in orientation. 1311 */ 1312 final int recycleLine; 1313 // Line of the furthest row. 1314 if (layoutState.mLayoutDirection == LAYOUT_END) { 1315 // ignore padding for recycler 1316 recycleLine = mPrimaryOrientation.getEndAfterPadding() + mLayoutState.mAvailable; 1317 targetLine = recycleLine + mLayoutState.mExtra + mPrimaryOrientation.getEndPadding(); 1318 1319 } else { // LAYOUT_START 1320 // ignore padding for recycler 1321 recycleLine = mPrimaryOrientation.getStartAfterPadding() - mLayoutState.mAvailable; 1322 targetLine = recycleLine - mLayoutState.mExtra - 1323 mPrimaryOrientation.getStartAfterPadding(); 1324 } 1325 updateAllRemainingSpans(layoutState.mLayoutDirection, targetLine); 1326 1327 // the default coordinate to add new view. 1328 final int defaultNewViewLine = mShouldReverseLayout 1329 ? mPrimaryOrientation.getEndAfterPadding() 1330 : mPrimaryOrientation.getStartAfterPadding(); 1331 1332 while (layoutState.hasMore(state) && !mRemainingSpans.isEmpty()) { 1333 View view = layoutState.next(recycler); 1334 LayoutParams lp = ((LayoutParams) view.getLayoutParams()); 1335 if (layoutState.mLayoutDirection == LAYOUT_END) { 1336 addView(view); 1337 } else { 1338 addView(view, 0); 1339 } 1340 measureChildWithDecorationsAndMargin(view, lp); 1341 1342 final int position = lp.getViewPosition(); 1343 final int spanIndex = mLazySpanLookup.getSpan(position); 1344 Span currentSpan; 1345 boolean assignSpan = spanIndex == LayoutParams.INVALID_SPAN_ID; 1346 if (assignSpan) { 1347 currentSpan = lp.mFullSpan ? mSpans[0] : getNextSpan(layoutState); 1348 mLazySpanLookup.setSpan(position, currentSpan); 1349 if (DEBUG) { 1350 Log.d(TAG, "assigned " + currentSpan.mIndex + " for " + position); 1351 } 1352 } else { 1353 if (DEBUG) { 1354 Log.d(TAG, "using " + spanIndex + " for pos " + position); 1355 } 1356 currentSpan = mSpans[spanIndex]; 1357 } 1358 final int start; 1359 final int end; 1360 1361 if (layoutState.mLayoutDirection == LAYOUT_END) { 1362 start = lp.mFullSpan ? getMaxEnd(defaultNewViewLine) 1363 : currentSpan.getEndLine(defaultNewViewLine); 1364 end = start + mPrimaryOrientation.getDecoratedMeasurement(view); 1365 if (assignSpan && lp.mFullSpan) { 1366 LazySpanLookup.FullSpanItem fullSpanItem; 1367 fullSpanItem = createFullSpanItemFromEnd(start); 1368 fullSpanItem.mGapDir = LAYOUT_START; 1369 fullSpanItem.mPosition = position; 1370 mLazySpanLookup.addFullSpanItem(fullSpanItem); 1371 } 1372 } else { 1373 end = lp.mFullSpan ? getMinStart(defaultNewViewLine) 1374 : currentSpan.getStartLine(defaultNewViewLine); 1375 start = end - mPrimaryOrientation.getDecoratedMeasurement(view); 1376 if (assignSpan && lp.mFullSpan) { 1377 LazySpanLookup.FullSpanItem fullSpanItem; 1378 fullSpanItem = createFullSpanItemFromStart(end); 1379 fullSpanItem.mGapDir = LAYOUT_END; 1380 fullSpanItem.mPosition = position; 1381 mLazySpanLookup.addFullSpanItem(fullSpanItem); 1382 } 1383 } 1384 1385 // check if this item may create gaps in the future 1386 if (lp.mFullSpan && layoutState.mItemDirection == ITEM_DIRECTION_HEAD && assignSpan) { 1387 mLaidOutInvalidFullSpan = true; 1388 } 1389 1390 lp.mSpan = currentSpan; 1391 attachViewToSpans(view, lp, layoutState); 1392 final int otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding() 1393 : currentSpan.mIndex * mSizePerSpan + 1394 mSecondaryOrientation.getStartAfterPadding(); 1395 final int otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view); 1396 if (mOrientation == VERTICAL) { 1397 layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end); 1398 } else { 1399 layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd); 1400 } 1401 1402 if (lp.mFullSpan) { 1403 updateAllRemainingSpans(mLayoutState.mLayoutDirection, targetLine); 1404 } else { 1405 updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine); 1406 } 1407 recycle(recycler, mLayoutState, currentSpan, recycleLine); 1408 } 1409 if (DEBUG) { 1410 Log.d(TAG, "fill, " + getChildCount()); 1411 } 1412 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 1413 final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding()); 1414 return Math.max(0, mLayoutState.mAvailable + (recycleLine - minStart)); 1415 } else { 1416 final int max = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); 1417 return Math.max(0, mLayoutState.mAvailable + (max - recycleLine)); 1418 } 1419 } 1420 1421 private LazySpanLookup.FullSpanItem createFullSpanItemFromEnd(int newItemTop) { 1422 LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem(); 1423 fsi.mGapPerSpan = new int[mSpanCount]; 1424 for (int i = 0; i < mSpanCount; i++) { 1425 fsi.mGapPerSpan[i] = newItemTop - mSpans[i].getEndLine(newItemTop); 1426 } 1427 return fsi; 1428 } 1429 1430 private LazySpanLookup.FullSpanItem createFullSpanItemFromStart(int newItemBottom) { 1431 LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem(); 1432 fsi.mGapPerSpan = new int[mSpanCount]; 1433 for (int i = 0; i < mSpanCount; i++) { 1434 fsi.mGapPerSpan[i] = mSpans[i].getStartLine(newItemBottom) - newItemBottom; 1435 } 1436 return fsi; 1437 } 1438 1439 private void attachViewToSpans(View view, LayoutParams lp, LayoutState layoutState) { 1440 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { 1441 if (lp.mFullSpan) { 1442 appendViewToAllSpans(view); 1443 } else { 1444 lp.mSpan.appendToSpan(view); 1445 } 1446 } else { 1447 if (lp.mFullSpan) { 1448 prependViewToAllSpans(view); 1449 } else { 1450 lp.mSpan.prependToSpan(view); 1451 } 1452 } 1453 } 1454 1455 private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState, 1456 Span updatedSpan, int recycleLine) { 1457 if (layoutState.mLayoutDirection == LAYOUT_START) { 1458 // calculate recycle line 1459 int maxStart = getMaxStart(updatedSpan.getStartLine()); 1460 recycleFromEnd(recycler, Math.max(recycleLine, maxStart) + 1461 (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding())); 1462 } else { 1463 // calculate recycle line 1464 int minEnd = getMinEnd(updatedSpan.getEndLine()); 1465 recycleFromStart(recycler, Math.min(recycleLine, minEnd) - 1466 (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding())); 1467 } 1468 } 1469 1470 private void appendViewToAllSpans(View view) { 1471 // traverse in reverse so that we end up assigning full span items to 0 1472 for (int i = mSpanCount - 1; i >= 0; i--) { 1473 mSpans[i].appendToSpan(view); 1474 } 1475 } 1476 1477 private void prependViewToAllSpans(View view) { 1478 // traverse in reverse so that we end up assigning full span items to 0 1479 for (int i = mSpanCount - 1; i >= 0; i--) { 1480 mSpans[i].prependToSpan(view); 1481 } 1482 } 1483 1484 private void layoutDecoratedWithMargins(View child, int left, int top, int right, int bottom) { 1485 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1486 if (DEBUG) { 1487 Log.d(TAG, "layout decorated pos: " + lp.getViewPosition() + ", span:" 1488 + lp.getSpanIndex() + ", fullspan:" + lp.mFullSpan 1489 + ". l:" + left + ",t:" + top 1490 + ", r:" + right + ", b:" + bottom); 1491 } 1492 layoutDecorated(child, left + lp.leftMargin, top + lp.topMargin, right - lp.rightMargin 1493 , bottom - lp.bottomMargin); 1494 } 1495 1496 private void updateAllRemainingSpans(int layoutDir, int targetLine) { 1497 for (int i = 0; i < mSpanCount; i++) { 1498 if (mSpans[i].mViews.isEmpty()) { 1499 continue; 1500 } 1501 updateRemainingSpans(mSpans[i], layoutDir, targetLine); 1502 } 1503 } 1504 1505 private void updateRemainingSpans(Span span, int layoutDir, int targetLine) { 1506 final int deletedSize = span.getDeletedSize(); 1507 if (layoutDir == LAYOUT_START) { 1508 final int line = span.getStartLine(); 1509 if (line + deletedSize < targetLine) { 1510 mRemainingSpans.set(span.mIndex, false); 1511 } 1512 } else { 1513 final int line = span.getEndLine(); 1514 if (line - deletedSize > targetLine) { 1515 mRemainingSpans.set(span.mIndex, false); 1516 } 1517 } 1518 } 1519 1520 private int getMaxStart(int def) { 1521 int maxStart = mSpans[0].getStartLine(def); 1522 for (int i = 1; i < mSpanCount; i++) { 1523 final int spanStart = mSpans[i].getStartLine(def); 1524 if (spanStart > maxStart) { 1525 maxStart = spanStart; 1526 } 1527 } 1528 return maxStart; 1529 } 1530 1531 private int getMinStart(int def) { 1532 int minStart = mSpans[0].getStartLine(def); 1533 for (int i = 1; i < mSpanCount; i++) { 1534 final int spanStart = mSpans[i].getStartLine(def); 1535 if (spanStart < minStart) { 1536 minStart = spanStart; 1537 } 1538 } 1539 return minStart; 1540 } 1541 1542 private int getMaxEnd(int def) { 1543 int maxEnd = mSpans[0].getEndLine(def); 1544 for (int i = 1; i < mSpanCount; i++) { 1545 final int spanEnd = mSpans[i].getEndLine(def); 1546 if (spanEnd > maxEnd) { 1547 maxEnd = spanEnd; 1548 } 1549 } 1550 return maxEnd; 1551 } 1552 1553 private int getMinEnd(int def) { 1554 int minEnd = mSpans[0].getEndLine(def); 1555 for (int i = 1; i < mSpanCount; i++) { 1556 final int spanEnd = mSpans[i].getEndLine(def); 1557 if (spanEnd < minEnd) { 1558 minEnd = spanEnd; 1559 } 1560 } 1561 return minEnd; 1562 } 1563 1564 private void recycleFromStart(RecyclerView.Recycler recycler, int line) { 1565 if (DEBUG) { 1566 Log.d(TAG, "recycling from start for line " + line); 1567 } 1568 while (getChildCount() > 0) { 1569 View child = getChildAt(0); 1570 if (mPrimaryOrientation.getDecoratedEnd(child) < line) { 1571 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1572 if (lp.mFullSpan) { 1573 for (int j = 0; j < mSpanCount; j++) { 1574 mSpans[j].popStart(); 1575 } 1576 } else { 1577 lp.mSpan.popStart(); 1578 } 1579 removeAndRecycleView(child, recycler); 1580 } else { 1581 return;// done 1582 } 1583 } 1584 } 1585 1586 private void recycleFromEnd(RecyclerView.Recycler recycler, int line) { 1587 final int childCount = getChildCount(); 1588 int i; 1589 for (i = childCount - 1; i >= 0; i--) { 1590 View child = getChildAt(i); 1591 if (mPrimaryOrientation.getDecoratedStart(child) > line) { 1592 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1593 if (lp.mFullSpan) { 1594 for (int j = 0; j < mSpanCount; j++) { 1595 mSpans[j].popEnd(); 1596 } 1597 } else { 1598 lp.mSpan.popEnd(); 1599 } 1600 removeAndRecycleView(child, recycler); 1601 } else { 1602 return;// done 1603 } 1604 } 1605 } 1606 1607 /** 1608 * @return True if last span is the first one we want to fill 1609 */ 1610 private boolean preferLastSpan(int layoutDir) { 1611 if (mOrientation == HORIZONTAL) { 1612 return (layoutDir == LAYOUT_START) != mShouldReverseLayout; 1613 } 1614 return ((layoutDir == LAYOUT_START) == mShouldReverseLayout) == isLayoutRTL(); 1615 } 1616 1617 /** 1618 * Finds the span for the next view. 1619 */ 1620 private Span getNextSpan(LayoutState layoutState) { 1621 final boolean preferLastSpan = preferLastSpan(layoutState.mLayoutDirection); 1622 final int startIndex, endIndex, diff; 1623 if (preferLastSpan) { 1624 startIndex = mSpanCount - 1; 1625 endIndex = -1; 1626 diff = -1; 1627 } else { 1628 startIndex = 0; 1629 endIndex = mSpanCount; 1630 diff = 1; 1631 } 1632 if (layoutState.mLayoutDirection == LAYOUT_END) { 1633 Span min = null; 1634 int minLine = Integer.MAX_VALUE; 1635 final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); 1636 for (int i = startIndex; i != endIndex; i += diff) { 1637 final Span other = mSpans[i]; 1638 int otherLine = other.getEndLine(defaultLine); 1639 if (otherLine < minLine) { 1640 min = other; 1641 minLine = otherLine; 1642 } 1643 } 1644 return min; 1645 } else { 1646 Span max = null; 1647 int maxLine = Integer.MIN_VALUE; 1648 final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); 1649 for (int i = startIndex; i != endIndex; i += diff) { 1650 final Span other = mSpans[i]; 1651 int otherLine = other.getStartLine(defaultLine); 1652 if (otherLine > maxLine) { 1653 max = other; 1654 maxLine = otherLine; 1655 } 1656 } 1657 return max; 1658 } 1659 } 1660 1661 @Override 1662 public boolean canScrollVertically() { 1663 return mOrientation == VERTICAL; 1664 } 1665 1666 @Override 1667 public boolean canScrollHorizontally() { 1668 return mOrientation == HORIZONTAL; 1669 } 1670 1671 @Override 1672 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 1673 RecyclerView.State state) { 1674 return scrollBy(dx, recycler, state); 1675 } 1676 1677 @Override 1678 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 1679 RecyclerView.State state) { 1680 return scrollBy(dy, recycler, state); 1681 } 1682 1683 private int calculateScrollDirectionForPosition(int position) { 1684 if (getChildCount() == 0) { 1685 return mShouldReverseLayout ? LAYOUT_END : LAYOUT_START; 1686 } 1687 final int firstChildPos = getFirstChildPosition(); 1688 return position < firstChildPos != mShouldReverseLayout ? LAYOUT_START : LAYOUT_END; 1689 } 1690 1691 @Override 1692 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 1693 int position) { 1694 LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { 1695 @Override 1696 public PointF computeScrollVectorForPosition(int targetPosition) { 1697 final int direction = calculateScrollDirectionForPosition(targetPosition); 1698 if (direction == 0) { 1699 return null; 1700 } 1701 if (mOrientation == HORIZONTAL) { 1702 return new PointF(direction, 0); 1703 } else { 1704 return new PointF(0, direction); 1705 } 1706 } 1707 }; 1708 scroller.setTargetPosition(position); 1709 startSmoothScroll(scroller); 1710 } 1711 1712 @Override 1713 public void scrollToPosition(int position) { 1714 if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) { 1715 mPendingSavedState.invalidateAnchorPositionInfo(); 1716 } 1717 mPendingScrollPosition = position; 1718 mPendingScrollPositionOffset = INVALID_OFFSET; 1719 requestLayout(); 1720 } 1721 1722 /** 1723 * Scroll to the specified adapter position with the given offset from layout start. 1724 * <p> 1725 * Note that scroll position change will not be reflected until the next layout call. 1726 * <p> 1727 * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. 1728 * 1729 * @param position Index (starting at 0) of the reference item. 1730 * @param offset The distance (in pixels) between the start edge of the item view and 1731 * start edge of the RecyclerView. 1732 * @see #setReverseLayout(boolean) 1733 * @see #scrollToPosition(int) 1734 */ 1735 public void scrollToPositionWithOffset(int position, int offset) { 1736 if (mPendingSavedState != null) { 1737 mPendingSavedState.invalidateAnchorPositionInfo(); 1738 } 1739 mPendingScrollPosition = position; 1740 mPendingScrollPositionOffset = offset; 1741 requestLayout(); 1742 } 1743 1744 int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { 1745 ensureOrientationHelper(); 1746 final int referenceChildPosition; 1747 if (dt > 0) { // layout towards end 1748 mLayoutState.mLayoutDirection = LAYOUT_END; 1749 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD 1750 : ITEM_DIRECTION_TAIL; 1751 referenceChildPosition = getLastChildPosition(); 1752 } else { 1753 mLayoutState.mLayoutDirection = LAYOUT_START; 1754 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL 1755 : ITEM_DIRECTION_HEAD; 1756 referenceChildPosition = getFirstChildPosition(); 1757 } 1758 mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; 1759 final int absDt = Math.abs(dt); 1760 mLayoutState.mAvailable = absDt; 1761 mLayoutState.mExtra = isSmoothScrolling() ? mPrimaryOrientation.getTotalSpace() : 0; 1762 int consumed = fill(recycler, mLayoutState, state); 1763 final int totalScroll; 1764 if (absDt < consumed) { 1765 totalScroll = dt; 1766 } else if (dt < 0) { 1767 totalScroll = -consumed; 1768 } else { // dt > 0 1769 totalScroll = consumed; 1770 } 1771 if (DEBUG) { 1772 Log.d(TAG, "asked " + dt + " scrolled" + totalScroll); 1773 } 1774 1775 mPrimaryOrientation.offsetChildren(-totalScroll); 1776 // always reset this if we scroll for a proper save instance state 1777 mLastLayoutFromEnd = mShouldReverseLayout; 1778 return totalScroll; 1779 } 1780 1781 private int getLastChildPosition() { 1782 final int childCount = getChildCount(); 1783 return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1)); 1784 } 1785 1786 private int getFirstChildPosition() { 1787 final int childCount = getChildCount(); 1788 return childCount == 0 ? 0 : getPosition(getChildAt(0)); 1789 } 1790 1791 /** 1792 * Finds the first View that can be used as an anchor View. 1793 * 1794 * @return Position of the View or 0 if it cannot find any such View. 1795 */ 1796 private int findFirstReferenceChildPosition(int itemCount) { 1797 final int limit = getChildCount(); 1798 for (int i = 0; i < limit; i++) { 1799 final View view = getChildAt(i); 1800 final int position = getPosition(view); 1801 if (position >= 0 && position < itemCount) { 1802 return position; 1803 } 1804 } 1805 return 0; 1806 } 1807 1808 /** 1809 * Finds the last View that can be used as an anchor View. 1810 * 1811 * @return Position of the View or 0 if it cannot find any such View. 1812 */ 1813 private int findLastReferenceChildPosition(int itemCount) { 1814 for (int i = getChildCount() - 1; i >= 0; i--) { 1815 final View view = getChildAt(i); 1816 final int position = getPosition(view); 1817 if (position >= 0 && position < itemCount) { 1818 return position; 1819 } 1820 } 1821 return 0; 1822 } 1823 1824 @Override 1825 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 1826 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 1827 ViewGroup.LayoutParams.WRAP_CONTENT); 1828 } 1829 1830 @Override 1831 public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 1832 return new LayoutParams(c, attrs); 1833 } 1834 1835 @Override 1836 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 1837 if (lp instanceof ViewGroup.MarginLayoutParams) { 1838 return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 1839 } else { 1840 return new LayoutParams(lp); 1841 } 1842 } 1843 1844 @Override 1845 public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 1846 return lp instanceof LayoutParams; 1847 } 1848 1849 public int getOrientation() { 1850 return mOrientation; 1851 } 1852 1853 1854 /** 1855 * LayoutParams used by StaggeredGridLayoutManager. 1856 */ 1857 public static class LayoutParams extends RecyclerView.LayoutParams { 1858 1859 /** 1860 * Span Id for Views that are not laid out yet. 1861 */ 1862 public static final int INVALID_SPAN_ID = -1; 1863 1864 // Package scope to be able to access from tests. 1865 Span mSpan; 1866 1867 boolean mFullSpan; 1868 1869 public LayoutParams(Context c, AttributeSet attrs) { 1870 super(c, attrs); 1871 } 1872 1873 public LayoutParams(int width, int height) { 1874 super(width, height); 1875 } 1876 1877 public LayoutParams(ViewGroup.MarginLayoutParams source) { 1878 super(source); 1879 } 1880 1881 public LayoutParams(ViewGroup.LayoutParams source) { 1882 super(source); 1883 } 1884 1885 public LayoutParams(RecyclerView.LayoutParams source) { 1886 super(source); 1887 } 1888 1889 /** 1890 * When set to true, the item will layout using all span area. That means, if orientation 1891 * is vertical, the view will have full width; if orientation is horizontal, the view will 1892 * have full height. 1893 * 1894 * @param fullSpan True if this item should traverse all spans. 1895 */ 1896 public void setFullSpan(boolean fullSpan) { 1897 mFullSpan = fullSpan; 1898 } 1899 1900 /** 1901 * Returns the Span index to which this View is assigned. 1902 * 1903 * @return The Span index of the View. If View is not yet assigned to any span, returns 1904 * {@link #INVALID_SPAN_ID}. 1905 */ 1906 public final int getSpanIndex() { 1907 if (mSpan == null) { 1908 return INVALID_SPAN_ID; 1909 } 1910 return mSpan.mIndex; 1911 } 1912 } 1913 1914 // Package scoped to access from tests. 1915 class Span { 1916 1917 static final int INVALID_LINE = Integer.MIN_VALUE; 1918 private ArrayList<View> mViews = new ArrayList<View>(); 1919 int mCachedStart = INVALID_LINE; 1920 int mCachedEnd = INVALID_LINE; 1921 int mDeletedSize = 0; 1922 final int mIndex; 1923 1924 private Span(int index) { 1925 mIndex = index; 1926 } 1927 1928 int getStartLine(int def) { 1929 if (mCachedStart != INVALID_LINE) { 1930 return mCachedStart; 1931 } 1932 if (mViews.size() == 0) { 1933 return def; 1934 } 1935 calculateCachedStart(); 1936 return mCachedStart; 1937 } 1938 1939 void calculateCachedStart() { 1940 final View startView = mViews.get(0); 1941 final LayoutParams lp = getLayoutParams(startView); 1942 mCachedStart = mPrimaryOrientation.getDecoratedStart(startView); 1943 if (lp.mFullSpan) { 1944 LazySpanLookup.FullSpanItem fsi = mLazySpanLookup 1945 .getFullSpanItem(lp.getViewPosition()); 1946 if (fsi != null && fsi.mGapDir == LAYOUT_START) { 1947 mCachedStart -= fsi.getGapForSpan(mIndex); 1948 } 1949 } 1950 } 1951 1952 // Use this one when default value does not make sense and not having a value means a bug. 1953 int getStartLine() { 1954 if (mCachedStart != INVALID_LINE) { 1955 return mCachedStart; 1956 } 1957 calculateCachedStart(); 1958 return mCachedStart; 1959 } 1960 1961 int getEndLine(int def) { 1962 if (mCachedEnd != INVALID_LINE) { 1963 return mCachedEnd; 1964 } 1965 final int size = mViews.size(); 1966 if (size == 0) { 1967 return def; 1968 } 1969 calculateCachedEnd(); 1970 return mCachedEnd; 1971 } 1972 1973 void calculateCachedEnd() { 1974 final View endView = mViews.get(mViews.size() - 1); 1975 final LayoutParams lp = getLayoutParams(endView); 1976 mCachedEnd = mPrimaryOrientation.getDecoratedEnd(endView); 1977 if (lp.mFullSpan) { 1978 LazySpanLookup.FullSpanItem fsi = mLazySpanLookup 1979 .getFullSpanItem(lp.getViewPosition()); 1980 if (fsi != null && fsi.mGapDir == LAYOUT_END) { 1981 mCachedEnd += fsi.getGapForSpan(mIndex); 1982 } 1983 } 1984 } 1985 1986 // Use this one when default value does not make sense and not having a value means a bug. 1987 int getEndLine() { 1988 if (mCachedEnd != INVALID_LINE) { 1989 return mCachedEnd; 1990 } 1991 calculateCachedEnd(); 1992 return mCachedEnd; 1993 } 1994 1995 void prependToSpan(View view) { 1996 LayoutParams lp = getLayoutParams(view); 1997 lp.mSpan = this; 1998 mViews.add(0, view); 1999 mCachedStart = INVALID_LINE; 2000 if (mViews.size() == 1) { 2001 mCachedEnd = INVALID_LINE; 2002 } 2003 if (lp.isItemRemoved() || lp.isItemChanged()) { 2004 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 2005 } 2006 } 2007 2008 void appendToSpan(View view) { 2009 LayoutParams lp = getLayoutParams(view); 2010 lp.mSpan = this; 2011 mViews.add(view); 2012 mCachedEnd = INVALID_LINE; 2013 if (mViews.size() == 1) { 2014 mCachedStart = INVALID_LINE; 2015 } 2016 if (lp.isItemRemoved() || lp.isItemChanged()) { 2017 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 2018 } 2019 } 2020 2021 // Useful method to preserve positions on a re-layout. 2022 void cacheReferenceLineAndClear(boolean reverseLayout, int offset) { 2023 int reference; 2024 if (reverseLayout) { 2025 reference = getEndLine(INVALID_LINE); 2026 } else { 2027 reference = getStartLine(INVALID_LINE); 2028 } 2029 clear(); 2030 if (reference == INVALID_LINE) { 2031 return; 2032 } 2033 if ((reverseLayout && reference < mPrimaryOrientation.getEndAfterPadding()) || 2034 (!reverseLayout && reference > mPrimaryOrientation.getStartAfterPadding()) ) { 2035 return; 2036 } 2037 if (offset != INVALID_OFFSET) { 2038 reference += offset; 2039 } 2040 mCachedStart = mCachedEnd = reference; 2041 } 2042 2043 void clear() { 2044 mViews.clear(); 2045 invalidateCache(); 2046 mDeletedSize = 0; 2047 } 2048 2049 void invalidateCache() { 2050 mCachedStart = INVALID_LINE; 2051 mCachedEnd = INVALID_LINE; 2052 } 2053 2054 void setLine(int line) { 2055 mCachedEnd = mCachedStart = line; 2056 } 2057 2058 void popEnd() { 2059 final int size = mViews.size(); 2060 View end = mViews.remove(size - 1); 2061 final LayoutParams lp = getLayoutParams(end); 2062 lp.mSpan = null; 2063 if (lp.isItemRemoved() || lp.isItemChanged()) { 2064 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end); 2065 } 2066 if (size == 1) { 2067 mCachedStart = INVALID_LINE; 2068 } 2069 mCachedEnd = INVALID_LINE; 2070 } 2071 2072 void popStart() { 2073 View start = mViews.remove(0); 2074 final LayoutParams lp = getLayoutParams(start); 2075 lp.mSpan = null; 2076 if (mViews.size() == 0) { 2077 mCachedEnd = INVALID_LINE; 2078 } 2079 if (lp.isItemRemoved() || lp.isItemChanged()) { 2080 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start); 2081 } 2082 mCachedStart = INVALID_LINE; 2083 } 2084 2085 public int getDeletedSize() { 2086 return mDeletedSize; 2087 } 2088 2089 LayoutParams getLayoutParams(View view) { 2090 return (LayoutParams) view.getLayoutParams(); 2091 } 2092 2093 void onOffset(int dt) { 2094 if (mCachedStart != INVALID_LINE) { 2095 mCachedStart += dt; 2096 } 2097 if (mCachedEnd != INVALID_LINE) { 2098 mCachedEnd += dt; 2099 } 2100 } 2101 2102 // normalized offset is how much this span can scroll 2103 int getNormalizedOffset(int dt, int targetStart, int targetEnd) { 2104 if (mViews.size() == 0) { 2105 return 0; 2106 } 2107 if (dt < 0) { 2108 final int endSpace = getEndLine() - targetEnd; 2109 if (endSpace <= 0) { 2110 return 0; 2111 } 2112 return -dt > endSpace ? -endSpace : dt; 2113 } else { 2114 final int startSpace = targetStart - getStartLine(); 2115 if (startSpace <= 0) { 2116 return 0; 2117 } 2118 return startSpace < dt ? startSpace : dt; 2119 } 2120 } 2121 2122 /** 2123 * Returns if there is no child between start-end lines 2124 * 2125 * @param start The start line 2126 * @param end The end line 2127 * @return true if a new child can be added between start and end 2128 */ 2129 boolean isEmpty(int start, int end) { 2130 final int count = mViews.size(); 2131 for (int i = 0; i < count; i++) { 2132 final View view = mViews.get(i); 2133 if (mPrimaryOrientation.getDecoratedStart(view) < end && 2134 mPrimaryOrientation.getDecoratedEnd(view) > start) { 2135 return false; 2136 } 2137 } 2138 return true; 2139 } 2140 2141 public int findFirstVisibleItemPosition() { 2142 return mReverseLayout 2143 ? findOneVisibleChild(mViews.size() -1, -1, false) 2144 : findOneVisibleChild(0, mViews.size(), false); 2145 } 2146 2147 public int findFirstCompletelyVisibleItemPosition() { 2148 return mReverseLayout 2149 ? findOneVisibleChild(mViews.size() -1, -1, true) 2150 : findOneVisibleChild(0, mViews.size(), true); 2151 } 2152 2153 public int findLastVisibleItemPosition() { 2154 return mReverseLayout 2155 ? findOneVisibleChild(0, mViews.size(), false) 2156 : findOneVisibleChild(mViews.size() -1, -1, false); 2157 } 2158 2159 public int findLastCompletelyVisibleItemPosition() { 2160 return mReverseLayout 2161 ? findOneVisibleChild(0, mViews.size(), true) 2162 : findOneVisibleChild(mViews.size() -1, -1, true); 2163 } 2164 2165 int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { 2166 final int start = mPrimaryOrientation.getStartAfterPadding(); 2167 final int end = mPrimaryOrientation.getEndAfterPadding(); 2168 final int next = toIndex > fromIndex ? 1 : -1; 2169 for (int i = fromIndex; i != toIndex; i += next) { 2170 final View child = mViews.get(i); 2171 final int childStart = mPrimaryOrientation.getDecoratedStart(child); 2172 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); 2173 if (childStart < end && childEnd > start) { 2174 if (completelyVisible) { 2175 if (childStart >= start && childEnd <= end) { 2176 return getPosition(child); 2177 } 2178 } else { 2179 return getPosition(child); 2180 } 2181 } 2182 } 2183 return NO_POSITION; 2184 } 2185 } 2186 2187 /** 2188 * An array of mappings from adapter position to span. 2189 * This only grows when a write happens and it grows up to the size of the adapter. 2190 */ 2191 static class LazySpanLookup { 2192 2193 private static final int MIN_SIZE = 10; 2194 int[] mData; 2195 int mAdapterSize; // we don't want to grow beyond that, unless it grows 2196 List<FullSpanItem> mFullSpanItems; 2197 2198 2199 /** 2200 * Invalidates everything after this position, including full span information 2201 */ 2202 int forceInvalidateAfter(int position) { 2203 if (mFullSpanItems != null) { 2204 for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { 2205 FullSpanItem fsi = mFullSpanItems.get(i); 2206 if (fsi.mPosition >= position) { 2207 mFullSpanItems.remove(i); 2208 } 2209 } 2210 } 2211 return invalidateAfter(position); 2212 } 2213 2214 /** 2215 * returns end position for invalidation. 2216 */ 2217 int invalidateAfter(int position) { 2218 if (mData == null) { 2219 return RecyclerView.NO_POSITION; 2220 } 2221 if (position >= mData.length) { 2222 return RecyclerView.NO_POSITION; 2223 } 2224 int endPosition = invalidateFullSpansAfter(position); 2225 if (endPosition == RecyclerView.NO_POSITION) { 2226 Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID); 2227 return mData.length; 2228 } else { 2229 // just invalidate items in between 2230 Arrays.fill(mData, position, endPosition + 1, LayoutParams.INVALID_SPAN_ID); 2231 return endPosition + 1; 2232 } 2233 } 2234 2235 int getSpan(int position) { 2236 if (mData == null || position >= mData.length) { 2237 return LayoutParams.INVALID_SPAN_ID; 2238 } else { 2239 return mData[position]; 2240 } 2241 } 2242 2243 void setSpan(int position, Span span) { 2244 ensureSize(position); 2245 mData[position] = span.mIndex; 2246 } 2247 2248 int sizeForPosition(int position) { 2249 int len = mData.length; 2250 while (len <= position) { 2251 len *= 2; 2252 } 2253 if (len > mAdapterSize) { 2254 len = mAdapterSize; 2255 } 2256 return len; 2257 } 2258 2259 void ensureSize(int position) { 2260 if (mData == null) { 2261 mData = new int[Math.max(position, MIN_SIZE) + 1]; 2262 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 2263 } else if (position >= mData.length) { 2264 int[] old = mData; 2265 mData = new int[sizeForPosition(position)]; 2266 System.arraycopy(old, 0, mData, 0, old.length); 2267 Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID); 2268 } 2269 } 2270 2271 void clear() { 2272 if (mData != null) { 2273 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 2274 } 2275 mFullSpanItems = null; 2276 } 2277 2278 void offsetForRemoval(int positionStart, int itemCount) { 2279 if (mData == null || positionStart >= mData.length) { 2280 return; 2281 } 2282 ensureSize(positionStart + itemCount); 2283 System.arraycopy(mData, positionStart + itemCount, mData, positionStart, 2284 mData.length - positionStart - itemCount); 2285 Arrays.fill(mData, mData.length - itemCount, mData.length, 2286 LayoutParams.INVALID_SPAN_ID); 2287 offsetFullSpansForRemoval(positionStart, itemCount); 2288 } 2289 2290 private void offsetFullSpansForRemoval(int positionStart, int itemCount) { 2291 if (mFullSpanItems == null) { 2292 return; 2293 } 2294 final int end = positionStart + itemCount; 2295 for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { 2296 FullSpanItem fsi = mFullSpanItems.get(i); 2297 if (fsi.mPosition < positionStart) { 2298 continue; 2299 } 2300 if (fsi.mPosition < end) { 2301 mFullSpanItems.remove(i); 2302 } else { 2303 fsi.mPosition -= itemCount; 2304 } 2305 } 2306 } 2307 2308 void offsetForAddition(int positionStart, int itemCount) { 2309 if (mData == null || positionStart >= mData.length) { 2310 return; 2311 } 2312 ensureSize(positionStart + itemCount); 2313 System.arraycopy(mData, positionStart, mData, positionStart + itemCount, 2314 mData.length - positionStart - itemCount); 2315 Arrays.fill(mData, positionStart, positionStart + itemCount, 2316 LayoutParams.INVALID_SPAN_ID); 2317 offsetFullSpansForAddition(positionStart, itemCount); 2318 } 2319 2320 private void offsetFullSpansForAddition(int positionStart, int itemCount) { 2321 if (mFullSpanItems == null) { 2322 return; 2323 } 2324 for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { 2325 FullSpanItem fsi = mFullSpanItems.get(i); 2326 if (fsi.mPosition < positionStart) { 2327 continue; 2328 } 2329 fsi.mPosition += itemCount; 2330 } 2331 } 2332 2333 /** 2334 * Returns when invalidation should end. e.g. hitting a full span position. 2335 * Returned position SHOULD BE invalidated. 2336 */ 2337 private int invalidateFullSpansAfter(int position) { 2338 if (mFullSpanItems == null) { 2339 return RecyclerView.NO_POSITION; 2340 } 2341 final FullSpanItem item = getFullSpanItem(position); 2342 // if there is an fsi at this position, get rid of it. 2343 if (item != null) { 2344 mFullSpanItems.remove(item); 2345 } 2346 int nextFsiIndex = -1; 2347 final int count = mFullSpanItems.size(); 2348 for (int i = 0; i < count; i++) { 2349 FullSpanItem fsi = mFullSpanItems.get(i); 2350 if (fsi.mPosition >= position) { 2351 nextFsiIndex = i; 2352 break; 2353 } 2354 } 2355 if (nextFsiIndex != -1) { 2356 FullSpanItem fsi = mFullSpanItems.get(nextFsiIndex); 2357 mFullSpanItems.remove(nextFsiIndex); 2358 return fsi.mPosition; 2359 } 2360 return RecyclerView.NO_POSITION; 2361 } 2362 2363 public void addFullSpanItem(FullSpanItem fullSpanItem) { 2364 if (mFullSpanItems == null) { 2365 mFullSpanItems = new ArrayList<FullSpanItem>(); 2366 } 2367 final int size = mFullSpanItems.size(); 2368 for (int i = 0; i < size; i++) { 2369 FullSpanItem other = mFullSpanItems.get(i); 2370 if (other.mPosition == fullSpanItem.mPosition) { 2371 if (DEBUG) { 2372 throw new IllegalStateException("two fsis for same position"); 2373 } else { 2374 mFullSpanItems.remove(i); 2375 } 2376 } 2377 if (other.mPosition >= fullSpanItem.mPosition) { 2378 mFullSpanItems.add(i, fullSpanItem); 2379 return; 2380 } 2381 } 2382 // if it is not added to a position. 2383 mFullSpanItems.add(fullSpanItem); 2384 } 2385 2386 public FullSpanItem getFullSpanItem(int position) { 2387 if (mFullSpanItems == null) { 2388 return null; 2389 } 2390 for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { 2391 final FullSpanItem fsi = mFullSpanItems.get(i); 2392 if (fsi.mPosition == position) { 2393 return fsi; 2394 } 2395 } 2396 return null; 2397 } 2398 2399 /** 2400 * @param minPos inclusive 2401 * @param maxPos exclusive 2402 * @param gapDir if not 0, returns FSIs on in that direction 2403 */ 2404 public FullSpanItem getFirstFullSpanItemInRange(int minPos, int maxPos, int gapDir) { 2405 if (mFullSpanItems == null) { 2406 return null; 2407 } 2408 for (int i = 0; i < mFullSpanItems.size(); i++) { 2409 FullSpanItem fsi = mFullSpanItems.get(i); 2410 if (fsi.mPosition >= maxPos) { 2411 return null; 2412 } 2413 if (fsi.mPosition >= minPos && (gapDir == 0 || fsi.mGapDir == gapDir)) { 2414 return fsi; 2415 } 2416 } 2417 return null; 2418 } 2419 2420 /** 2421 * We keep information about full span items because they may create gaps in the UI. 2422 */ 2423 static class FullSpanItem implements Parcelable { 2424 2425 int mPosition; 2426 int mGapDir; 2427 int[] mGapPerSpan; 2428 2429 public FullSpanItem(Parcel in) { 2430 mPosition = in.readInt(); 2431 mGapDir = in.readInt(); 2432 int spanCount = in.readInt(); 2433 if (spanCount > 0) { 2434 mGapPerSpan = new int[spanCount]; 2435 in.readIntArray(mGapPerSpan); 2436 } 2437 } 2438 2439 public FullSpanItem() { 2440 } 2441 2442 int getGapForSpan(int spanIndex) { 2443 return mGapPerSpan == null ? 0 : mGapPerSpan[spanIndex]; 2444 } 2445 2446 public void invalidateSpanGaps() { 2447 mGapPerSpan = null; 2448 } 2449 2450 @Override 2451 public int describeContents() { 2452 return 0; 2453 } 2454 2455 @Override 2456 public void writeToParcel(Parcel dest, int flags) { 2457 dest.writeInt(mPosition); 2458 dest.writeInt(mGapDir); 2459 if (mGapPerSpan != null && mGapPerSpan.length > 0) { 2460 dest.writeInt(mGapPerSpan.length); 2461 dest.writeIntArray(mGapPerSpan); 2462 } else { 2463 dest.writeInt(0); 2464 } 2465 } 2466 2467 @Override 2468 public String toString() { 2469 return "FullSpanItem{" + 2470 "mPosition=" + mPosition + 2471 ", mGapDir=" + mGapDir + 2472 ", mGapPerSpan=" + Arrays.toString(mGapPerSpan) + 2473 '}'; 2474 } 2475 2476 public static final Parcelable.Creator<FullSpanItem> CREATOR 2477 = new Parcelable.Creator<FullSpanItem>() { 2478 @Override 2479 public FullSpanItem createFromParcel(Parcel in) { 2480 return new FullSpanItem(in); 2481 } 2482 2483 @Override 2484 public FullSpanItem[] newArray(int size) { 2485 return new FullSpanItem[size]; 2486 } 2487 }; 2488 } 2489 } 2490 2491 static class SavedState implements Parcelable { 2492 2493 int mAnchorPosition; 2494 int mVisibleAnchorPosition; // Replacement for span info when spans are invalidated 2495 int mSpanOffsetsSize; 2496 int[] mSpanOffsets; 2497 int mSpanLookupSize; 2498 int[] mSpanLookup; 2499 List<LazySpanLookup.FullSpanItem> mFullSpanItems; 2500 boolean mReverseLayout; 2501 boolean mAnchorLayoutFromEnd; 2502 boolean mLastLayoutRTL; 2503 2504 public SavedState() { 2505 } 2506 2507 SavedState(Parcel in) { 2508 mAnchorPosition = in.readInt(); 2509 mVisibleAnchorPosition = in.readInt(); 2510 mSpanOffsetsSize = in.readInt(); 2511 if (mSpanOffsetsSize > 0) { 2512 mSpanOffsets = new int[mSpanOffsetsSize]; 2513 in.readIntArray(mSpanOffsets); 2514 } 2515 2516 mSpanLookupSize = in.readInt(); 2517 if (mSpanLookupSize > 0) { 2518 mSpanLookup = new int[mSpanLookupSize]; 2519 in.readIntArray(mSpanLookup); 2520 } 2521 mReverseLayout = in.readInt() == 1; 2522 mAnchorLayoutFromEnd = in.readInt() == 1; 2523 mLastLayoutRTL = in.readInt() == 1; 2524 mFullSpanItems = in.readArrayList( 2525 LazySpanLookup.FullSpanItem.class.getClassLoader()); 2526 } 2527 2528 public SavedState(SavedState other) { 2529 mSpanOffsetsSize = other.mSpanOffsetsSize; 2530 mAnchorPosition = other.mAnchorPosition; 2531 mVisibleAnchorPosition = other.mVisibleAnchorPosition; 2532 mSpanOffsets = other.mSpanOffsets; 2533 mSpanLookupSize = other.mSpanLookupSize; 2534 mSpanLookup = other.mSpanLookup; 2535 mReverseLayout = other.mReverseLayout; 2536 mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; 2537 mLastLayoutRTL = other.mLastLayoutRTL; 2538 mFullSpanItems = other.mFullSpanItems; 2539 } 2540 2541 void invalidateSpanInfo() { 2542 mSpanOffsets = null; 2543 mSpanOffsetsSize = 0; 2544 mSpanLookupSize = 0; 2545 mSpanLookup = null; 2546 mFullSpanItems = null; 2547 } 2548 2549 void invalidateAnchorPositionInfo() { 2550 mSpanOffsets = null; 2551 mSpanOffsetsSize = 0; 2552 mAnchorPosition = NO_POSITION; 2553 mVisibleAnchorPosition = NO_POSITION; 2554 } 2555 2556 @Override 2557 public int describeContents() { 2558 return 0; 2559 } 2560 2561 @Override 2562 public void writeToParcel(Parcel dest, int flags) { 2563 dest.writeInt(mAnchorPosition); 2564 dest.writeInt(mVisibleAnchorPosition); 2565 dest.writeInt(mSpanOffsetsSize); 2566 if (mSpanOffsetsSize > 0) { 2567 dest.writeIntArray(mSpanOffsets); 2568 } 2569 dest.writeInt(mSpanLookupSize); 2570 if (mSpanLookupSize > 0) { 2571 dest.writeIntArray(mSpanLookup); 2572 } 2573 dest.writeInt(mReverseLayout ? 1 : 0); 2574 dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); 2575 dest.writeInt(mLastLayoutRTL ? 1 : 0); 2576 dest.writeList(mFullSpanItems); 2577 } 2578 2579 public static final Parcelable.Creator<SavedState> CREATOR 2580 = new Parcelable.Creator<SavedState>() { 2581 @Override 2582 public SavedState createFromParcel(Parcel in) { 2583 return new SavedState(in); 2584 } 2585 2586 @Override 2587 public SavedState[] newArray(int size) { 2588 return new SavedState[size]; 2589 } 2590 }; 2591 } 2592 2593 /** 2594 * Data class to hold the information about an anchor position which is used in onLayout call. 2595 */ 2596 private class AnchorInfo { 2597 2598 int mPosition; 2599 int mOffset; 2600 boolean mLayoutFromEnd; 2601 boolean mInvalidateOffsets; 2602 2603 void reset() { 2604 mPosition = NO_POSITION; 2605 mOffset = INVALID_OFFSET; 2606 mLayoutFromEnd = false; 2607 mInvalidateOffsets = false; 2608 } 2609 2610 void assignCoordinateFromPadding() { 2611 mOffset = mLayoutFromEnd ? mPrimaryOrientation.getEndAfterPadding() 2612 : mPrimaryOrientation.getStartAfterPadding(); 2613 } 2614 2615 void assignCoordinateFromPadding(int addedDistance) { 2616 if (mLayoutFromEnd) { 2617 mOffset = mPrimaryOrientation.getEndAfterPadding() - addedDistance; 2618 } else { 2619 mOffset = mPrimaryOrientation.getStartAfterPadding() + addedDistance; 2620 } 2621 } 2622 } 2623} 2624