1 /* 2 * Copyright 2018 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 androidx.recyclerview.widget; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY; 20 21 import android.annotation.SuppressLint; 22 import android.content.Context; 23 import android.graphics.PointF; 24 import android.graphics.Rect; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.accessibility.AccessibilityEvent; 32 33 import androidx.annotation.RestrictTo; 34 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 35 36 import org.jspecify.annotations.NonNull; 37 import org.jspecify.annotations.Nullable; 38 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.BitSet; 42 import java.util.List; 43 44 /** 45 * A LayoutManager that lays out children in a staggered grid formation. 46 * It supports horizontal & vertical layout as well as an ability to layout children in reverse. 47 * <p> 48 * Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps, 49 * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can 50 * control this behavior via {@link #setGapStrategy(int)}. 51 */ 52 public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements 53 RecyclerView.SmoothScroller.ScrollVectorProvider { 54 55 private static final String TAG = "StaggeredGridLManager"; 56 57 static final boolean DEBUG = false; 58 59 public static final int HORIZONTAL = RecyclerView.HORIZONTAL; 60 61 public static final int VERTICAL = RecyclerView.VERTICAL; 62 63 /** 64 * Does not do anything to hide gaps. 65 */ 66 public static final int GAP_HANDLING_NONE = 0; 67 68 /** 69 * @deprecated No longer supported. 70 */ 71 @SuppressWarnings("unused") 72 @Deprecated 73 public static final int GAP_HANDLING_LAZY = 1; 74 75 /** 76 * When scroll state is changed to {@link RecyclerView#SCROLL_STATE_IDLE}, StaggeredGrid will 77 * check if there are gaps in the because of full span items. If it finds, it will re-layout 78 * and move items to correct positions with animations. 79 * <p> 80 * For example, if LayoutManager ends up with the following layout due to adapter changes: 81 * <pre> 82 * AAA 83 * _BC 84 * DDD 85 * </pre> 86 * <p> 87 * It will animate to the following state: 88 * <pre> 89 * AAA 90 * BC_ 91 * DDD 92 * </pre> 93 */ 94 public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2; 95 96 static final int INVALID_OFFSET = Integer.MIN_VALUE; 97 /** 98 * While trying to find next view to focus, LayoutManager will not try to scroll more 99 * than this factor times the total space of the list. If layout is vertical, total space is the 100 * height minus padding, if layout is horizontal, total space is the width minus padding. 101 */ 102 private static final float MAX_SCROLL_FACTOR = 1 / 3f; 103 104 /** 105 * Number of spans 106 */ 107 private int mSpanCount = -1; 108 109 Span[] mSpans; 110 111 /** 112 * Primary orientation is the layout's orientation, secondary orientation is the orientation 113 * for spans. Having both makes code much cleaner for calculations. 114 */ 115 @NonNull OrientationHelper mPrimaryOrientation; 116 @NonNull OrientationHelper mSecondaryOrientation; 117 118 private int mOrientation; 119 120 /** 121 * The width or height per span, depending on the orientation. 122 */ 123 private int mSizePerSpan; 124 125 private final @NonNull LayoutState mLayoutState; 126 127 boolean mReverseLayout = false; 128 129 /** 130 * Aggregated reverse layout value that takes RTL into account. 131 */ 132 boolean mShouldReverseLayout = false; 133 134 /** 135 * Temporary variable used during fill method to check which spans needs to be filled. 136 */ 137 private BitSet mRemainingSpans; 138 139 /** 140 * When LayoutManager needs to scroll to a position, it sets this variable and requests a 141 * layout which will check this variable and re-layout accordingly. 142 */ 143 int mPendingScrollPosition = RecyclerView.NO_POSITION; 144 145 /** 146 * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is 147 * called. 148 */ 149 int mPendingScrollPositionOffset = INVALID_OFFSET; 150 151 /** 152 * Keeps the mapping between the adapter positions and spans. This is necessary to provide 153 * a consistent experience when user scrolls the list. 154 */ 155 LazySpanLookup mLazySpanLookup = new LazySpanLookup(); 156 157 /** 158 * how we handle gaps in UI. 159 */ 160 private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 161 162 /** 163 * Saved state needs this information to properly layout on restore. 164 */ 165 private boolean mLastLayoutFromEnd; 166 167 /** 168 * Saved state and onLayout needs this information to re-layout properly 169 */ 170 private boolean mLastLayoutRTL; 171 172 /** 173 * SavedState is not handled until a layout happens. This is where we keep it until next 174 * layout. 175 */ 176 private SavedState mPendingSavedState; 177 178 /** 179 * Re-used measurement specs. updated by onLayout. 180 */ 181 private int mFullSizeSpec; 182 183 /** 184 * Re-used rectangle to get child decor offsets. 185 */ 186 private final Rect mTmpRect = new Rect(); 187 188 /** 189 * Re-used anchor info. 190 */ 191 private final AnchorInfo mAnchorInfo = new AnchorInfo(); 192 193 /** 194 * If a full span item is invalid / or created in reverse direction; it may create gaps in 195 * the UI. While laying out, if such case is detected, we set this flag. 196 * <p> 197 * After scrolling stops, we check this flag and if it is set, re-layout. 198 */ 199 private boolean mLaidOutInvalidFullSpan = false; 200 201 /** 202 * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. 203 * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} 204 */ 205 private boolean mSmoothScrollbarEnabled = true; 206 207 /** 208 * Temporary array used (solely in {@link #collectAdjacentPrefetchPositions}) for stashing and 209 * sorting distances to views being prefetched. 210 */ 211 private int[] mPrefetchDistances; 212 213 private final Runnable mCheckForGapsRunnable = new Runnable() { 214 @Override 215 public void run() { 216 checkForGaps(); 217 } 218 }; 219 220 /** 221 * Constructor used when layout manager is set in XML by RecyclerView attribute 222 * "layoutManager". Defaults to single column and vertical. 223 */ 224 @SuppressWarnings("unused") StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)225 public StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, 226 int defStyleRes) { 227 Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); 228 setOrientation(properties.orientation); 229 setSpanCount(properties.spanCount); 230 setReverseLayout(properties.reverseLayout); 231 mLayoutState = new LayoutState(); 232 createOrientationHelpers(); 233 } 234 235 /** 236 * Creates a StaggeredGridLayoutManager with given parameters. 237 * 238 * @param spanCount If orientation is vertical, spanCount is number of columns. If 239 * orientation is horizontal, spanCount is number of rows. 240 * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL} 241 */ StaggeredGridLayoutManager(int spanCount, int orientation)242 public StaggeredGridLayoutManager(int spanCount, int orientation) { 243 mOrientation = orientation; 244 setSpanCount(spanCount); 245 mLayoutState = new LayoutState(); 246 createOrientationHelpers(); 247 } 248 249 @Override isAutoMeasureEnabled()250 public boolean isAutoMeasureEnabled() { 251 return mGapStrategy != GAP_HANDLING_NONE; 252 } 253 createOrientationHelpers()254 private void createOrientationHelpers() { 255 mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation); 256 mSecondaryOrientation = OrientationHelper 257 .createOrientationHelper(this, 1 - mOrientation); 258 } 259 260 /** 261 * Checks for gaps in the UI that may be caused by adapter changes. 262 * <p> 263 * When a full span item is laid out in reverse direction, it sets a flag which we check when 264 * scroll is stopped (or re-layout happens) and re-layout after first valid item. 265 */ checkForGaps()266 boolean checkForGaps() { 267 if (getChildCount() == 0 || mGapStrategy == GAP_HANDLING_NONE || !isAttachedToWindow()) { 268 return false; 269 } 270 final int minPos, maxPos; 271 if (mShouldReverseLayout) { 272 minPos = getLastChildPosition(); 273 maxPos = getFirstChildPosition(); 274 } else { 275 minPos = getFirstChildPosition(); 276 maxPos = getLastChildPosition(); 277 } 278 if (minPos == 0) { 279 View gapView = hasGapsToFix(); 280 if (gapView != null) { 281 mLazySpanLookup.clear(); 282 requestSimpleAnimationsInNextLayout(); 283 requestLayout(); 284 return true; 285 } 286 } 287 if (!mLaidOutInvalidFullSpan) { 288 return false; 289 } 290 int invalidGapDir = mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; 291 final LazySpanLookup.FullSpanItem invalidFsi = mLazySpanLookup 292 .getFirstFullSpanItemInRange(minPos, maxPos + 1, invalidGapDir, true); 293 if (invalidFsi == null) { 294 mLaidOutInvalidFullSpan = false; 295 mLazySpanLookup.forceInvalidateAfter(maxPos + 1); 296 return false; 297 } 298 final LazySpanLookup.FullSpanItem validFsi = mLazySpanLookup 299 .getFirstFullSpanItemInRange(minPos, invalidFsi.mPosition, 300 invalidGapDir * -1, true); 301 if (validFsi == null) { 302 mLazySpanLookup.forceInvalidateAfter(invalidFsi.mPosition); 303 } else { 304 mLazySpanLookup.forceInvalidateAfter(validFsi.mPosition + 1); 305 } 306 requestSimpleAnimationsInNextLayout(); 307 requestLayout(); 308 return true; 309 } 310 311 @Override onScrollStateChanged(int state)312 public void onScrollStateChanged(int state) { 313 if (state == RecyclerView.SCROLL_STATE_IDLE) { 314 checkForGaps(); 315 } 316 } 317 318 @Override onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler)319 public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 320 super.onDetachedFromWindow(view, recycler); 321 322 removeCallbacks(mCheckForGapsRunnable); 323 for (int i = 0; i < mSpanCount; i++) { 324 mSpans[i].clear(); 325 } 326 // SGLM will require fresh layout call to recover state after detach 327 view.requestLayout(); 328 } 329 330 /** 331 * Checks for gaps if we've reached to the top of the list. 332 * <p> 333 * Intermediate gaps created by full span items are tracked via mLaidOutInvalidFullSpan field. 334 */ hasGapsToFix()335 View hasGapsToFix() { 336 int startChildIndex = 0; 337 int endChildIndex = getChildCount() - 1; 338 BitSet mSpansToCheck = new BitSet(mSpanCount); 339 mSpansToCheck.set(0, mSpanCount, true); 340 341 final int firstChildIndex, childLimit; 342 final int preferredSpanDir = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1; 343 344 if (mShouldReverseLayout) { 345 firstChildIndex = endChildIndex; 346 childLimit = startChildIndex - 1; 347 } else { 348 firstChildIndex = startChildIndex; 349 childLimit = endChildIndex + 1; 350 } 351 final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1; 352 for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) { 353 View child = getChildAt(i); 354 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 355 if (mSpansToCheck.get(lp.mSpan.mIndex)) { 356 if (checkSpanForGap(lp.mSpan)) { 357 return child; 358 } 359 mSpansToCheck.clear(lp.mSpan.mIndex); 360 } 361 if (lp.mFullSpan) { 362 continue; // quick reject 363 } 364 365 if (i + nextChildDiff != childLimit) { 366 View nextChild = getChildAt(i + nextChildDiff); 367 boolean compareSpans = false; 368 if (mShouldReverseLayout) { 369 // ensure child's end is below nextChild's end 370 int myEnd = mPrimaryOrientation.getDecoratedEnd(child); 371 int nextEnd = mPrimaryOrientation.getDecoratedEnd(nextChild); 372 if (myEnd < nextEnd) { 373 return child; //i should have a better position 374 } else if (myEnd == nextEnd) { 375 compareSpans = true; 376 } 377 } else { 378 int myStart = mPrimaryOrientation.getDecoratedStart(child); 379 int nextStart = mPrimaryOrientation.getDecoratedStart(nextChild); 380 if (myStart > nextStart) { 381 return child; //i should have a better position 382 } else if (myStart == nextStart) { 383 compareSpans = true; 384 } 385 } 386 if (compareSpans) { 387 // equal, check span indices. 388 LayoutParams nextLp = (LayoutParams) nextChild.getLayoutParams(); 389 if (lp.mSpan.mIndex - nextLp.mSpan.mIndex < 0 != preferredSpanDir < 0) { 390 return child; 391 } 392 } 393 } 394 } 395 // everything looks good 396 return null; 397 } 398 399 private boolean checkSpanForGap(Span span) { 400 if (mShouldReverseLayout) { 401 if (span.getEndLine() < mPrimaryOrientation.getEndAfterPadding()) { 402 // if it is full span, it is OK 403 final View endView = span.mViews.get(span.mViews.size() - 1); 404 final LayoutParams lp = span.getLayoutParams(endView); 405 return !lp.mFullSpan; 406 } 407 } else if (span.getStartLine() > mPrimaryOrientation.getStartAfterPadding()) { 408 // if it is full span, it is OK 409 final View startView = span.mViews.get(0); 410 final LayoutParams lp = span.getLayoutParams(startView); 411 return !lp.mFullSpan; 412 } 413 return false; 414 } 415 416 /** 417 * Sets the number of spans for the layout. This will invalidate all of the span assignments 418 * for Views. 419 * <p> 420 * Calling this method will automatically result in a new layout request unless the spanCount 421 * parameter is equal to current span count. 422 * 423 * @param spanCount Number of spans to layout 424 */ 425 public void setSpanCount(int spanCount) { 426 assertNotInLayoutOrScroll(null); 427 if (spanCount != mSpanCount) { 428 invalidateSpanAssignments(); 429 mSpanCount = spanCount; 430 mRemainingSpans = new BitSet(mSpanCount); 431 mSpans = new Span[mSpanCount]; 432 for (int i = 0; i < mSpanCount; i++) { 433 mSpans[i] = new Span(i); 434 } 435 requestLayout(); 436 } 437 } 438 439 /** 440 * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep 441 * scroll position if this method is called after views are laid out. 442 * 443 * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} 444 */ 445 public void setOrientation(int orientation) { 446 if (orientation != HORIZONTAL && orientation != VERTICAL) { 447 throw new IllegalArgumentException("invalid orientation."); 448 } 449 assertNotInLayoutOrScroll(null); 450 if (orientation == mOrientation) { 451 return; 452 } 453 mOrientation = orientation; 454 OrientationHelper tmp = mPrimaryOrientation; 455 mPrimaryOrientation = mSecondaryOrientation; 456 mSecondaryOrientation = tmp; 457 requestLayout(); 458 } 459 460 /** 461 * Sets whether LayoutManager should start laying out items from the end of the UI. The order 462 * items are traversed is not affected by this call. 463 * <p> 464 * For vertical layout, if it is set to <code>true</code>, first item will be at the bottom of 465 * the list. 466 * <p> 467 * For horizontal layouts, it depends on the layout direction. 468 * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if 469 * {@link RecyclerView}} is RTL, it will layout from LTR. 470 * 471 * @param reverseLayout Whether layout should be in reverse or not 472 */ 473 public void setReverseLayout(boolean reverseLayout) { 474 assertNotInLayoutOrScroll(null); 475 if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) { 476 mPendingSavedState.mReverseLayout = reverseLayout; 477 } 478 mReverseLayout = reverseLayout; 479 requestLayout(); 480 } 481 482 /** 483 * Returns the current gap handling strategy for StaggeredGridLayoutManager. 484 * <p> 485 * Staggered grid may have gaps in the layout due to changes in the adapter. To avoid gaps, 486 * StaggeredGridLayoutManager provides 2 options. Check {@link #GAP_HANDLING_NONE} and 487 * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} for details. 488 * <p> 489 * By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}. 490 * 491 * @return Current gap handling strategy. 492 * @see #setGapStrategy(int) 493 * @see #GAP_HANDLING_NONE 494 * @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS 495 */ 496 public int getGapStrategy() { 497 return mGapStrategy; 498 } 499 500 /** 501 * Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter 502 * is different than the current strategy, calling this method will trigger a layout request. 503 * 504 * @param gapStrategy The new gap handling strategy. Should be 505 * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or {@link 506 * #GAP_HANDLING_NONE}. 507 * @see #getGapStrategy() 508 */ 509 public void setGapStrategy(int gapStrategy) { 510 assertNotInLayoutOrScroll(null); 511 if (gapStrategy == mGapStrategy) { 512 return; 513 } 514 if (gapStrategy != GAP_HANDLING_NONE 515 && gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) { 516 throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE " 517 + "or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS"); 518 } 519 mGapStrategy = gapStrategy; 520 requestLayout(); 521 } 522 523 @Override 524 public void assertNotInLayoutOrScroll(String message) { 525 if (mPendingSavedState == null) { 526 super.assertNotInLayoutOrScroll(message); 527 } 528 } 529 530 /** 531 * Returns the number of spans laid out by StaggeredGridLayoutManager. 532 * 533 * @return Number of spans in the layout 534 */ 535 public int getSpanCount() { 536 return mSpanCount; 537 } 538 539 /** 540 * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items. 541 * <p> 542 * If you need to cancel current assignments, you can call this method which will clear all 543 * assignments and request a new layout. 544 */ 545 public void invalidateSpanAssignments() { 546 mLazySpanLookup.clear(); 547 requestLayout(); 548 } 549 550 /** 551 * Calculates the views' layout order. (e.g. from end to start or start to end) 552 * RTL layout support is applied automatically. So if layout is RTL and 553 * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. 554 */ 555 private void resolveShouldLayoutReverse() { 556 // A == B is the same result, but we rather keep it readable 557 if (mOrientation == VERTICAL || !isLayoutRTL()) { 558 mShouldReverseLayout = mReverseLayout; 559 } else { 560 mShouldReverseLayout = !mReverseLayout; 561 } 562 } 563 564 boolean isLayoutRTL() { 565 return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 566 } 567 568 /** 569 * Returns whether views are laid out in reverse order or not. 570 * <p> 571 * Not that this value is not affected by RecyclerView's layout direction. 572 * 573 * @return True if layout is reversed, false otherwise 574 * @see #setReverseLayout(boolean) 575 */ 576 public boolean getReverseLayout() { 577 return mReverseLayout; 578 } 579 580 @Override 581 public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { 582 // we don't like it to wrap content in our non-scroll direction. 583 final int width, height; 584 final int horizontalPadding = getPaddingLeft() + getPaddingRight(); 585 final int verticalPadding = getPaddingTop() + getPaddingBottom(); 586 if (mOrientation == VERTICAL) { 587 final int usedHeight = childrenBounds.height() + verticalPadding; 588 height = chooseSize(hSpec, usedHeight, getMinimumHeight()); 589 width = chooseSize(wSpec, mSizePerSpan * mSpanCount + horizontalPadding, 590 getMinimumWidth()); 591 } else { 592 final int usedWidth = childrenBounds.width() + horizontalPadding; 593 width = chooseSize(wSpec, usedWidth, getMinimumWidth()); 594 height = chooseSize(hSpec, mSizePerSpan * mSpanCount + verticalPadding, 595 getMinimumHeight()); 596 } 597 setMeasuredDimension(width, height); 598 } 599 600 @Override 601 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 602 onLayoutChildren(recycler, state, true); 603 } 604 605 @Override 606 public void onAdapterChanged(RecyclerView.@Nullable Adapter oldAdapter, 607 RecyclerView.@Nullable Adapter newAdapter) { 608 // RV will remove all views so we should clear all spans and assignments of views into spans 609 mLazySpanLookup.clear(); 610 for (int i = 0; i < mSpanCount; i++) { 611 mSpans[i].clear(); 612 } 613 } 614 615 private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state, 616 boolean shouldCheckForGaps) { 617 final AnchorInfo anchorInfo = mAnchorInfo; 618 if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) { 619 if (state.getItemCount() == 0) { 620 removeAndRecycleAllViews(recycler); 621 anchorInfo.reset(); 622 return; 623 } 624 } 625 626 boolean recalculateAnchor = !anchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION 627 || mPendingSavedState != null; 628 if (recalculateAnchor) { 629 anchorInfo.reset(); 630 if (mPendingSavedState != null) { 631 applyPendingSavedState(anchorInfo); 632 } else { 633 resolveShouldLayoutReverse(); 634 anchorInfo.mLayoutFromEnd = mShouldReverseLayout; 635 } 636 updateAnchorInfoForLayout(state, anchorInfo); 637 anchorInfo.mValid = true; 638 } 639 if (mPendingSavedState == null && mPendingScrollPosition == RecyclerView.NO_POSITION) { 640 if (anchorInfo.mLayoutFromEnd != mLastLayoutFromEnd 641 || isLayoutRTL() != mLastLayoutRTL) { 642 mLazySpanLookup.clear(); 643 anchorInfo.mInvalidateOffsets = true; 644 } 645 } 646 647 if (getChildCount() > 0 && (mPendingSavedState == null 648 || mPendingSavedState.mSpanOffsetsSize < 1)) { 649 if (anchorInfo.mInvalidateOffsets) { 650 for (int i = 0; i < mSpanCount; i++) { 651 // Scroll to position is set, clear. 652 mSpans[i].clear(); 653 if (anchorInfo.mOffset != INVALID_OFFSET) { 654 mSpans[i].setLine(anchorInfo.mOffset); 655 } 656 } 657 } else { 658 if (recalculateAnchor || mAnchorInfo.mSpanReferenceLines == null) { 659 for (int i = 0; i < mSpanCount; i++) { 660 mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout, 661 anchorInfo.mOffset); 662 } 663 mAnchorInfo.saveSpanReferenceLines(mSpans); 664 } else { 665 for (int i = 0; i < mSpanCount; i++) { 666 final Span span = mSpans[i]; 667 span.clear(); 668 span.setLine(mAnchorInfo.mSpanReferenceLines[i]); 669 } 670 } 671 } 672 } 673 detachAndScrapAttachedViews(recycler); 674 mLayoutState.mRecycle = false; 675 mLaidOutInvalidFullSpan = false; 676 updateMeasureSpecs(mSecondaryOrientation.getTotalSpace()); 677 updateLayoutState(anchorInfo.mPosition, state); 678 if (anchorInfo.mLayoutFromEnd) { 679 // Layout start. 680 setLayoutStateDirection(LayoutState.LAYOUT_START); 681 fill(recycler, mLayoutState, state); 682 // Layout end. 683 setLayoutStateDirection(LayoutState.LAYOUT_END); 684 mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection; 685 fill(recycler, mLayoutState, state); 686 } else { 687 // Layout end. 688 setLayoutStateDirection(LayoutState.LAYOUT_END); 689 fill(recycler, mLayoutState, state); 690 // Layout start. 691 setLayoutStateDirection(LayoutState.LAYOUT_START); 692 mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection; 693 fill(recycler, mLayoutState, state); 694 } 695 696 repositionToWrapContentIfNecessary(); 697 698 if (getChildCount() > 0) { 699 if (mShouldReverseLayout) { 700 fixEndGap(recycler, state, true); 701 fixStartGap(recycler, state, false); 702 } else { 703 fixStartGap(recycler, state, true); 704 fixEndGap(recycler, state, false); 705 } 706 } 707 boolean hasGaps = false; 708 if (shouldCheckForGaps && !state.isPreLayout()) { 709 final boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE 710 && getChildCount() > 0 711 && (mLaidOutInvalidFullSpan || hasGapsToFix() != null); 712 if (needToCheckForGaps) { 713 removeCallbacks(mCheckForGapsRunnable); 714 if (checkForGaps()) { 715 hasGaps = true; 716 } 717 } 718 } 719 if (state.isPreLayout()) { 720 mAnchorInfo.reset(); 721 } 722 mLastLayoutFromEnd = anchorInfo.mLayoutFromEnd; 723 mLastLayoutRTL = isLayoutRTL(); 724 if (hasGaps) { 725 mAnchorInfo.reset(); 726 onLayoutChildren(recycler, state, false); 727 } 728 } 729 730 @Override 731 public void onLayoutCompleted(RecyclerView.State state) { 732 super.onLayoutCompleted(state); 733 mPendingScrollPosition = RecyclerView.NO_POSITION; 734 mPendingScrollPositionOffset = INVALID_OFFSET; 735 mPendingSavedState = null; // we don't need this anymore 736 mAnchorInfo.reset(); 737 } 738 739 private void repositionToWrapContentIfNecessary() { 740 if (mSecondaryOrientation.getMode() == View.MeasureSpec.EXACTLY) { 741 return; // nothing to do 742 } 743 float maxSize = 0; 744 final int childCount = getChildCount(); 745 for (int i = 0; i < childCount; i++) { 746 View child = getChildAt(i); 747 float size = mSecondaryOrientation.getDecoratedMeasurement(child); 748 if (size < maxSize) { 749 continue; 750 } 751 LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); 752 if (layoutParams.isFullSpan()) { 753 size = 1f * size / mSpanCount; 754 } 755 maxSize = Math.max(maxSize, size); 756 } 757 int before = mSizePerSpan; 758 int desired = Math.round(maxSize * mSpanCount); 759 if (mSecondaryOrientation.getMode() == View.MeasureSpec.AT_MOST) { 760 desired = Math.min(desired, mSecondaryOrientation.getTotalSpace()); 761 } 762 updateMeasureSpecs(desired); 763 if (mSizePerSpan == before) { 764 return; // nothing has changed 765 } 766 for (int i = 0; i < childCount; i++) { 767 View child = getChildAt(i); 768 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 769 if (lp.mFullSpan) { 770 continue; 771 } 772 if (isLayoutRTL() && mOrientation == VERTICAL) { 773 int newOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * mSizePerSpan; 774 int prevOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * before; 775 child.offsetLeftAndRight(newOffset - prevOffset); 776 } else { 777 int newOffset = lp.mSpan.mIndex * mSizePerSpan; 778 int prevOffset = lp.mSpan.mIndex * before; 779 if (mOrientation == VERTICAL) { 780 child.offsetLeftAndRight(newOffset - prevOffset); 781 } else { 782 child.offsetTopAndBottom(newOffset - prevOffset); 783 } 784 } 785 } 786 } 787 788 private void applyPendingSavedState(AnchorInfo anchorInfo) { 789 if (DEBUG) { 790 Log.d(TAG, "found saved state: " + mPendingSavedState); 791 } 792 if (mPendingSavedState.mSpanOffsetsSize > 0) { 793 if (mPendingSavedState.mSpanOffsetsSize == mSpanCount) { 794 for (int i = 0; i < mSpanCount; i++) { 795 mSpans[i].clear(); 796 int line = mPendingSavedState.mSpanOffsets[i]; 797 if (line != Span.INVALID_LINE) { 798 if (mPendingSavedState.mAnchorLayoutFromEnd) { 799 line += mPrimaryOrientation.getEndAfterPadding(); 800 } else { 801 line += mPrimaryOrientation.getStartAfterPadding(); 802 } 803 } 804 mSpans[i].setLine(line); 805 } 806 } else { 807 mPendingSavedState.invalidateSpanInfo(); 808 mPendingSavedState.mAnchorPosition = mPendingSavedState.mVisibleAnchorPosition; 809 } 810 } 811 mLastLayoutRTL = mPendingSavedState.mLastLayoutRTL; 812 setReverseLayout(mPendingSavedState.mReverseLayout); 813 resolveShouldLayoutReverse(); 814 815 if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) { 816 mPendingScrollPosition = mPendingSavedState.mAnchorPosition; 817 anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; 818 } else { 819 anchorInfo.mLayoutFromEnd = mShouldReverseLayout; 820 } 821 if (mPendingSavedState.mSpanLookupSize > 1) { 822 mLazySpanLookup.mData = mPendingSavedState.mSpanLookup; 823 mLazySpanLookup.mFullSpanItems = mPendingSavedState.mFullSpanItems; 824 } 825 } 826 827 void updateAnchorInfoForLayout(RecyclerView.State state, AnchorInfo anchorInfo) { 828 if (updateAnchorFromPendingData(state, anchorInfo)) { 829 return; 830 } 831 if (updateAnchorFromChildren(state, anchorInfo)) { 832 return; 833 } 834 if (DEBUG) { 835 Log.d(TAG, "Deciding anchor info from fresh state"); 836 } 837 anchorInfo.assignCoordinateFromPadding(); 838 anchorInfo.mPosition = 0; 839 } 840 841 private boolean updateAnchorFromChildren(RecyclerView.State state, AnchorInfo anchorInfo) { 842 // We don't recycle views out of adapter order. This way, we can rely on the first or 843 // last child as the anchor position. 844 // Layout direction may change but we should select the child depending on the latest 845 // layout direction. Otherwise, we'll choose the wrong child. 846 anchorInfo.mPosition = mLastLayoutFromEnd 847 ? findLastReferenceChildPosition(state.getItemCount()) 848 : findFirstReferenceChildPosition(state.getItemCount()); 849 anchorInfo.mOffset = INVALID_OFFSET; 850 return true; 851 } 852 853 boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { 854 // Validate scroll position if exists. 855 if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) { 856 return false; 857 } 858 // Validate it. 859 if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { 860 mPendingScrollPosition = RecyclerView.NO_POSITION; 861 mPendingScrollPositionOffset = INVALID_OFFSET; 862 return false; 863 } 864 865 if (mPendingSavedState == null || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION 866 || mPendingSavedState.mSpanOffsetsSize < 1) { 867 // If item is visible, make it fully visible. 868 final View child = findViewByPosition(mPendingScrollPosition); 869 if (child != null) { 870 // Use regular anchor position, offset according to pending offset and target 871 // child 872 anchorInfo.mPosition = mShouldReverseLayout ? getLastChildPosition() 873 : getFirstChildPosition(); 874 if (mPendingScrollPositionOffset != INVALID_OFFSET) { 875 if (anchorInfo.mLayoutFromEnd) { 876 final int target = mPrimaryOrientation.getEndAfterPadding() 877 - mPendingScrollPositionOffset; 878 anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedEnd(child); 879 } else { 880 final int target = mPrimaryOrientation.getStartAfterPadding() 881 + mPendingScrollPositionOffset; 882 anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedStart(child); 883 } 884 return true; 885 } 886 887 // no offset provided. Decide according to the child location 888 final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child); 889 if (childSize > mPrimaryOrientation.getTotalSpace()) { 890 // Item does not fit. Fix depending on layout direction. 891 anchorInfo.mOffset = anchorInfo.mLayoutFromEnd 892 ? mPrimaryOrientation.getEndAfterPadding() 893 : mPrimaryOrientation.getStartAfterPadding(); 894 return true; 895 } 896 897 final int startGap = mPrimaryOrientation.getDecoratedStart(child) 898 - mPrimaryOrientation.getStartAfterPadding(); 899 if (startGap < 0) { 900 anchorInfo.mOffset = -startGap; 901 return true; 902 } 903 final int endGap = mPrimaryOrientation.getEndAfterPadding() 904 - mPrimaryOrientation.getDecoratedEnd(child); 905 if (endGap < 0) { 906 anchorInfo.mOffset = endGap; 907 return true; 908 } 909 // child already visible. just layout as usual 910 anchorInfo.mOffset = INVALID_OFFSET; 911 } else { 912 // Child is not visible. Set anchor coordinate depending on in which direction 913 // child will be visible. 914 anchorInfo.mPosition = mPendingScrollPosition; 915 if (mPendingScrollPositionOffset == INVALID_OFFSET) { 916 final int position = calculateScrollDirectionForPosition( 917 anchorInfo.mPosition); 918 anchorInfo.mLayoutFromEnd = position == LayoutState.LAYOUT_END; 919 anchorInfo.assignCoordinateFromPadding(); 920 } else { 921 anchorInfo.assignCoordinateFromPadding(mPendingScrollPositionOffset); 922 } 923 anchorInfo.mInvalidateOffsets = true; 924 } 925 } else { 926 anchorInfo.mOffset = INVALID_OFFSET; 927 anchorInfo.mPosition = mPendingScrollPosition; 928 } 929 return true; 930 } 931 932 void updateMeasureSpecs(int totalSpace) { 933 mSizePerSpan = totalSpace / mSpanCount; 934 //noinspection ResourceType 935 mFullSizeSpec = View.MeasureSpec.makeMeasureSpec( 936 totalSpace, mSecondaryOrientation.getMode()); 937 } 938 939 @Override 940 public boolean supportsPredictiveItemAnimations() { 941 return mPendingSavedState == null; 942 } 943 944 /** 945 * Returns the adapter position of the first visible view for each span. 946 * <p> 947 * Note that, this value is not affected by layout orientation or item order traversal. 948 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 949 * not in the layout. 950 * <p> 951 * If RecyclerView has item decorators, they will be considered in calculations as well. 952 * <p> 953 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 954 * views are ignored in this method. 955 * 956 * @param into An array to put the results into. If you don't provide any, LayoutManager will 957 * create a new one. 958 * @return The adapter position of the first visible item in each span. If a span does not have 959 * any items, {@link RecyclerView#NO_POSITION} is returned for that span. 960 * @see #findFirstCompletelyVisibleItemPositions(int[]) 961 * @see #findLastVisibleItemPositions(int[]) 962 */ 963 public int[] findFirstVisibleItemPositions(int[] into) { 964 if (into == null) { 965 into = new int[mSpanCount]; 966 } else if (into.length < mSpanCount) { 967 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 968 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 969 } 970 for (int i = 0; i < mSpanCount; i++) { 971 into[i] = mSpans[i].findFirstVisibleItemPosition(); 972 } 973 return into; 974 } 975 976 /** 977 * Returns the adapter position of the first completely visible view for each span. 978 * <p> 979 * Note that, this value is not affected by layout orientation or item order traversal. 980 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 981 * not in the layout. 982 * <p> 983 * If RecyclerView has item decorators, they will be considered in calculations as well. 984 * <p> 985 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 986 * views are ignored in this method. 987 * 988 * @param into An array to put the results into. If you don't provide any, LayoutManager will 989 * create a new one. 990 * @return The adapter position of the first fully visible item in each span. If a span does 991 * not have any items, {@link RecyclerView#NO_POSITION} is returned for that span. 992 * @see #findFirstVisibleItemPositions(int[]) 993 * @see #findLastCompletelyVisibleItemPositions(int[]) 994 */ 995 public int[] findFirstCompletelyVisibleItemPositions(int[] into) { 996 if (into == null) { 997 into = new int[mSpanCount]; 998 } else if (into.length < mSpanCount) { 999 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 1000 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 1001 } 1002 for (int i = 0; i < mSpanCount; i++) { 1003 into[i] = mSpans[i].findFirstCompletelyVisibleItemPosition(); 1004 } 1005 return into; 1006 } 1007 1008 /** 1009 * Returns the adapter position of the last visible view for each span. 1010 * <p> 1011 * Note that, this value is not affected by layout orientation or item order traversal. 1012 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 1013 * not in the layout. 1014 * <p> 1015 * If RecyclerView has item decorators, they will be considered in calculations as well. 1016 * <p> 1017 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 1018 * views are ignored in this method. 1019 * 1020 * @param into An array to put the results into. If you don't provide any, LayoutManager will 1021 * create a new one. 1022 * @return The adapter position of the last visible item in each span. If a span does not have 1023 * any items, {@link RecyclerView#NO_POSITION} is returned for that span. 1024 * @see #findLastCompletelyVisibleItemPositions(int[]) 1025 * @see #findFirstVisibleItemPositions(int[]) 1026 */ 1027 public int[] findLastVisibleItemPositions(int[] into) { 1028 if (into == null) { 1029 into = new int[mSpanCount]; 1030 } else if (into.length < mSpanCount) { 1031 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 1032 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 1033 } 1034 for (int i = 0; i < mSpanCount; i++) { 1035 into[i] = mSpans[i].findLastVisibleItemPosition(); 1036 } 1037 return into; 1038 } 1039 1040 /** 1041 * Returns the adapter position of the last completely visible view for each span. 1042 * <p> 1043 * Note that, this value is not affected by layout orientation or item order traversal. 1044 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 1045 * not in the layout. 1046 * <p> 1047 * If RecyclerView has item decorators, they will be considered in calculations as well. 1048 * <p> 1049 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 1050 * views are ignored in this method. 1051 * 1052 * @param into An array to put the results into. If you don't provide any, LayoutManager will 1053 * create a new one. 1054 * @return The adapter position of the last fully visible item in each span. If a span does not 1055 * have any items, {@link RecyclerView#NO_POSITION} is returned for that span. 1056 * @see #findFirstCompletelyVisibleItemPositions(int[]) 1057 * @see #findLastVisibleItemPositions(int[]) 1058 */ 1059 public int[] findLastCompletelyVisibleItemPositions(int[] into) { 1060 if (into == null) { 1061 into = new int[mSpanCount]; 1062 } else if (into.length < mSpanCount) { 1063 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 1064 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 1065 } 1066 for (int i = 0; i < mSpanCount; i++) { 1067 into[i] = mSpans[i].findLastCompletelyVisibleItemPosition(); 1068 } 1069 return into; 1070 } 1071 1072 @Override 1073 public int computeHorizontalScrollOffset(RecyclerView.State state) { 1074 return computeScrollOffset(state); 1075 } 1076 1077 private int computeScrollOffset(RecyclerView.State state) { 1078 if (getChildCount() == 0) { 1079 return 0; 1080 } 1081 return ScrollbarHelper.computeScrollOffset(state, mPrimaryOrientation, 1082 findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled), 1083 findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled), 1084 this, mSmoothScrollbarEnabled, mShouldReverseLayout); 1085 } 1086 1087 @Override 1088 public int computeVerticalScrollOffset(RecyclerView.State state) { 1089 return computeScrollOffset(state); 1090 } 1091 1092 @Override 1093 public int computeHorizontalScrollExtent(RecyclerView.State state) { 1094 return computeScrollExtent(state); 1095 } 1096 1097 private int computeScrollExtent(RecyclerView.State state) { 1098 if (getChildCount() == 0) { 1099 return 0; 1100 } 1101 return ScrollbarHelper.computeScrollExtent(state, mPrimaryOrientation, 1102 findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled), 1103 findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled), 1104 this, mSmoothScrollbarEnabled); 1105 } 1106 1107 @Override 1108 public int computeVerticalScrollExtent(RecyclerView.State state) { 1109 return computeScrollExtent(state); 1110 } 1111 1112 @Override 1113 public int computeHorizontalScrollRange(RecyclerView.State state) { 1114 return computeScrollRange(state); 1115 } 1116 1117 private int computeScrollRange(RecyclerView.State state) { 1118 if (getChildCount() == 0) { 1119 return 0; 1120 } 1121 return ScrollbarHelper.computeScrollRange(state, mPrimaryOrientation, 1122 findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled), 1123 findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled), 1124 this, mSmoothScrollbarEnabled); 1125 } 1126 1127 @Override 1128 public int computeVerticalScrollRange(RecyclerView.State state) { 1129 return computeScrollRange(state); 1130 } 1131 1132 private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp, 1133 boolean alreadyMeasured) { 1134 if (lp.mFullSpan) { 1135 if (mOrientation == VERTICAL) { 1136 measureChildWithDecorationsAndMargin(child, mFullSizeSpec, 1137 getChildMeasureSpec( 1138 getHeight(), 1139 getHeightMode(), 1140 getPaddingTop() + getPaddingBottom(), 1141 lp.height, 1142 true), 1143 alreadyMeasured); 1144 } else { 1145 measureChildWithDecorationsAndMargin( 1146 child, 1147 getChildMeasureSpec( 1148 getWidth(), 1149 getWidthMode(), 1150 getPaddingLeft() + getPaddingRight(), 1151 lp.width, 1152 true), 1153 mFullSizeSpec, 1154 alreadyMeasured); 1155 } 1156 } else { 1157 if (mOrientation == VERTICAL) { 1158 // Padding for width measure spec is 0 because left and right padding were already 1159 // factored into mSizePerSpan. 1160 measureChildWithDecorationsAndMargin( 1161 child, 1162 getChildMeasureSpec( 1163 mSizePerSpan, 1164 getWidthMode(), 1165 0, 1166 lp.width, 1167 false), 1168 getChildMeasureSpec( 1169 getHeight(), 1170 getHeightMode(), 1171 getPaddingTop() + getPaddingBottom(), 1172 lp.height, 1173 true), 1174 alreadyMeasured); 1175 } else { 1176 // Padding for height measure spec is 0 because top and bottom padding were already 1177 // factored into mSizePerSpan. 1178 measureChildWithDecorationsAndMargin( 1179 child, 1180 getChildMeasureSpec( 1181 getWidth(), 1182 getWidthMode(), 1183 getPaddingLeft() + getPaddingRight(), 1184 lp.width, 1185 true), 1186 getChildMeasureSpec( 1187 mSizePerSpan, 1188 getHeightMode(), 1189 0, 1190 lp.height, 1191 false), 1192 alreadyMeasured); 1193 } 1194 } 1195 } 1196 1197 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, 1198 int heightSpec, boolean alreadyMeasured) { 1199 calculateItemDecorationsForChild(child, mTmpRect); 1200 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1201 widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mTmpRect.left, 1202 lp.rightMargin + mTmpRect.right); 1203 heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mTmpRect.top, 1204 lp.bottomMargin + mTmpRect.bottom); 1205 final boolean measure = alreadyMeasured 1206 ? shouldReMeasureChild(child, widthSpec, heightSpec, lp) 1207 : shouldMeasureChild(child, widthSpec, heightSpec, lp); 1208 if (measure) { 1209 child.measure(widthSpec, heightSpec); 1210 } 1211 1212 } 1213 1214 private int updateSpecWithExtra(int spec, int startInset, int endInset) { 1215 if (startInset == 0 && endInset == 0) { 1216 return spec; 1217 } 1218 final int mode = View.MeasureSpec.getMode(spec); 1219 if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { 1220 return View.MeasureSpec.makeMeasureSpec( 1221 Math.max(0, View.MeasureSpec.getSize(spec) - startInset - endInset), mode); 1222 } 1223 return spec; 1224 } 1225 1226 @Override 1227 public void onRestoreInstanceState(Parcelable state) { 1228 if (state instanceof SavedState) { 1229 mPendingSavedState = (SavedState) state; 1230 if (mPendingScrollPosition != RecyclerView.NO_POSITION) { 1231 mPendingSavedState.invalidateAnchorPositionInfo(); 1232 mPendingSavedState.invalidateSpanInfo(); 1233 } 1234 requestLayout(); 1235 } else if (DEBUG) { 1236 Log.d(TAG, "invalid saved state class"); 1237 } 1238 } 1239 1240 @Override 1241 public Parcelable onSaveInstanceState() { 1242 if (mPendingSavedState != null) { 1243 return new SavedState(mPendingSavedState); 1244 } 1245 SavedState state = new SavedState(); 1246 state.mReverseLayout = mReverseLayout; 1247 state.mAnchorLayoutFromEnd = mLastLayoutFromEnd; 1248 state.mLastLayoutRTL = mLastLayoutRTL; 1249 1250 if (mLazySpanLookup != null && mLazySpanLookup.mData != null) { 1251 state.mSpanLookup = mLazySpanLookup.mData; 1252 state.mSpanLookupSize = state.mSpanLookup.length; 1253 state.mFullSpanItems = mLazySpanLookup.mFullSpanItems; 1254 } else { 1255 state.mSpanLookupSize = 0; 1256 } 1257 1258 if (getChildCount() > 0) { 1259 state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition() 1260 : getFirstChildPosition(); 1261 state.mVisibleAnchorPosition = findFirstVisibleItemPositionInt(); 1262 state.mSpanOffsetsSize = mSpanCount; 1263 state.mSpanOffsets = new int[mSpanCount]; 1264 for (int i = 0; i < mSpanCount; i++) { 1265 int line; 1266 if (mLastLayoutFromEnd) { 1267 line = mSpans[i].getEndLine(Span.INVALID_LINE); 1268 if (line != Span.INVALID_LINE) { 1269 line -= mPrimaryOrientation.getEndAfterPadding(); 1270 } 1271 } else { 1272 line = mSpans[i].getStartLine(Span.INVALID_LINE); 1273 if (line != Span.INVALID_LINE) { 1274 line -= mPrimaryOrientation.getStartAfterPadding(); 1275 } 1276 } 1277 state.mSpanOffsets[i] = line; 1278 } 1279 } else { 1280 state.mAnchorPosition = RecyclerView.NO_POSITION; 1281 state.mVisibleAnchorPosition = RecyclerView.NO_POSITION; 1282 state.mSpanOffsetsSize = 0; 1283 } 1284 if (DEBUG) { 1285 Log.d(TAG, "saved state:\n" + state); 1286 } 1287 return state; 1288 } 1289 1290 @Override 1291 public void onInitializeAccessibilityNodeInfo(RecyclerView.@NonNull Recycler recycler, 1292 RecyclerView.@NonNull State state, @NonNull AccessibilityNodeInfoCompat info) { 1293 super.onInitializeAccessibilityNodeInfo(recycler, state, info); 1294 // Setting the classname allows accessibility services to set a role for staggered grids 1295 // and ensures that they are treated distinctly from canonical grids with clear row/column 1296 // semantics. 1297 info.setClassName("androidx.recyclerview.widget.StaggeredGridLayoutManager"); 1298 } 1299 1300 @Override 1301 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.@NonNull Recycler recycler, 1302 RecyclerView.@NonNull State state, @NonNull View host, 1303 @NonNull AccessibilityNodeInfoCompat info) { 1304 ViewGroup.LayoutParams lp = host.getLayoutParams(); 1305 if (!(lp instanceof LayoutParams)) { 1306 super.onInitializeAccessibilityNodeInfoForItem(host, info); 1307 return; 1308 } 1309 LayoutParams sglp = (LayoutParams) lp; 1310 if (mOrientation == HORIZONTAL) { 1311 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 1312 sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1, 1313 -1, -1, false, false)); 1314 } else { // VERTICAL 1315 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 1316 -1, -1, 1317 sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1, false, false)); 1318 } 1319 } 1320 @Override 1321 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1322 super.onInitializeAccessibilityEvent(event); 1323 if (getChildCount() > 0) { 1324 final View start = findFirstVisibleItemClosestToStart(false); 1325 final View end = findFirstVisibleItemClosestToEnd(false); 1326 if (start == null || end == null) { 1327 return; 1328 } 1329 final int startPos = getPosition(start); 1330 final int endPos = getPosition(end); 1331 if (startPos < endPos) { 1332 event.setFromIndex(startPos); 1333 event.setToIndex(endPos); 1334 } else { 1335 event.setFromIndex(endPos); 1336 event.setToIndex(startPos); 1337 } 1338 } 1339 } 1340 1341 /** 1342 * Finds the first fully visible child to be used as an anchor child if span count changes when 1343 * state is restored. If no children is fully visible, returns a partially visible child instead 1344 * of returning null. 1345 */ 1346 int findFirstVisibleItemPositionInt() { 1347 final View first = mShouldReverseLayout ? findFirstVisibleItemClosestToEnd(true) : 1348 findFirstVisibleItemClosestToStart(true); 1349 return first == null ? RecyclerView.NO_POSITION : getPosition(first); 1350 } 1351 1352 @Override 1353 public int getRowCountForAccessibility(RecyclerView.@NonNull Recycler recycler, 1354 RecyclerView.@NonNull State state) { 1355 if (mOrientation == HORIZONTAL) { 1356 return Math.min(mSpanCount, state.getItemCount()); 1357 } 1358 return -1; 1359 } 1360 1361 @Override 1362 public int getColumnCountForAccessibility(RecyclerView.@NonNull Recycler recycler, 1363 RecyclerView.@NonNull State state) { 1364 if (mOrientation == VERTICAL) { 1365 return Math.min(mSpanCount, state.getItemCount()); 1366 } 1367 return -1; 1368 } 1369 1370 /** 1371 * This is for internal use. Not necessarily the child closest to start but the first child 1372 * we find that matches the criteria. 1373 * This method does not do any sorting based on child's start coordinate, instead, it uses 1374 * children order. 1375 */ 1376 View findFirstVisibleItemClosestToStart(boolean fullyVisible) { 1377 final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); 1378 final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); 1379 final int limit = getChildCount(); 1380 View partiallyVisible = null; 1381 for (int i = 0; i < limit; i++) { 1382 final View child = getChildAt(i); 1383 final int childStart = mPrimaryOrientation.getDecoratedStart(child); 1384 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); 1385 if (childEnd <= boundsStart || childStart >= boundsEnd) { 1386 continue; // not visible at all 1387 } 1388 if (childStart >= boundsStart || !fullyVisible) { 1389 // when checking for start, it is enough even if part of the child's top is visible 1390 // as long as fully visible is not requested. 1391 return child; 1392 } 1393 if (partiallyVisible == null) { 1394 partiallyVisible = child; 1395 } 1396 } 1397 return partiallyVisible; 1398 } 1399 1400 /** 1401 * This is for internal use. Not necessarily the child closest to bottom but the first child 1402 * we find that matches the criteria. 1403 * This method does not do any sorting based on child's end coordinate, instead, it uses 1404 * children order. 1405 */ 1406 View findFirstVisibleItemClosestToEnd(boolean fullyVisible) { 1407 final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); 1408 final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); 1409 View partiallyVisible = null; 1410 for (int i = getChildCount() - 1; i >= 0; i--) { 1411 final View child = getChildAt(i); 1412 final int childStart = mPrimaryOrientation.getDecoratedStart(child); 1413 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); 1414 if (childEnd <= boundsStart || childStart >= boundsEnd) { 1415 continue; // not visible at all 1416 } 1417 if (childEnd <= boundsEnd || !fullyVisible) { 1418 // when checking for end, it is enough even if part of the child's bottom is visible 1419 // as long as fully visible is not requested. 1420 return child; 1421 } 1422 if (partiallyVisible == null) { 1423 partiallyVisible = child; 1424 } 1425 } 1426 return partiallyVisible; 1427 } 1428 1429 private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state, 1430 boolean canOffsetChildren) { 1431 final int maxEndLine = getMaxEnd(Integer.MIN_VALUE); 1432 if (maxEndLine == Integer.MIN_VALUE) { 1433 return; 1434 } 1435 int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine; 1436 int fixOffset; 1437 if (gap > 0) { 1438 fixOffset = -scrollBy(-gap, recycler, state); 1439 } else { 1440 return; // nothing to fix 1441 } 1442 gap -= fixOffset; 1443 if (canOffsetChildren && gap > 0) { 1444 mPrimaryOrientation.offsetChildren(gap); 1445 } 1446 } 1447 1448 private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state, 1449 boolean canOffsetChildren) { 1450 final int minStartLine = getMinStart(Integer.MAX_VALUE); 1451 if (minStartLine == Integer.MAX_VALUE) { 1452 return; 1453 } 1454 int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding(); 1455 int fixOffset; 1456 if (gap > 0) { 1457 fixOffset = scrollBy(gap, recycler, state); 1458 } else { 1459 return; // nothing to fix 1460 } 1461 gap -= fixOffset; 1462 if (canOffsetChildren && gap > 0) { 1463 mPrimaryOrientation.offsetChildren(-gap); 1464 } 1465 } 1466 1467 private void updateLayoutState(int anchorPosition, RecyclerView.State state) { 1468 mLayoutState.mAvailable = 0; 1469 mLayoutState.mCurrentPosition = anchorPosition; 1470 int startExtra = 0; 1471 int endExtra = 0; 1472 if (isSmoothScrolling()) { 1473 final int targetPos = state.getTargetScrollPosition(); 1474 if (targetPos != RecyclerView.NO_POSITION) { 1475 if (mShouldReverseLayout == targetPos < anchorPosition) { 1476 endExtra = mPrimaryOrientation.getTotalSpace(); 1477 } else { 1478 startExtra = mPrimaryOrientation.getTotalSpace(); 1479 } 1480 } 1481 } 1482 1483 // Line of the furthest row. 1484 final boolean clipToPadding = getClipToPadding(); 1485 if (clipToPadding) { 1486 mLayoutState.mStartLine = mPrimaryOrientation.getStartAfterPadding() - startExtra; 1487 mLayoutState.mEndLine = mPrimaryOrientation.getEndAfterPadding() + endExtra; 1488 } else { 1489 mLayoutState.mEndLine = mPrimaryOrientation.getEnd() + endExtra; 1490 mLayoutState.mStartLine = -startExtra; 1491 } 1492 mLayoutState.mStopInFocusable = false; 1493 mLayoutState.mRecycle = true; 1494 mLayoutState.mInfinite = mPrimaryOrientation.getMode() == View.MeasureSpec.UNSPECIFIED 1495 && mPrimaryOrientation.getEnd() == 0; 1496 } 1497 1498 private void setLayoutStateDirection(int direction) { 1499 mLayoutState.mLayoutDirection = direction; 1500 mLayoutState.mItemDirection = (mShouldReverseLayout == (direction == LayoutState.LAYOUT_START)) 1501 ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; 1502 } 1503 1504 @Override 1505 public void offsetChildrenHorizontal(int dx) { 1506 super.offsetChildrenHorizontal(dx); 1507 for (int i = 0; i < mSpanCount; i++) { 1508 mSpans[i].onOffset(dx); 1509 } 1510 } 1511 1512 @Override 1513 public void offsetChildrenVertical(int dy) { 1514 super.offsetChildrenVertical(dy); 1515 for (int i = 0; i < mSpanCount; i++) { 1516 mSpans[i].onOffset(dy); 1517 } 1518 } 1519 1520 @Override 1521 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 1522 handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.REMOVE); 1523 } 1524 1525 @Override 1526 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 1527 handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.ADD); 1528 } 1529 1530 @Override 1531 public void onItemsChanged(RecyclerView recyclerView) { 1532 mLazySpanLookup.clear(); 1533 requestLayout(); 1534 } 1535 1536 @Override 1537 public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { 1538 handleUpdate(from, to, AdapterHelper.UpdateOp.MOVE); 1539 } 1540 1541 @Override 1542 public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, 1543 Object payload) { 1544 handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.UPDATE); 1545 } 1546 1547 /** 1548 * Checks whether it should invalidate span assignments in response to an adapter change. 1549 */ 1550 private void handleUpdate(int positionStart, int itemCountOrToPosition, int cmd) { 1551 int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition(); 1552 final int affectedRangeEnd; // exclusive 1553 final int affectedRangeStart; // inclusive 1554 1555 if (cmd == AdapterHelper.UpdateOp.MOVE) { 1556 if (positionStart < itemCountOrToPosition) { 1557 affectedRangeEnd = itemCountOrToPosition + 1; 1558 affectedRangeStart = positionStart; 1559 } else { 1560 affectedRangeEnd = positionStart + 1; 1561 affectedRangeStart = itemCountOrToPosition; 1562 } 1563 } else { 1564 affectedRangeStart = positionStart; 1565 affectedRangeEnd = positionStart + itemCountOrToPosition; 1566 } 1567 1568 mLazySpanLookup.invalidateAfter(affectedRangeStart); 1569 switch (cmd) { 1570 case AdapterHelper.UpdateOp.ADD: 1571 mLazySpanLookup.offsetForAddition(positionStart, itemCountOrToPosition); 1572 break; 1573 case AdapterHelper.UpdateOp.REMOVE: 1574 mLazySpanLookup.offsetForRemoval(positionStart, itemCountOrToPosition); 1575 break; 1576 case AdapterHelper.UpdateOp.MOVE: 1577 // TODO optimize 1578 mLazySpanLookup.offsetForRemoval(positionStart, 1); 1579 mLazySpanLookup.offsetForAddition(itemCountOrToPosition, 1); 1580 break; 1581 } 1582 1583 if (affectedRangeEnd <= minPosition) { 1584 return; 1585 } 1586 1587 int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition(); 1588 if (affectedRangeStart <= maxPosition) { 1589 requestLayout(); 1590 } 1591 } 1592 1593 private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, 1594 RecyclerView.State state) { 1595 mRemainingSpans.set(0, mSpanCount, true); 1596 // The target position we are trying to reach. 1597 final int targetLine; 1598 1599 // Line of the furthest row. 1600 if (mLayoutState.mInfinite) { 1601 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { 1602 targetLine = Integer.MAX_VALUE; 1603 } else { // LAYOUT_START 1604 targetLine = Integer.MIN_VALUE; 1605 } 1606 } else { 1607 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { 1608 targetLine = layoutState.mEndLine + layoutState.mAvailable; 1609 } else { // LAYOUT_START 1610 targetLine = layoutState.mStartLine - layoutState.mAvailable; 1611 } 1612 } 1613 1614 updateAllRemainingSpans(layoutState.mLayoutDirection, targetLine); 1615 if (DEBUG) { 1616 Log.d(TAG, "FILLING targetLine: " + targetLine + "," 1617 + "remaining spans:" + mRemainingSpans + ", state: " + layoutState); 1618 } 1619 1620 // the default coordinate to add new view. 1621 final int defaultNewViewLine = mShouldReverseLayout 1622 ? mPrimaryOrientation.getEndAfterPadding() 1623 : mPrimaryOrientation.getStartAfterPadding(); 1624 boolean added = false; 1625 while (layoutState.hasMore(state) 1626 && (mLayoutState.mInfinite || !mRemainingSpans.isEmpty())) { 1627 View view = layoutState.next(recycler); 1628 LayoutParams lp = ((LayoutParams) view.getLayoutParams()); 1629 final int position = lp.getViewLayoutPosition(); 1630 final int spanIndex = mLazySpanLookup.getSpan(position); 1631 Span currentSpan; 1632 final boolean assignSpan = spanIndex == LayoutParams.INVALID_SPAN_ID; 1633 if (assignSpan) { 1634 currentSpan = lp.mFullSpan ? mSpans[0] : getNextSpan(layoutState); 1635 mLazySpanLookup.setSpan(position, currentSpan); 1636 if (DEBUG) { 1637 Log.d(TAG, "assigned " + currentSpan.mIndex + " for " + position); 1638 } 1639 } else { 1640 if (DEBUG) { 1641 Log.d(TAG, "using " + spanIndex + " for pos " + position); 1642 } 1643 currentSpan = mSpans[spanIndex]; 1644 } 1645 // assign span before measuring so that item decorators can get updated span index 1646 lp.mSpan = currentSpan; 1647 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { 1648 addView(view); 1649 } else { 1650 addView(view, 0); 1651 } 1652 measureChildWithDecorationsAndMargin(view, lp, false); 1653 1654 final int start; 1655 final int end; 1656 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { 1657 start = lp.mFullSpan ? getMaxEnd(defaultNewViewLine) 1658 : currentSpan.getEndLine(defaultNewViewLine); 1659 end = start + mPrimaryOrientation.getDecoratedMeasurement(view); 1660 if (assignSpan && lp.mFullSpan) { 1661 LazySpanLookup.FullSpanItem fullSpanItem; 1662 fullSpanItem = createFullSpanItemFromEnd(start); 1663 fullSpanItem.mGapDir = LayoutState.LAYOUT_START; 1664 fullSpanItem.mPosition = position; 1665 mLazySpanLookup.addFullSpanItem(fullSpanItem); 1666 } 1667 } else { 1668 end = lp.mFullSpan ? getMinStart(defaultNewViewLine) 1669 : currentSpan.getStartLine(defaultNewViewLine); 1670 start = end - mPrimaryOrientation.getDecoratedMeasurement(view); 1671 if (assignSpan && lp.mFullSpan) { 1672 LazySpanLookup.FullSpanItem fullSpanItem; 1673 fullSpanItem = createFullSpanItemFromStart(end); 1674 fullSpanItem.mGapDir = LayoutState.LAYOUT_END; 1675 fullSpanItem.mPosition = position; 1676 mLazySpanLookup.addFullSpanItem(fullSpanItem); 1677 } 1678 } 1679 1680 // check if this item may create gaps in the future 1681 if (lp.mFullSpan && layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_HEAD) { 1682 if (assignSpan) { 1683 mLaidOutInvalidFullSpan = true; 1684 } else { 1685 final boolean hasInvalidGap; 1686 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { 1687 hasInvalidGap = !areAllEndsEqual(); 1688 } else { // layoutState.mLayoutDirection == LAYOUT_START 1689 hasInvalidGap = !areAllStartsEqual(); 1690 } 1691 if (hasInvalidGap) { 1692 final LazySpanLookup.FullSpanItem fullSpanItem = mLazySpanLookup 1693 .getFullSpanItem(position); 1694 if (fullSpanItem != null) { 1695 fullSpanItem.mHasUnwantedGapAfter = true; 1696 } 1697 mLaidOutInvalidFullSpan = true; 1698 } 1699 } 1700 } 1701 attachViewToSpans(view, lp, layoutState); 1702 final int otherStart; 1703 final int otherEnd; 1704 if (isLayoutRTL() && mOrientation == VERTICAL) { 1705 otherEnd = lp.mFullSpan ? mSecondaryOrientation.getEndAfterPadding() : 1706 mSecondaryOrientation.getEndAfterPadding() 1707 - (mSpanCount - 1 - currentSpan.mIndex) * mSizePerSpan; 1708 otherStart = otherEnd - mSecondaryOrientation.getDecoratedMeasurement(view); 1709 } else { 1710 otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding() 1711 : currentSpan.mIndex * mSizePerSpan 1712 + mSecondaryOrientation.getStartAfterPadding(); 1713 otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view); 1714 } 1715 1716 if (mOrientation == VERTICAL) { 1717 layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end); 1718 } else { 1719 layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd); 1720 } 1721 1722 if (lp.mFullSpan) { 1723 updateAllRemainingSpans(mLayoutState.mLayoutDirection, targetLine); 1724 } else { 1725 updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine); 1726 } 1727 recycle(recycler, mLayoutState); 1728 if (mLayoutState.mStopInFocusable && view.hasFocusable()) { 1729 if (lp.mFullSpan) { 1730 mRemainingSpans.clear(); 1731 } else { 1732 mRemainingSpans.set(currentSpan.mIndex, false); 1733 } 1734 } 1735 added = true; 1736 } 1737 if (!added) { 1738 recycle(recycler, mLayoutState); 1739 } 1740 final int diff; 1741 if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1742 final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding()); 1743 diff = mPrimaryOrientation.getStartAfterPadding() - minStart; 1744 } else { 1745 final int maxEnd = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); 1746 diff = maxEnd - mPrimaryOrientation.getEndAfterPadding(); 1747 } 1748 return diff > 0 ? Math.min(layoutState.mAvailable, diff) : 0; 1749 } 1750 1751 private LazySpanLookup.FullSpanItem createFullSpanItemFromEnd(int newItemTop) { 1752 LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem(); 1753 fsi.mGapPerSpan = new int[mSpanCount]; 1754 for (int i = 0; i < mSpanCount; i++) { 1755 fsi.mGapPerSpan[i] = newItemTop - mSpans[i].getEndLine(newItemTop); 1756 } 1757 return fsi; 1758 } 1759 1760 private LazySpanLookup.FullSpanItem createFullSpanItemFromStart(int newItemBottom) { 1761 LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem(); 1762 fsi.mGapPerSpan = new int[mSpanCount]; 1763 for (int i = 0; i < mSpanCount; i++) { 1764 fsi.mGapPerSpan[i] = mSpans[i].getStartLine(newItemBottom) - newItemBottom; 1765 } 1766 return fsi; 1767 } 1768 1769 private void attachViewToSpans(View view, LayoutParams lp, LayoutState layoutState) { 1770 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { 1771 if (lp.mFullSpan) { 1772 appendViewToAllSpans(view); 1773 } else { 1774 lp.mSpan.appendToSpan(view); 1775 } 1776 } else { 1777 if (lp.mFullSpan) { 1778 prependViewToAllSpans(view); 1779 } else { 1780 lp.mSpan.prependToSpan(view); 1781 } 1782 } 1783 } 1784 1785 private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState) { 1786 if (!layoutState.mRecycle || layoutState.mInfinite) { 1787 return; 1788 } 1789 if (layoutState.mAvailable == 0) { 1790 // easy, recycle line is still valid 1791 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1792 recycleFromEnd(recycler, layoutState.mEndLine); 1793 } else { 1794 recycleFromStart(recycler, layoutState.mStartLine); 1795 } 1796 } else { 1797 // scrolling case, recycle line can be shifted by how much space we could cover 1798 // by adding new views 1799 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1800 // calculate recycle line 1801 int scrolled = layoutState.mStartLine - getMaxStart(layoutState.mStartLine); 1802 final int line; 1803 if (scrolled < 0) { 1804 line = layoutState.mEndLine; 1805 } else { 1806 line = layoutState.mEndLine - Math.min(scrolled, layoutState.mAvailable); 1807 } 1808 recycleFromEnd(recycler, line); 1809 } else { 1810 // calculate recycle line 1811 int scrolled = getMinEnd(layoutState.mEndLine) - layoutState.mEndLine; 1812 final int line; 1813 if (scrolled < 0) { 1814 line = layoutState.mStartLine; 1815 } else { 1816 line = layoutState.mStartLine + Math.min(scrolled, layoutState.mAvailable); 1817 } 1818 recycleFromStart(recycler, line); 1819 } 1820 } 1821 1822 } 1823 1824 private void appendViewToAllSpans(View view) { 1825 // traverse in reverse so that we end up assigning full span items to 0 1826 for (int i = mSpanCount - 1; i >= 0; i--) { 1827 mSpans[i].appendToSpan(view); 1828 } 1829 } 1830 1831 private void prependViewToAllSpans(View view) { 1832 // traverse in reverse so that we end up assigning full span items to 0 1833 for (int i = mSpanCount - 1; i >= 0; i--) { 1834 mSpans[i].prependToSpan(view); 1835 } 1836 } 1837 1838 private void updateAllRemainingSpans(int layoutDir, int targetLine) { 1839 for (int i = 0; i < mSpanCount; i++) { 1840 if (mSpans[i].mViews.isEmpty()) { 1841 continue; 1842 } 1843 updateRemainingSpans(mSpans[i], layoutDir, targetLine); 1844 } 1845 } 1846 1847 private void updateRemainingSpans(Span span, int layoutDir, int targetLine) { 1848 final int deletedSize = span.getDeletedSize(); 1849 if (layoutDir == LayoutState.LAYOUT_START) { 1850 final int line = span.getStartLine(); 1851 if (line + deletedSize <= targetLine) { 1852 mRemainingSpans.set(span.mIndex, false); 1853 } 1854 } else { 1855 final int line = span.getEndLine(); 1856 if (line - deletedSize >= targetLine) { 1857 mRemainingSpans.set(span.mIndex, false); 1858 } 1859 } 1860 } 1861 1862 private int getMaxStart(int def) { 1863 int maxStart = mSpans[0].getStartLine(def); 1864 for (int i = 1; i < mSpanCount; i++) { 1865 final int spanStart = mSpans[i].getStartLine(def); 1866 if (spanStart > maxStart) { 1867 maxStart = spanStart; 1868 } 1869 } 1870 return maxStart; 1871 } 1872 1873 private int getMinStart(int def) { 1874 int minStart = mSpans[0].getStartLine(def); 1875 for (int i = 1; i < mSpanCount; i++) { 1876 final int spanStart = mSpans[i].getStartLine(def); 1877 if (spanStart < minStart) { 1878 minStart = spanStart; 1879 } 1880 } 1881 return minStart; 1882 } 1883 1884 boolean areAllEndsEqual() { 1885 int end = mSpans[0].getEndLine(Span.INVALID_LINE); 1886 for (int i = 1; i < mSpanCount; i++) { 1887 if (mSpans[i].getEndLine(Span.INVALID_LINE) != end) { 1888 return false; 1889 } 1890 } 1891 return true; 1892 } 1893 1894 boolean areAllStartsEqual() { 1895 int start = mSpans[0].getStartLine(Span.INVALID_LINE); 1896 for (int i = 1; i < mSpanCount; i++) { 1897 if (mSpans[i].getStartLine(Span.INVALID_LINE) != start) { 1898 return false; 1899 } 1900 } 1901 return true; 1902 } 1903 1904 private int getMaxEnd(int def) { 1905 int maxEnd = mSpans[0].getEndLine(def); 1906 for (int i = 1; i < mSpanCount; i++) { 1907 final int spanEnd = mSpans[i].getEndLine(def); 1908 if (spanEnd > maxEnd) { 1909 maxEnd = spanEnd; 1910 } 1911 } 1912 return maxEnd; 1913 } 1914 1915 private int getMinEnd(int def) { 1916 int minEnd = mSpans[0].getEndLine(def); 1917 for (int i = 1; i < mSpanCount; i++) { 1918 final int spanEnd = mSpans[i].getEndLine(def); 1919 if (spanEnd < minEnd) { 1920 minEnd = spanEnd; 1921 } 1922 } 1923 return minEnd; 1924 } 1925 1926 private void recycleFromStart(RecyclerView.Recycler recycler, int line) { 1927 while (getChildCount() > 0) { 1928 View child = getChildAt(0); 1929 if (mPrimaryOrientation.getDecoratedEnd(child) <= line 1930 && mPrimaryOrientation.getTransformedEndWithDecoration(child) <= line) { 1931 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1932 // Don't recycle the last View in a span not to lose span's start/end lines 1933 if (lp.mFullSpan) { 1934 for (int j = 0; j < mSpanCount; j++) { 1935 if (mSpans[j].mViews.size() == 1) { 1936 return; 1937 } 1938 } 1939 for (int j = 0; j < mSpanCount; j++) { 1940 mSpans[j].popStart(); 1941 } 1942 } else { 1943 if (lp.mSpan.mViews.size() == 1) { 1944 return; 1945 } 1946 lp.mSpan.popStart(); 1947 } 1948 removeAndRecycleView(child, recycler); 1949 } else { 1950 return; // done 1951 } 1952 } 1953 } 1954 1955 private void recycleFromEnd(RecyclerView.Recycler recycler, int line) { 1956 final int childCount = getChildCount(); 1957 int i; 1958 for (i = childCount - 1; i >= 0; i--) { 1959 View child = getChildAt(i); 1960 if (mPrimaryOrientation.getDecoratedStart(child) >= line 1961 && mPrimaryOrientation.getTransformedStartWithDecoration(child) >= line) { 1962 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1963 // Don't recycle the last View in a span not to lose span's start/end lines 1964 if (lp.mFullSpan) { 1965 for (int j = 0; j < mSpanCount; j++) { 1966 if (mSpans[j].mViews.size() == 1) { 1967 return; 1968 } 1969 } 1970 for (int j = 0; j < mSpanCount; j++) { 1971 mSpans[j].popEnd(); 1972 } 1973 } else { 1974 if (lp.mSpan.mViews.size() == 1) { 1975 return; 1976 } 1977 lp.mSpan.popEnd(); 1978 } 1979 removeAndRecycleView(child, recycler); 1980 } else { 1981 return; // done 1982 } 1983 } 1984 } 1985 1986 /** 1987 * @return True if last span is the first one we want to fill 1988 */ 1989 private boolean preferLastSpan(int layoutDir) { 1990 if (mOrientation == HORIZONTAL) { 1991 return (layoutDir == LayoutState.LAYOUT_START) != mShouldReverseLayout; 1992 } 1993 return ((layoutDir == LayoutState.LAYOUT_START) == mShouldReverseLayout) == isLayoutRTL(); 1994 } 1995 1996 /** 1997 * Finds the span for the next view. 1998 */ 1999 private Span getNextSpan(LayoutState layoutState) { 2000 final boolean preferLastSpan = preferLastSpan(layoutState.mLayoutDirection); 2001 final int startIndex, endIndex, diff; 2002 if (preferLastSpan) { 2003 startIndex = mSpanCount - 1; 2004 endIndex = -1; 2005 diff = -1; 2006 } else { 2007 startIndex = 0; 2008 endIndex = mSpanCount; 2009 diff = 1; 2010 } 2011 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { 2012 Span min = null; 2013 int minLine = Integer.MAX_VALUE; 2014 final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); 2015 for (int i = startIndex; i != endIndex; i += diff) { 2016 final Span other = mSpans[i]; 2017 int otherLine = other.getEndLine(defaultLine); 2018 if (otherLine < minLine) { 2019 min = other; 2020 minLine = otherLine; 2021 } 2022 } 2023 return min; 2024 } else { 2025 Span max = null; 2026 int maxLine = Integer.MIN_VALUE; 2027 final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); 2028 for (int i = startIndex; i != endIndex; i += diff) { 2029 final Span other = mSpans[i]; 2030 int otherLine = other.getStartLine(defaultLine); 2031 if (otherLine > maxLine) { 2032 max = other; 2033 maxLine = otherLine; 2034 } 2035 } 2036 return max; 2037 } 2038 } 2039 2040 @Override 2041 public boolean canScrollVertically() { 2042 return mOrientation == VERTICAL; 2043 } 2044 2045 @Override 2046 public boolean canScrollHorizontally() { 2047 return mOrientation == HORIZONTAL; 2048 } 2049 2050 @Override 2051 public boolean isLayoutReversed() { 2052 return mReverseLayout; 2053 } 2054 2055 @Override 2056 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 2057 RecyclerView.State state) { 2058 return scrollBy(dx, recycler, state); 2059 } 2060 2061 @Override 2062 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 2063 RecyclerView.State state) { 2064 return scrollBy(dy, recycler, state); 2065 } 2066 2067 private int calculateScrollDirectionForPosition(int position) { 2068 if (getChildCount() == 0) { 2069 return mShouldReverseLayout ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; 2070 } 2071 final int firstChildPos = getFirstChildPosition(); 2072 return position < firstChildPos != mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; 2073 } 2074 2075 @Override 2076 public PointF computeScrollVectorForPosition(int targetPosition) { 2077 final int direction = calculateScrollDirectionForPosition(targetPosition); 2078 PointF outVector = new PointF(); 2079 if (direction == 0) { 2080 return null; 2081 } 2082 if (mOrientation == HORIZONTAL) { 2083 outVector.x = direction; 2084 outVector.y = 0; 2085 } else { 2086 outVector.x = 0; 2087 outVector.y = direction; 2088 } 2089 return outVector; 2090 } 2091 2092 @Override 2093 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 2094 int position) { 2095 LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()); 2096 scroller.setTargetPosition(position); 2097 startSmoothScroll(scroller); 2098 } 2099 2100 @Override 2101 public void scrollToPosition(int position) { 2102 if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) { 2103 mPendingSavedState.invalidateAnchorPositionInfo(); 2104 } 2105 mPendingScrollPosition = position; 2106 mPendingScrollPositionOffset = INVALID_OFFSET; 2107 requestLayout(); 2108 } 2109 2110 /** 2111 * Scroll to the specified adapter position with the given offset from layout start. 2112 * <p> 2113 * Note that scroll position change will not be reflected until the next layout call. 2114 * <p> 2115 * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. 2116 * 2117 * @param position Index (starting at 0) of the reference item. 2118 * @param offset The distance (in pixels) between the start edge of the item view and 2119 * start edge of the RecyclerView. 2120 * @see #setReverseLayout(boolean) 2121 * @see #scrollToPosition(int) 2122 */ 2123 public void scrollToPositionWithOffset(int position, int offset) { 2124 if (mPendingSavedState != null) { 2125 mPendingSavedState.invalidateAnchorPositionInfo(); 2126 } 2127 mPendingScrollPosition = position; 2128 mPendingScrollPositionOffset = offset; 2129 requestLayout(); 2130 } 2131 2132 @Override 2133 @RestrictTo(LIBRARY) 2134 public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, 2135 LayoutPrefetchRegistry layoutPrefetchRegistry) { 2136 /* This method uses the simplifying assumption that the next N items (where N = span count) 2137 * will be assigned, one-to-one, to spans, where ordering is based on which span extends 2138 * least beyond the viewport. 2139 * 2140 * While this simplified model will be incorrect in some cases, it's difficult to know 2141 * item heights, or whether individual items will be full span prior to construction. 2142 * 2143 * While this greedy estimation approach may underestimate the distance to prefetch items, 2144 * it's very unlikely to overestimate them, so distances can be conservatively used to know 2145 * the soonest (in terms of scroll distance) a prefetched view may come on screen. 2146 */ 2147 int delta = (mOrientation == HORIZONTAL) ? dx : dy; 2148 if (getChildCount() == 0 || delta == 0) { 2149 // can't support this scroll, so don't bother prefetching 2150 return; 2151 } 2152 prepareLayoutStateForDelta(delta, state); 2153 2154 // build sorted list of distances to end of each span (though we don't care which is which) 2155 if (mPrefetchDistances == null || mPrefetchDistances.length < mSpanCount) { 2156 mPrefetchDistances = new int[mSpanCount]; 2157 } 2158 2159 int itemPrefetchCount = 0; 2160 for (int i = 0; i < mSpanCount; i++) { 2161 // compute number of pixels past the edge of the viewport that the current span extends 2162 int distance = mLayoutState.mItemDirection == LayoutState.LAYOUT_START 2163 ? mLayoutState.mStartLine - mSpans[i].getStartLine(mLayoutState.mStartLine) 2164 : mSpans[i].getEndLine(mLayoutState.mEndLine) - mLayoutState.mEndLine; 2165 if (distance >= 0) { 2166 // span extends to the edge, so prefetch next item 2167 mPrefetchDistances[itemPrefetchCount] = distance; 2168 itemPrefetchCount++; 2169 } 2170 } 2171 Arrays.sort(mPrefetchDistances, 0, itemPrefetchCount); 2172 2173 // then assign them in order to the next N views (where N = span count) 2174 for (int i = 0; i < itemPrefetchCount && mLayoutState.hasMore(state); i++) { 2175 layoutPrefetchRegistry.addPosition(mLayoutState.mCurrentPosition, 2176 mPrefetchDistances[i]); 2177 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 2178 } 2179 } 2180 2181 void prepareLayoutStateForDelta(int delta, RecyclerView.State state) { 2182 final int referenceChildPosition; 2183 final int layoutDir; 2184 if (delta > 0) { // layout towards end 2185 layoutDir = LayoutState.LAYOUT_END; 2186 referenceChildPosition = getLastChildPosition(); 2187 } else { 2188 layoutDir = LayoutState.LAYOUT_START; 2189 referenceChildPosition = getFirstChildPosition(); 2190 } 2191 mLayoutState.mRecycle = true; 2192 updateLayoutState(referenceChildPosition, state); 2193 setLayoutStateDirection(layoutDir); 2194 mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; 2195 mLayoutState.mAvailable = Math.abs(delta); 2196 } 2197 2198 int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { 2199 if (getChildCount() == 0 || dt == 0) { 2200 return 0; 2201 } 2202 2203 prepareLayoutStateForDelta(dt, state); 2204 int consumed = fill(recycler, mLayoutState, state); 2205 final int available = mLayoutState.mAvailable; 2206 final int totalScroll; 2207 if (available < consumed) { 2208 totalScroll = dt; 2209 } else if (dt < 0) { 2210 totalScroll = -consumed; 2211 } else { // dt > 0 2212 totalScroll = consumed; 2213 } 2214 if (DEBUG) { 2215 Log.d(TAG, "asked " + dt + " scrolled" + totalScroll); 2216 } 2217 2218 mPrimaryOrientation.offsetChildren(-totalScroll); 2219 // always reset this if we scroll for a proper save instance state 2220 mLastLayoutFromEnd = mShouldReverseLayout; 2221 mLayoutState.mAvailable = 0; 2222 recycle(recycler, mLayoutState); 2223 return totalScroll; 2224 } 2225 2226 int getLastChildPosition() { 2227 final int childCount = getChildCount(); 2228 return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1)); 2229 } 2230 2231 int getFirstChildPosition() { 2232 final int childCount = getChildCount(); 2233 return childCount == 0 ? 0 : getPosition(getChildAt(0)); 2234 } 2235 2236 /** 2237 * Finds the first View that can be used as an anchor View. 2238 * 2239 * @return Position of the View or 0 if it cannot find any such View. 2240 */ 2241 private int findFirstReferenceChildPosition(int itemCount) { 2242 final int limit = getChildCount(); 2243 for (int i = 0; i < limit; i++) { 2244 final View view = getChildAt(i); 2245 final int position = getPosition(view); 2246 if (position >= 0 && position < itemCount) { 2247 return position; 2248 } 2249 } 2250 return 0; 2251 } 2252 2253 /** 2254 * Finds the last View that can be used as an anchor View. 2255 * 2256 * @return Position of the View or 0 if it cannot find any such View. 2257 */ 2258 private int findLastReferenceChildPosition(int itemCount) { 2259 for (int i = getChildCount() - 1; i >= 0; i--) { 2260 final View view = getChildAt(i); 2261 final int position = getPosition(view); 2262 if (position >= 0 && position < itemCount) { 2263 return position; 2264 } 2265 } 2266 return 0; 2267 } 2268 2269 @SuppressWarnings("deprecation") 2270 @Override 2271 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 2272 if (mOrientation == HORIZONTAL) { 2273 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 2274 ViewGroup.LayoutParams.MATCH_PARENT); 2275 } else { 2276 return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2277 ViewGroup.LayoutParams.WRAP_CONTENT); 2278 } 2279 } 2280 2281 @Override 2282 public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 2283 return new LayoutParams(c, attrs); 2284 } 2285 2286 @Override 2287 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 2288 if (lp instanceof ViewGroup.MarginLayoutParams) { 2289 return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 2290 } else { 2291 return new LayoutParams(lp); 2292 } 2293 } 2294 2295 @Override 2296 public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 2297 return lp instanceof LayoutParams; 2298 } 2299 2300 public int getOrientation() { 2301 return mOrientation; 2302 } 2303 2304 @Override 2305 public @Nullable View onFocusSearchFailed(View focused, int direction, 2306 RecyclerView.Recycler recycler, RecyclerView.State state) { 2307 if (getChildCount() == 0) { 2308 return null; 2309 } 2310 2311 final View directChild = findContainingItemView(focused); 2312 if (directChild == null) { 2313 return null; 2314 } 2315 2316 resolveShouldLayoutReverse(); 2317 final int layoutDir = convertFocusDirectionToLayoutDirection(direction); 2318 if (layoutDir == LayoutState.INVALID_LAYOUT) { 2319 return null; 2320 } 2321 LayoutParams prevFocusLayoutParams = (LayoutParams) directChild.getLayoutParams(); 2322 boolean prevFocusFullSpan = prevFocusLayoutParams.mFullSpan; 2323 final Span prevFocusSpan = prevFocusLayoutParams.mSpan; 2324 final int referenceChildPosition; 2325 if (layoutDir == LayoutState.LAYOUT_END) { // layout towards end 2326 referenceChildPosition = getLastChildPosition(); 2327 } else { 2328 referenceChildPosition = getFirstChildPosition(); 2329 } 2330 updateLayoutState(referenceChildPosition, state); 2331 setLayoutStateDirection(layoutDir); 2332 2333 mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; 2334 mLayoutState.mAvailable = (int) (MAX_SCROLL_FACTOR * mPrimaryOrientation.getTotalSpace()); 2335 mLayoutState.mStopInFocusable = true; 2336 mLayoutState.mRecycle = false; 2337 fill(recycler, mLayoutState, state); 2338 mLastLayoutFromEnd = mShouldReverseLayout; 2339 if (!prevFocusFullSpan) { 2340 View view = prevFocusSpan.getFocusableViewAfter(referenceChildPosition, layoutDir); 2341 if (view != null && view != directChild) { 2342 return view; 2343 } 2344 } 2345 2346 // either could not find from the desired span or prev view is full span. 2347 // traverse all spans 2348 if (preferLastSpan(layoutDir)) { 2349 for (int i = mSpanCount - 1; i >= 0; i--) { 2350 View view = mSpans[i].getFocusableViewAfter(referenceChildPosition, layoutDir); 2351 if (view != null && view != directChild) { 2352 return view; 2353 } 2354 } 2355 } else { 2356 for (int i = 0; i < mSpanCount; i++) { 2357 View view = mSpans[i].getFocusableViewAfter(referenceChildPosition, layoutDir); 2358 if (view != null && view != directChild) { 2359 return view; 2360 } 2361 } 2362 } 2363 2364 // Could not find any focusable views from any of the existing spans. Now start the search 2365 // to find the best unfocusable candidate to become visible on the screen next. The search 2366 // is done in the same fashion: first, check the views in the desired span and if no 2367 // candidate is found, traverse the views in all the remaining spans. 2368 boolean shouldSearchFromStart = !mReverseLayout == (layoutDir == LayoutState.LAYOUT_START); 2369 View unfocusableCandidate = null; 2370 if (!prevFocusFullSpan) { 2371 unfocusableCandidate = findViewByPosition(shouldSearchFromStart 2372 ? prevFocusSpan.findFirstPartiallyVisibleItemPosition() : 2373 prevFocusSpan.findLastPartiallyVisibleItemPosition()); 2374 if (unfocusableCandidate != null && unfocusableCandidate != directChild) { 2375 return unfocusableCandidate; 2376 } 2377 } 2378 2379 if (preferLastSpan(layoutDir)) { 2380 for (int i = mSpanCount - 1; i >= 0; i--) { 2381 if (i == prevFocusSpan.mIndex) { 2382 continue; 2383 } 2384 unfocusableCandidate = findViewByPosition(shouldSearchFromStart 2385 ? mSpans[i].findFirstPartiallyVisibleItemPosition() : 2386 mSpans[i].findLastPartiallyVisibleItemPosition()); 2387 if (unfocusableCandidate != null && unfocusableCandidate != directChild) { 2388 return unfocusableCandidate; 2389 } 2390 } 2391 } else { 2392 for (int i = 0; i < mSpanCount; i++) { 2393 unfocusableCandidate = findViewByPosition(shouldSearchFromStart 2394 ? mSpans[i].findFirstPartiallyVisibleItemPosition() : 2395 mSpans[i].findLastPartiallyVisibleItemPosition()); 2396 if (unfocusableCandidate != null && unfocusableCandidate != directChild) { 2397 return unfocusableCandidate; 2398 } 2399 } 2400 } 2401 return null; 2402 } 2403 2404 /** 2405 * Converts a focusDirection to orientation. 2406 * 2407 * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, 2408 * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, 2409 * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} 2410 * or 0 for not applicable 2411 * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction 2412 * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise. 2413 */ 2414 private int convertFocusDirectionToLayoutDirection(int focusDirection) { 2415 switch (focusDirection) { 2416 case View.FOCUS_BACKWARD: 2417 if (mOrientation == VERTICAL) { 2418 return LayoutState.LAYOUT_START; 2419 } else if (isLayoutRTL()) { 2420 return LayoutState.LAYOUT_END; 2421 } else { 2422 return LayoutState.LAYOUT_START; 2423 } 2424 case View.FOCUS_FORWARD: 2425 if (mOrientation == VERTICAL) { 2426 return LayoutState.LAYOUT_END; 2427 } else if (isLayoutRTL()) { 2428 return LayoutState.LAYOUT_START; 2429 } else { 2430 return LayoutState.LAYOUT_END; 2431 } 2432 case View.FOCUS_UP: 2433 return mOrientation == VERTICAL ? LayoutState.LAYOUT_START 2434 : LayoutState.INVALID_LAYOUT; 2435 case View.FOCUS_DOWN: 2436 return mOrientation == VERTICAL ? LayoutState.LAYOUT_END 2437 : LayoutState.INVALID_LAYOUT; 2438 case View.FOCUS_LEFT: 2439 return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START 2440 : LayoutState.INVALID_LAYOUT; 2441 case View.FOCUS_RIGHT: 2442 return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END 2443 : LayoutState.INVALID_LAYOUT; 2444 default: 2445 if (DEBUG) { 2446 Log.d(TAG, "Unknown focus request:" + focusDirection); 2447 } 2448 return LayoutState.INVALID_LAYOUT; 2449 } 2450 2451 } 2452 2453 /** 2454 * LayoutParams used by StaggeredGridLayoutManager. 2455 * <p> 2456 * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the 2457 * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is 2458 * expected to fill all of the space given to it. 2459 */ 2460 public static class LayoutParams extends RecyclerView.LayoutParams { 2461 2462 /** 2463 * Span Id for Views that are not laid out yet. 2464 */ 2465 public static final int INVALID_SPAN_ID = -1; 2466 2467 // Package scope to be able to access from tests. 2468 Span mSpan; 2469 2470 boolean mFullSpan; 2471 2472 public LayoutParams(Context c, AttributeSet attrs) { 2473 super(c, attrs); 2474 } 2475 2476 public LayoutParams(int width, int height) { 2477 super(width, height); 2478 } 2479 2480 public LayoutParams(ViewGroup.MarginLayoutParams source) { 2481 super(source); 2482 } 2483 2484 public LayoutParams(ViewGroup.LayoutParams source) { 2485 super(source); 2486 } 2487 2488 public LayoutParams(RecyclerView.LayoutParams source) { 2489 super(source); 2490 } 2491 2492 /** 2493 * When set to true, the item will layout using all span area. That means, if orientation 2494 * is vertical, the view will have full width; if orientation is horizontal, the view will 2495 * have full height. 2496 * 2497 * @param fullSpan True if this item should traverse all spans. 2498 * @see #isFullSpan() 2499 */ 2500 public void setFullSpan(boolean fullSpan) { 2501 mFullSpan = fullSpan; 2502 } 2503 2504 /** 2505 * Returns whether this View occupies all available spans or just one. 2506 * 2507 * @return True if the View occupies all spans or false otherwise. 2508 * @see #setFullSpan(boolean) 2509 */ 2510 public boolean isFullSpan() { 2511 return mFullSpan; 2512 } 2513 2514 /** 2515 * Returns the Span index to which this View is assigned. 2516 * 2517 * @return The Span index of the View. If View is not yet assigned to any span, returns 2518 * {@link #INVALID_SPAN_ID}. 2519 */ 2520 public final int getSpanIndex() { 2521 if (mSpan == null) { 2522 return INVALID_SPAN_ID; 2523 } 2524 return mSpan.mIndex; 2525 } 2526 } 2527 2528 // Package scoped to access from tests. 2529 class Span { 2530 2531 static final int INVALID_LINE = Integer.MIN_VALUE; 2532 ArrayList<View> mViews = new ArrayList<>(); 2533 int mCachedStart = INVALID_LINE; 2534 int mCachedEnd = INVALID_LINE; 2535 int mDeletedSize = 0; 2536 final int mIndex; 2537 2538 Span(int index) { 2539 mIndex = index; 2540 } 2541 2542 int getStartLine(int def) { 2543 if (mCachedStart != INVALID_LINE) { 2544 return mCachedStart; 2545 } 2546 if (mViews.size() == 0) { 2547 return def; 2548 } 2549 calculateCachedStart(); 2550 return mCachedStart; 2551 } 2552 2553 void calculateCachedStart() { 2554 final View startView = mViews.get(0); 2555 final LayoutParams lp = getLayoutParams(startView); 2556 mCachedStart = mPrimaryOrientation.getDecoratedStart(startView); 2557 if (lp.mFullSpan) { 2558 LazySpanLookup.FullSpanItem fsi = mLazySpanLookup 2559 .getFullSpanItem(lp.getViewLayoutPosition()); 2560 if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_START) { 2561 mCachedStart -= fsi.getGapForSpan(mIndex); 2562 } 2563 } 2564 } 2565 2566 // Use this one when default value does not make sense and not having a value means a bug. 2567 int getStartLine() { 2568 if (mCachedStart != INVALID_LINE) { 2569 return mCachedStart; 2570 } 2571 calculateCachedStart(); 2572 return mCachedStart; 2573 } 2574 2575 int getEndLine(int def) { 2576 if (mCachedEnd != INVALID_LINE) { 2577 return mCachedEnd; 2578 } 2579 final int size = mViews.size(); 2580 if (size == 0) { 2581 return def; 2582 } 2583 calculateCachedEnd(); 2584 return mCachedEnd; 2585 } 2586 2587 void calculateCachedEnd() { 2588 final View endView = mViews.get(mViews.size() - 1); 2589 final LayoutParams lp = getLayoutParams(endView); 2590 mCachedEnd = mPrimaryOrientation.getDecoratedEnd(endView); 2591 if (lp.mFullSpan) { 2592 LazySpanLookup.FullSpanItem fsi = mLazySpanLookup 2593 .getFullSpanItem(lp.getViewLayoutPosition()); 2594 if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_END) { 2595 mCachedEnd += fsi.getGapForSpan(mIndex); 2596 } 2597 } 2598 } 2599 2600 // Use this one when default value does not make sense and not having a value means a bug. 2601 int getEndLine() { 2602 if (mCachedEnd != INVALID_LINE) { 2603 return mCachedEnd; 2604 } 2605 calculateCachedEnd(); 2606 return mCachedEnd; 2607 } 2608 2609 void prependToSpan(View view) { 2610 LayoutParams lp = getLayoutParams(view); 2611 lp.mSpan = this; 2612 mViews.add(0, view); 2613 mCachedStart = INVALID_LINE; 2614 if (mViews.size() == 1) { 2615 mCachedEnd = INVALID_LINE; 2616 } 2617 if (lp.isItemRemoved() || lp.isItemChanged()) { 2618 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 2619 } 2620 } 2621 2622 void appendToSpan(View view) { 2623 LayoutParams lp = getLayoutParams(view); 2624 lp.mSpan = this; 2625 mViews.add(view); 2626 mCachedEnd = INVALID_LINE; 2627 if (mViews.size() == 1) { 2628 mCachedStart = INVALID_LINE; 2629 } 2630 if (lp.isItemRemoved() || lp.isItemChanged()) { 2631 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 2632 } 2633 } 2634 2635 // Useful method to preserve positions on a re-layout. 2636 void cacheReferenceLineAndClear(boolean reverseLayout, int offset) { 2637 int reference; 2638 if (reverseLayout) { 2639 reference = getEndLine(INVALID_LINE); 2640 } else { 2641 reference = getStartLine(INVALID_LINE); 2642 } 2643 clear(); 2644 if (reference == INVALID_LINE) { 2645 return; 2646 } 2647 if ((reverseLayout && reference < mPrimaryOrientation.getEndAfterPadding()) 2648 || (!reverseLayout && reference > mPrimaryOrientation.getStartAfterPadding())) { 2649 return; 2650 } 2651 if (offset != INVALID_OFFSET) { 2652 reference += offset; 2653 } 2654 mCachedStart = mCachedEnd = reference; 2655 } 2656 2657 void clear() { 2658 mViews.clear(); 2659 invalidateCache(); 2660 mDeletedSize = 0; 2661 } 2662 2663 void invalidateCache() { 2664 mCachedStart = INVALID_LINE; 2665 mCachedEnd = INVALID_LINE; 2666 } 2667 2668 void setLine(int line) { 2669 mCachedEnd = mCachedStart = line; 2670 } 2671 2672 void popEnd() { 2673 final int size = mViews.size(); 2674 View end = mViews.remove(size - 1); 2675 final LayoutParams lp = getLayoutParams(end); 2676 lp.mSpan = null; 2677 if (lp.isItemRemoved() || lp.isItemChanged()) { 2678 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end); 2679 } 2680 if (size == 1) { 2681 mCachedStart = INVALID_LINE; 2682 } 2683 mCachedEnd = INVALID_LINE; 2684 } 2685 2686 void popStart() { 2687 View start = mViews.remove(0); 2688 final LayoutParams lp = getLayoutParams(start); 2689 lp.mSpan = null; 2690 if (mViews.size() == 0) { 2691 mCachedEnd = INVALID_LINE; 2692 } 2693 if (lp.isItemRemoved() || lp.isItemChanged()) { 2694 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start); 2695 } 2696 mCachedStart = INVALID_LINE; 2697 } 2698 2699 public int getDeletedSize() { 2700 return mDeletedSize; 2701 } 2702 2703 LayoutParams getLayoutParams(View view) { 2704 return (LayoutParams) view.getLayoutParams(); 2705 } 2706 2707 void onOffset(int dt) { 2708 if (mCachedStart != INVALID_LINE) { 2709 mCachedStart += dt; 2710 } 2711 if (mCachedEnd != INVALID_LINE) { 2712 mCachedEnd += dt; 2713 } 2714 } 2715 2716 public int findFirstVisibleItemPosition() { 2717 return mReverseLayout 2718 ? findOneVisibleChild(mViews.size() - 1, -1, false) 2719 : findOneVisibleChild(0, mViews.size(), false); 2720 } 2721 2722 public int findFirstPartiallyVisibleItemPosition() { 2723 return mReverseLayout 2724 ? findOnePartiallyVisibleChild(mViews.size() - 1, -1, true) 2725 : findOnePartiallyVisibleChild(0, mViews.size(), true); 2726 } 2727 2728 public int findFirstCompletelyVisibleItemPosition() { 2729 return mReverseLayout 2730 ? findOneVisibleChild(mViews.size() - 1, -1, true) 2731 : findOneVisibleChild(0, mViews.size(), true); 2732 } 2733 2734 public int findLastVisibleItemPosition() { 2735 return mReverseLayout 2736 ? findOneVisibleChild(0, mViews.size(), false) 2737 : findOneVisibleChild(mViews.size() - 1, -1, false); 2738 } 2739 2740 public int findLastPartiallyVisibleItemPosition() { 2741 return mReverseLayout 2742 ? findOnePartiallyVisibleChild(0, mViews.size(), true) 2743 : findOnePartiallyVisibleChild(mViews.size() - 1, -1, true); 2744 } 2745 2746 public int findLastCompletelyVisibleItemPosition() { 2747 return mReverseLayout 2748 ? findOneVisibleChild(0, mViews.size(), true) 2749 : findOneVisibleChild(mViews.size() - 1, -1, true); 2750 } 2751 2752 /** 2753 * Returns the first view within this span that is partially or fully visible. Partially 2754 * visible refers to a view that overlaps but is not fully contained within RV's padded 2755 * bounded area. This view returned can be defined to have an area of overlap strictly 2756 * greater than zero if acceptEndPointInclusion is false. If true, the view's endpoint 2757 * inclusion is enough to consider it partially visible. The latter case can then refer to 2758 * an out-of-bounds view positioned right at the top (or bottom) boundaries of RV's padded 2759 * area. This is used e.g. inside 2760 * {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} for 2761 * calculating the next unfocusable child to become visible on the screen. 2762 * @param fromIndex The child position index to start the search from. 2763 * @param toIndex The child position index to end the search at. 2764 * @param completelyVisible True if we have to only consider completely visible views, 2765 * false otherwise. 2766 * @param acceptCompletelyVisible True if we can consider both partially or fully visible 2767 * views, false, if only a partially visible child should be 2768 * returned. 2769 * @param acceptEndPointInclusion If the view's endpoint intersection with RV's padded 2770 * bounded area is enough to consider it partially visible, 2771 * false otherwise 2772 * @return The adapter position of the first view that's either partially or fully visible. 2773 * {@link RecyclerView#NO_POSITION} if no such view is found. 2774 */ 2775 int findOnePartiallyOrCompletelyVisibleChild(int fromIndex, int toIndex, 2776 boolean completelyVisible, 2777 boolean acceptCompletelyVisible, 2778 boolean acceptEndPointInclusion) { 2779 final int start = mPrimaryOrientation.getStartAfterPadding(); 2780 final int end = mPrimaryOrientation.getEndAfterPadding(); 2781 final int next = toIndex > fromIndex ? 1 : -1; 2782 for (int i = fromIndex; i != toIndex; i += next) { 2783 final View child = mViews.get(i); 2784 final int childStart = mPrimaryOrientation.getDecoratedStart(child); 2785 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); 2786 boolean childStartInclusion = acceptEndPointInclusion ? (childStart <= end) 2787 : (childStart < end); 2788 boolean childEndInclusion = acceptEndPointInclusion ? (childEnd >= start) 2789 : (childEnd > start); 2790 if (childStartInclusion && childEndInclusion) { 2791 if (completelyVisible && acceptCompletelyVisible) { 2792 // the child has to be completely visible to be returned. 2793 if (childStart >= start && childEnd <= end) { 2794 return getPosition(child); 2795 } 2796 } else if (acceptCompletelyVisible) { 2797 // can return either a partially or completely visible child. 2798 return getPosition(child); 2799 } else if (childStart < start || childEnd > end) { 2800 // should return a partially visible child if exists and a completely 2801 // visible child is not acceptable in this case. 2802 return getPosition(child); 2803 } 2804 } 2805 } 2806 return RecyclerView.NO_POSITION; 2807 } 2808 2809 int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { 2810 return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, completelyVisible, 2811 true, false); 2812 } 2813 2814 int findOnePartiallyVisibleChild(int fromIndex, int toIndex, 2815 boolean acceptEndPointInclusion) { 2816 return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, false, false, 2817 acceptEndPointInclusion); 2818 } 2819 2820 /** 2821 * Depending on the layout direction, returns the View that is after the given position. 2822 */ 2823 public View getFocusableViewAfter(int referenceChildPosition, int layoutDir) { 2824 View candidate = null; 2825 if (layoutDir == LayoutState.LAYOUT_START) { 2826 final int limit = mViews.size(); 2827 for (int i = 0; i < limit; i++) { 2828 final View view = mViews.get(i); 2829 if ((mReverseLayout && getPosition(view) <= referenceChildPosition) 2830 || (!mReverseLayout && getPosition(view) >= referenceChildPosition)) { 2831 break; 2832 } 2833 if (view.hasFocusable()) { 2834 candidate = view; 2835 } else { 2836 break; 2837 } 2838 } 2839 } else { 2840 for (int i = mViews.size() - 1; i >= 0; i--) { 2841 final View view = mViews.get(i); 2842 if ((mReverseLayout && getPosition(view) >= referenceChildPosition) 2843 || (!mReverseLayout && getPosition(view) <= referenceChildPosition)) { 2844 break; 2845 } 2846 if (view.hasFocusable()) { 2847 candidate = view; 2848 } else { 2849 break; 2850 } 2851 } 2852 } 2853 return candidate; 2854 } 2855 } 2856 2857 /** 2858 * An array of mappings from adapter position to span. 2859 * This only grows when a write happens and it grows up to the size of the adapter. 2860 */ 2861 static class LazySpanLookup { 2862 2863 private static final int MIN_SIZE = 10; 2864 int[] mData; 2865 List<FullSpanItem> mFullSpanItems; 2866 2867 2868 /** 2869 * Invalidates everything after this position, including full span information 2870 */ 2871 int forceInvalidateAfter(int position) { 2872 if (mFullSpanItems != null) { 2873 for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { 2874 FullSpanItem fsi = mFullSpanItems.get(i); 2875 if (fsi.mPosition >= position) { 2876 mFullSpanItems.remove(i); 2877 } 2878 } 2879 } 2880 return invalidateAfter(position); 2881 } 2882 2883 /** 2884 * returns end position for invalidation. 2885 */ 2886 int invalidateAfter(int position) { 2887 if (mData == null) { 2888 return RecyclerView.NO_POSITION; 2889 } 2890 if (position >= mData.length) { 2891 return RecyclerView.NO_POSITION; 2892 } 2893 int endPosition = invalidateFullSpansAfter(position); 2894 if (endPosition == RecyclerView.NO_POSITION) { 2895 Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID); 2896 return mData.length; 2897 } else { 2898 // Just invalidate items in between `position` and the next full span item, or the 2899 // end of the tracked spans in mData if it's not been lengthened yet. 2900 final int invalidateToIndex = Math.min(endPosition + 1, mData.length); 2901 Arrays.fill(mData, position, invalidateToIndex, LayoutParams.INVALID_SPAN_ID); 2902 return invalidateToIndex; 2903 } 2904 } 2905 2906 int getSpan(int position) { 2907 if (mData == null || position >= mData.length) { 2908 return LayoutParams.INVALID_SPAN_ID; 2909 } else { 2910 return mData[position]; 2911 } 2912 } 2913 2914 void setSpan(int position, Span span) { 2915 ensureSize(position); 2916 mData[position] = span.mIndex; 2917 } 2918 2919 int sizeForPosition(int position) { 2920 int len = mData.length; 2921 while (len <= position) { 2922 len *= 2; 2923 } 2924 return len; 2925 } 2926 2927 void ensureSize(int position) { 2928 if (mData == null) { 2929 mData = new int[Math.max(position, MIN_SIZE) + 1]; 2930 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 2931 } else if (position >= mData.length) { 2932 int[] old = mData; 2933 mData = new int[sizeForPosition(position)]; 2934 System.arraycopy(old, 0, mData, 0, old.length); 2935 Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID); 2936 } 2937 } 2938 2939 void clear() { 2940 if (mData != null) { 2941 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 2942 } 2943 mFullSpanItems = null; 2944 } 2945 2946 void offsetForRemoval(int positionStart, int itemCount) { 2947 if (mData == null || positionStart >= mData.length) { 2948 return; 2949 } 2950 ensureSize(positionStart + itemCount); 2951 System.arraycopy(mData, positionStart + itemCount, mData, positionStart, 2952 mData.length - positionStart - itemCount); 2953 Arrays.fill(mData, mData.length - itemCount, mData.length, 2954 LayoutParams.INVALID_SPAN_ID); 2955 offsetFullSpansForRemoval(positionStart, itemCount); 2956 } 2957 2958 private void offsetFullSpansForRemoval(int positionStart, int itemCount) { 2959 if (mFullSpanItems == null) { 2960 return; 2961 } 2962 final int end = positionStart + itemCount; 2963 for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { 2964 FullSpanItem fsi = mFullSpanItems.get(i); 2965 if (fsi.mPosition < positionStart) { 2966 continue; 2967 } 2968 if (fsi.mPosition < end) { 2969 mFullSpanItems.remove(i); 2970 } else { 2971 fsi.mPosition -= itemCount; 2972 } 2973 } 2974 } 2975 2976 void offsetForAddition(int positionStart, int itemCount) { 2977 if (mData == null || positionStart >= mData.length) { 2978 return; 2979 } 2980 ensureSize(positionStart + itemCount); 2981 System.arraycopy(mData, positionStart, mData, positionStart + itemCount, 2982 mData.length - positionStart - itemCount); 2983 Arrays.fill(mData, positionStart, positionStart + itemCount, 2984 LayoutParams.INVALID_SPAN_ID); 2985 offsetFullSpansForAddition(positionStart, itemCount); 2986 } 2987 2988 private void offsetFullSpansForAddition(int positionStart, int itemCount) { 2989 if (mFullSpanItems == null) { 2990 return; 2991 } 2992 for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { 2993 FullSpanItem fsi = mFullSpanItems.get(i); 2994 if (fsi.mPosition < positionStart) { 2995 continue; 2996 } 2997 fsi.mPosition += itemCount; 2998 } 2999 } 3000 3001 /** 3002 * Returns when invalidation should end. e.g. hitting a full span position. 3003 * Returned position SHOULD BE invalidated. 3004 */ 3005 private int invalidateFullSpansAfter(int position) { 3006 if (mFullSpanItems == null) { 3007 return RecyclerView.NO_POSITION; 3008 } 3009 final FullSpanItem item = getFullSpanItem(position); 3010 // if there is an fsi at this position, get rid of it. 3011 if (item != null) { 3012 mFullSpanItems.remove(item); 3013 } 3014 int nextFsiIndex = -1; 3015 final int count = mFullSpanItems.size(); 3016 for (int i = 0; i < count; i++) { 3017 FullSpanItem fsi = mFullSpanItems.get(i); 3018 if (fsi.mPosition >= position) { 3019 nextFsiIndex = i; 3020 break; 3021 } 3022 } 3023 if (nextFsiIndex != -1) { 3024 FullSpanItem fsi = mFullSpanItems.get(nextFsiIndex); 3025 mFullSpanItems.remove(nextFsiIndex); 3026 return fsi.mPosition; 3027 } 3028 return RecyclerView.NO_POSITION; 3029 } 3030 3031 public void addFullSpanItem(FullSpanItem fullSpanItem) { 3032 if (mFullSpanItems == null) { 3033 mFullSpanItems = new ArrayList<>(); 3034 } 3035 final int size = mFullSpanItems.size(); 3036 for (int i = 0; i < size; i++) { 3037 FullSpanItem other = mFullSpanItems.get(i); 3038 if (other.mPosition == fullSpanItem.mPosition) { 3039 if (DEBUG) { 3040 throw new IllegalStateException("two fsis for same position"); 3041 } else { 3042 mFullSpanItems.remove(i); 3043 } 3044 } 3045 if (other.mPosition >= fullSpanItem.mPosition) { 3046 mFullSpanItems.add(i, fullSpanItem); 3047 return; 3048 } 3049 } 3050 // if it is not added to a position. 3051 mFullSpanItems.add(fullSpanItem); 3052 } 3053 3054 public FullSpanItem getFullSpanItem(int position) { 3055 if (mFullSpanItems == null) { 3056 return null; 3057 } 3058 for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { 3059 final FullSpanItem fsi = mFullSpanItems.get(i); 3060 if (fsi.mPosition == position) { 3061 return fsi; 3062 } 3063 } 3064 return null; 3065 } 3066 3067 /** 3068 * @param minPos inclusive 3069 * @param maxPos exclusive 3070 * @param gapDir if not 0, returns FSIs on in that direction 3071 * @param hasUnwantedGapAfter If true, when full span item has unwanted gaps, it will be 3072 * returned even if its gap direction does not match. 3073 */ 3074 public FullSpanItem getFirstFullSpanItemInRange(int minPos, int maxPos, int gapDir, 3075 boolean hasUnwantedGapAfter) { 3076 if (mFullSpanItems == null) { 3077 return null; 3078 } 3079 final int limit = mFullSpanItems.size(); 3080 for (int i = 0; i < limit; i++) { 3081 FullSpanItem fsi = mFullSpanItems.get(i); 3082 if (fsi.mPosition >= maxPos) { 3083 return null; 3084 } 3085 if (fsi.mPosition >= minPos 3086 && (gapDir == 0 || fsi.mGapDir == gapDir 3087 || (hasUnwantedGapAfter && fsi.mHasUnwantedGapAfter))) { 3088 return fsi; 3089 } 3090 } 3091 return null; 3092 } 3093 3094 /** 3095 * We keep information about full span items because they may create gaps in the UI. 3096 */ 3097 @SuppressLint("BanParcelableUsage") 3098 static class FullSpanItem implements Parcelable { 3099 3100 int mPosition; 3101 int mGapDir; 3102 int[] mGapPerSpan; 3103 // A full span may be laid out in primary direction but may have gaps due to 3104 // invalidation of views after it. This is recorded during a reverse scroll and if 3105 // view is still on the screen after scroll stops, we have to recalculate layout 3106 boolean mHasUnwantedGapAfter; 3107 3108 FullSpanItem(Parcel in) { 3109 mPosition = in.readInt(); 3110 mGapDir = in.readInt(); 3111 mHasUnwantedGapAfter = in.readInt() == 1; 3112 int spanCount = in.readInt(); 3113 if (spanCount > 0) { 3114 mGapPerSpan = new int[spanCount]; 3115 in.readIntArray(mGapPerSpan); 3116 } 3117 } 3118 3119 FullSpanItem() { 3120 } 3121 3122 int getGapForSpan(int spanIndex) { 3123 return mGapPerSpan == null ? 0 : mGapPerSpan[spanIndex]; 3124 } 3125 3126 @Override 3127 public int describeContents() { 3128 return 0; 3129 } 3130 3131 @Override 3132 public void writeToParcel(Parcel dest, int flags) { 3133 dest.writeInt(mPosition); 3134 dest.writeInt(mGapDir); 3135 dest.writeInt(mHasUnwantedGapAfter ? 1 : 0); 3136 if (mGapPerSpan != null && mGapPerSpan.length > 0) { 3137 dest.writeInt(mGapPerSpan.length); 3138 dest.writeIntArray(mGapPerSpan); 3139 } else { 3140 dest.writeInt(0); 3141 } 3142 } 3143 3144 @Override 3145 public String toString() { 3146 return "FullSpanItem{" 3147 + "mPosition=" + mPosition 3148 + ", mGapDir=" + mGapDir 3149 + ", mHasUnwantedGapAfter=" + mHasUnwantedGapAfter 3150 + ", mGapPerSpan=" + Arrays.toString(mGapPerSpan) 3151 + '}'; 3152 } 3153 3154 public static final Parcelable.Creator<FullSpanItem> CREATOR = 3155 new Parcelable.Creator<FullSpanItem>() { 3156 @Override 3157 public FullSpanItem createFromParcel(Parcel in) { 3158 return new FullSpanItem(in); 3159 } 3160 3161 @Override 3162 public FullSpanItem[] newArray(int size) { 3163 return new FullSpanItem[size]; 3164 } 3165 }; 3166 } 3167 } 3168 3169 /** 3170 */ 3171 @RestrictTo(LIBRARY) 3172 @SuppressLint("BanParcelableUsage") 3173 public static class SavedState implements Parcelable { 3174 3175 int mAnchorPosition; 3176 int mVisibleAnchorPosition; // Replacement for span info when spans are invalidated 3177 int mSpanOffsetsSize; 3178 int[] mSpanOffsets; 3179 int mSpanLookupSize; 3180 int[] mSpanLookup; 3181 List<LazySpanLookup.FullSpanItem> mFullSpanItems; 3182 boolean mReverseLayout; 3183 boolean mAnchorLayoutFromEnd; 3184 boolean mLastLayoutRTL; 3185 3186 public SavedState() { 3187 } 3188 3189 SavedState(Parcel in) { 3190 mAnchorPosition = in.readInt(); 3191 mVisibleAnchorPosition = in.readInt(); 3192 mSpanOffsetsSize = in.readInt(); 3193 if (mSpanOffsetsSize > 0) { 3194 mSpanOffsets = new int[mSpanOffsetsSize]; 3195 in.readIntArray(mSpanOffsets); 3196 } 3197 3198 mSpanLookupSize = in.readInt(); 3199 if (mSpanLookupSize > 0) { 3200 mSpanLookup = new int[mSpanLookupSize]; 3201 in.readIntArray(mSpanLookup); 3202 } 3203 mReverseLayout = in.readInt() == 1; 3204 mAnchorLayoutFromEnd = in.readInt() == 1; 3205 mLastLayoutRTL = in.readInt() == 1; 3206 @SuppressWarnings("unchecked") 3207 List<LazySpanLookup.FullSpanItem> fullSpanItems = 3208 in.readArrayList(LazySpanLookup.FullSpanItem.class.getClassLoader()); 3209 mFullSpanItems = fullSpanItems; 3210 } 3211 3212 public SavedState(SavedState other) { 3213 mSpanOffsetsSize = other.mSpanOffsetsSize; 3214 mAnchorPosition = other.mAnchorPosition; 3215 mVisibleAnchorPosition = other.mVisibleAnchorPosition; 3216 mSpanOffsets = other.mSpanOffsets; 3217 mSpanLookupSize = other.mSpanLookupSize; 3218 mSpanLookup = other.mSpanLookup; 3219 mReverseLayout = other.mReverseLayout; 3220 mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; 3221 mLastLayoutRTL = other.mLastLayoutRTL; 3222 mFullSpanItems = other.mFullSpanItems; 3223 } 3224 3225 void invalidateSpanInfo() { 3226 mSpanOffsets = null; 3227 mSpanOffsetsSize = 0; 3228 mSpanLookupSize = 0; 3229 mSpanLookup = null; 3230 mFullSpanItems = null; 3231 } 3232 3233 void invalidateAnchorPositionInfo() { 3234 mSpanOffsets = null; 3235 mSpanOffsetsSize = 0; 3236 mAnchorPosition = RecyclerView.NO_POSITION; 3237 mVisibleAnchorPosition = RecyclerView.NO_POSITION; 3238 } 3239 3240 @Override 3241 public int describeContents() { 3242 return 0; 3243 } 3244 3245 @Override 3246 public void writeToParcel(Parcel dest, int flags) { 3247 dest.writeInt(mAnchorPosition); 3248 dest.writeInt(mVisibleAnchorPosition); 3249 dest.writeInt(mSpanOffsetsSize); 3250 if (mSpanOffsetsSize > 0) { 3251 dest.writeIntArray(mSpanOffsets); 3252 } 3253 dest.writeInt(mSpanLookupSize); 3254 if (mSpanLookupSize > 0) { 3255 dest.writeIntArray(mSpanLookup); 3256 } 3257 dest.writeInt(mReverseLayout ? 1 : 0); 3258 dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); 3259 dest.writeInt(mLastLayoutRTL ? 1 : 0); 3260 dest.writeList(mFullSpanItems); 3261 } 3262 3263 public static final Parcelable.Creator<SavedState> CREATOR = 3264 new Parcelable.Creator<SavedState>() { 3265 @Override 3266 public SavedState createFromParcel(Parcel in) { 3267 return new SavedState(in); 3268 } 3269 3270 @Override 3271 public SavedState[] newArray(int size) { 3272 return new SavedState[size]; 3273 } 3274 }; 3275 } 3276 3277 /** 3278 * Data class to hold the information about an anchor position which is used in onLayout call. 3279 */ 3280 class AnchorInfo { 3281 3282 int mPosition; 3283 int mOffset; 3284 boolean mLayoutFromEnd; 3285 boolean mInvalidateOffsets; 3286 boolean mValid; 3287 // this is where we save span reference lines in case we need to re-use them for multi-pass 3288 // measure steps 3289 int[] mSpanReferenceLines; 3290 3291 AnchorInfo() { 3292 reset(); 3293 } 3294 3295 void reset() { 3296 mPosition = RecyclerView.NO_POSITION; 3297 mOffset = INVALID_OFFSET; 3298 mLayoutFromEnd = false; 3299 mInvalidateOffsets = false; 3300 mValid = false; 3301 if (mSpanReferenceLines != null) { 3302 Arrays.fill(mSpanReferenceLines, -1); 3303 } 3304 } 3305 3306 void saveSpanReferenceLines(Span[] spans) { 3307 int spanCount = spans.length; 3308 if (mSpanReferenceLines == null || mSpanReferenceLines.length < spanCount) { 3309 mSpanReferenceLines = new int[mSpans.length]; 3310 } 3311 for (int i = 0; i < spanCount; i++) { 3312 // does not matter start or end since this is only recorded when span is reset 3313 mSpanReferenceLines[i] = spans[i].getStartLine(Span.INVALID_LINE); 3314 } 3315 } 3316 3317 void assignCoordinateFromPadding() { 3318 mOffset = mLayoutFromEnd ? mPrimaryOrientation.getEndAfterPadding() 3319 : mPrimaryOrientation.getStartAfterPadding(); 3320 } 3321 3322 void assignCoordinateFromPadding(int addedDistance) { 3323 if (mLayoutFromEnd) { 3324 mOffset = mPrimaryOrientation.getEndAfterPadding() - addedDistance; 3325 } else { 3326 mOffset = mPrimaryOrientation.getStartAfterPadding() + addedDistance; 3327 } 3328 } 3329 } 3330 } 3331