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