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