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