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