StaggeredGridLayoutManager.java revision 6b4d950d0d1e26165a1e643a2fd1fe4e283786f1
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 android.content.Context; 20import android.graphics.PointF; 21import android.graphics.Rect; 22import android.os.Parcel; 23import android.os.Parcelable; 24import android.support.v4.view.ViewCompat; 25import android.util.AttributeSet; 26import android.util.Log; 27import android.view.View; 28import android.view.ViewGroup; 29 30 31import java.util.ArrayList; 32import java.util.Arrays; 33import java.util.BitSet; 34 35 36import static android.support.v7.widget.LayoutState.LAYOUT_START; 37import static android.support.v7.widget.LayoutState.LAYOUT_END; 38import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD; 39import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL; 40/** 41 * A LayoutManager that lays out children in a staggered grid formation. 42 * It supports horizontal & vertical layout as well as an ability to layout children in reverse. 43 * <p> 44 * Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps, 45 * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can 46 * control this behavior via {@link #setGapStrategy(int)}. 47 */ 48public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager { 49 50 public static final String TAG = "StaggeredGridLayoutManager"; 51 52 private static final boolean DEBUG = false; 53 54 public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; 55 56 public static final int VERTICAL = OrientationHelper.VERTICAL; 57 58 /** 59 * Does not do anything to hide gaps 60 */ 61 public static final int GAP_HANDLING_NONE = 0; 62 63 /** 64 * Scroll the shorter span slower to avoid gaps in the UI. 65 * <p> 66 * For example, if LayoutManager ends up with the following layout: 67 * <code> 68 * BXC 69 * DEF 70 * </code> 71 * Where B has two spans height, if user scrolls down it will keep the positions of 2nd and 3rd 72 * columns, 73 * which will result in: 74 * <code> 75 * BXC 76 * BEF 77 * </code> 78 * instead of 79 * <code> 80 * B 81 * BEF 82 * </code> 83 */ 84 public static final int GAP_HANDLING_LAZY = 1; 85 86 /** 87 * On scroll, LayoutManager checks for a view that is assigned to wrong span. 88 * When such a situation is detected, LayoutManager will wait until scroll is complete and then 89 * move children to their correct spans. 90 * <p> 91 * For example, if LayoutManager ends up with the following layout due to adapter changes: 92 * <code> 93 * AAA 94 * _BC 95 * DDD 96 * </code> 97 * It will animate to the following state: 98 * <code> 99 * AAA 100 * BC_ 101 * DDD 102 * </code> 103 */ 104 public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2; 105 106 private static final int INVALID_OFFSET = Integer.MIN_VALUE; 107 108 /** 109 * Number of spans 110 */ 111 private int mSpanCount = -1; 112 113 private Span[] mSpans; 114 115 /** 116 * Primary orientation is the layout's orientation, secondary orientation is the orientation 117 * for spans. Having both makes code much cleaner for calculations. 118 */ 119 OrientationHelper mPrimaryOrientation; 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 private LayoutState mLayoutState; 130 131 private boolean mReverseLayout = false; 132 133 /** 134 * Aggregated reverse layout value that takes RTL into account. 135 */ 136 private boolean mShouldReverseLayout = false; 137 138 /** 139 * Temporary variable used during fill method to check which spans needs to be filled. 140 */ 141 private BitSet mRemainingSpans; 142 143 /** 144 * When LayoutManager needs to scroll to a position, it sets this variable and requests a 145 * layout which will check this variable and re-layout accordingly. 146 */ 147 int mPendingScrollPosition = RecyclerView.NO_POSITION; 148 149 /** 150 * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is 151 * called. 152 */ 153 int mPendingScrollPositionOffset = INVALID_OFFSET; 154 155 /** 156 * Keeps the mapping between the adapter positions and spans. This is necessary to provide 157 * a consistent experience when user scrolls the list. 158 */ 159 LazySpanLookup mLazySpanLookup = new LazySpanLookup(); 160 161 /** 162 * how we handle gaps in UI. 163 */ 164 private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 165 166 /** 167 * Saved state needs this information to properly layout on restore. 168 */ 169 private boolean mLastLayoutFromEnd; 170 171 /** 172 * SavedState is not handled until a layout happens. This is where we keep it until next 173 * layout. 174 */ 175 private SavedState mPendingSavedState; 176 177 /** 178 * If LayoutManager detects an unwanted gap in the layout, it sets this flag which will trigger 179 * a runnable after scrolling ends and will re-check. If invalid view state is still present, 180 * it will request a layout to fix it. 181 */ 182 private boolean mHasGaps; 183 184 /** 185 * Creates a StaggeredGridLayoutManager with given parameters. 186 * 187 * @param spanCount If orientation is vertical, spanCount is number of columns. If 188 * orientation is horizontal, spanCount is number of rows. 189 * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL} 190 */ 191 public StaggeredGridLayoutManager(int spanCount, int orientation) { 192 mOrientation = orientation; 193 setSpanCount(spanCount); 194 } 195 196 @Override 197 public void onScrollStateChanged(int state) { 198 if (state == RecyclerView.SCROLL_STATE_IDLE && mHasGaps) { 199 // re-check for gaps 200 View gapView = hasGapsToFix(0, getChildCount()); 201 if (gapView == null) { 202 mHasGaps = false; // yay, gap disappeared :) 203 // We should invalidate positions after the last visible child. No reason to 204 // re-layout. 205 final int lastVisiblePosition = mShouldReverseLayout ? getFirstChildPosition() 206 : getLastChildPosition(); 207 mLazySpanLookup.invalidateAfter(lastVisiblePosition + 1); 208 } else { 209 mLazySpanLookup.invalidateAfter(getPosition(gapView)); 210 requestSimpleAnimationsInNextLayout(); 211 requestLayout(); // Trigger a re-layout which will fix the layout assignments. 212 } 213 } 214 } 215 216 /** 217 * Sets the number of spans for the layout. This will invalidate all of the span assignments 218 * for Views. 219 * <p> 220 * Calling this method will automatically result in a new layout request unless the spanCount 221 * parameter is equal to current span count. 222 * 223 * @param spanCount Number of spans to layout 224 */ 225 public void setSpanCount(int spanCount) { 226 assertNotInLayoutOrScroll(null); 227 if (mPendingSavedState != null && mPendingSavedState.mSpanCount != spanCount) { 228 // invalidate span info in saved state 229 mPendingSavedState.invalidateSpanInfo(); 230 mPendingSavedState.mSpanCount = spanCount; 231 mPendingSavedState.mAnchorPosition = mPendingSavedState.mVisibleAnchorPosition; 232 } 233 if (spanCount != mSpanCount) { 234 invalidateSpanAssignments(); 235 mSpanCount = spanCount; 236 mRemainingSpans = new BitSet(mSpanCount); 237 mSpans = new Span[mSpanCount]; 238 for (int i = 0; i < mSpanCount; i++) { 239 mSpans[i] = new Span(i); 240 } 241 requestLayout(); 242 } 243 } 244 245 /** 246 * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep 247 * scroll position. 248 * 249 * @param orientation {@link OrientationHelper#HORIZONTAL} or {@link OrientationHelper#VERTICAL} 250 */ 251 public void setOrientation(int orientation) { 252 if (orientation != HORIZONTAL && orientation != VERTICAL) { 253 throw new IllegalArgumentException("invalid orientation."); 254 } 255 assertNotInLayoutOrScroll(null); 256 if (mPendingSavedState != null && mPendingSavedState.mOrientation != orientation) { 257 // override pending state 258 mPendingSavedState.mOrientation = orientation; 259 } 260 if (orientation == mOrientation) { 261 return; 262 } 263 mOrientation = orientation; 264 if (mPrimaryOrientation != null && mSecondaryOrientation != null) { 265 // swap 266 OrientationHelper tmp = mPrimaryOrientation; 267 mPrimaryOrientation = mSecondaryOrientation; 268 mSecondaryOrientation = tmp; 269 } 270 requestLayout(); 271 } 272 273 /** 274 * Sets whether LayoutManager should start laying out items from the end of the UI. The order 275 * items are traversed is not affected by this call. 276 * <p> 277 * This behaves similar to the layout change for RTL views. When set to true, first item is 278 * laid out at the end of the ViewGroup, second item is laid out before it etc. 279 * <p> 280 * For horizontal layouts, it depends on the layout direction. 281 * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if 282 * {@link RecyclerView}} is RTL, it will layout from LTR. 283 * 284 * @param reverseLayout Whether layout should be in reverse or not 285 */ 286 public void setReverseLayout(boolean reverseLayout) { 287 assertNotInLayoutOrScroll(null); 288 if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) { 289 mPendingSavedState.mReverseLayout = reverseLayout; 290 } 291 mReverseLayout = reverseLayout; 292 requestLayout(); 293 } 294 295 /** 296 * Returns the current gap handling strategy for StaggeredGridLayoutManager. 297 * <p> 298 * Staggered grid may have gaps in the layout as items may have different sizes. To avoid gaps, 299 * StaggeredGridLayoutManager provides 3 options. Check {@link #GAP_HANDLING_NONE}, 300 * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}, {@link #GAP_HANDLING_LAZY} for details. 301 * <p> 302 * By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}. 303 * 304 * @return Current gap handling strategy. 305 * @see #setGapStrategy(int) 306 * @see #GAP_HANDLING_NONE 307 * @see #GAP_HANDLING_LAZY 308 * @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS 309 */ 310 public int getGapStrategy() { 311 return mGapStrategy; 312 } 313 314 /** 315 * Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter 316 * is different than the current strategy, calling this method will trigger a layout request. 317 * 318 * @param gapStrategy The new gap handling strategy. Should be {@link #GAP_HANDLING_LAZY} 319 * , {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or 320 * {@link #GAP_HANDLING_NONE} 321 * @see #getGapStrategy() 322 */ 323 public void setGapStrategy(int gapStrategy) { 324 assertNotInLayoutOrScroll(null); 325 if (mPendingSavedState != null && mPendingSavedState.mGapStrategy != gapStrategy) { 326 mPendingSavedState.mGapStrategy = gapStrategy; 327 } 328 if (gapStrategy == mGapStrategy) { 329 return; 330 } 331 if (gapStrategy != GAP_HANDLING_LAZY && gapStrategy != GAP_HANDLING_NONE && 332 gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) { 333 throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE " 334 + ", GAP_HANDLING_LAZY or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS"); 335 } 336 mGapStrategy = gapStrategy; 337 requestLayout(); 338 } 339 340 @Override 341 public void assertNotInLayoutOrScroll(String message) { 342 if (mPendingSavedState == null) { 343 super.assertNotInLayoutOrScroll(message); 344 } 345 } 346 347 /** 348 * Returns the number of spans laid out by StaggeredGridLayoutManager. 349 * 350 * @return Number of spans in the layout 351 */ 352 public int getSpanCount() { 353 return mSpanCount; 354 } 355 356 /** 357 * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items. 358 * <p> 359 * If you need to cancel current assignments, you can call this method which will clear all 360 * assignments and request a new layout. 361 */ 362 public void invalidateSpanAssignments() { 363 mLazySpanLookup.clear(); 364 requestLayout(); 365 } 366 367 private void ensureOrientationHelper() { 368 if (mPrimaryOrientation == null) { 369 mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation); 370 mSecondaryOrientation = OrientationHelper 371 .createOrientationHelper(this, 1 - mOrientation); 372 mLayoutState = new LayoutState(); 373 } 374 } 375 376 /** 377 * Calculates the views' layout order. (e.g. from end to start or start to end) 378 * RTL layout support is applied automatically. So if layout is RTL and 379 * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. 380 */ 381 private void resolveShouldLayoutReverse() { 382 // A == B is the same result, but we rather keep it readable 383 if (mOrientation == VERTICAL || !isLayoutRTL()) { 384 mShouldReverseLayout = mReverseLayout; 385 } else { 386 mShouldReverseLayout = !mReverseLayout; 387 } 388 } 389 390 private boolean isLayoutRTL() { 391 return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; 392 } 393 394 /** 395 * Returns whether views are laid out in reverse order or not. 396 * <p> 397 * Not that this value is not affected by RecyclerView's layout direction. 398 * 399 * @return True if layout is reversed, false otherwise 400 * @see #setReverseLayout(boolean) 401 */ 402 public boolean getReverseLayout() { 403 return mReverseLayout; 404 } 405 406 @Override 407 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 408 ensureOrientationHelper(); 409 // Update adapter size. 410 mLazySpanLookup.mAdapterSize = state.getItemCount(); 411 int anchorItemPosition; 412 int anchorOffset; 413 // This value may change if we are jumping to a position. 414 boolean layoutFromEnd; 415 416 // If set to true, spans will clear their offsets and they'll be laid out from start 417 // depending on the layout direction. Invalidating span offsets is necessary to be able 418 // to jump to a position. 419 boolean invalidateSpanOffsets = false; 420 421 if (mPendingSavedState != null) { 422 if (DEBUG) { 423 Log.d(TAG, "found saved state: " + mPendingSavedState); 424 } 425 setOrientation(mPendingSavedState.mOrientation); 426 setSpanCount(mPendingSavedState.mSpanCount); 427 setGapStrategy(mPendingSavedState.mGapStrategy); 428 setReverseLayout(mPendingSavedState.mReverseLayout); 429 resolveShouldLayoutReverse(); 430 431 if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) { 432 mPendingScrollPosition = mPendingSavedState.mAnchorPosition; 433 layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; 434 } else { 435 layoutFromEnd = mShouldReverseLayout; 436 } 437 if (mPendingSavedState.mHasSpanOffsets) { 438 for (int i = 0; i < mSpanCount; i++) { 439 mSpans[i].clear(); 440 mSpans[i].setLine(mPendingSavedState.mSpanOffsets[i]); 441 } 442 } 443 if (mPendingSavedState.mSpanLookupSize > 1) { 444 mLazySpanLookup.mData = mPendingSavedState.mSpanLookup; 445 } 446 447 } else { 448 resolveShouldLayoutReverse(); 449 layoutFromEnd = mShouldReverseLayout; // get updated value. 450 } 451 452 // Validate scroll position if exists. 453 if (!state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION) { 454 // Validate it. 455 if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { 456 mPendingScrollPosition = RecyclerView.NO_POSITION; 457 mPendingScrollPositionOffset = INVALID_OFFSET; 458 } 459 } 460 461 if (!state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION) { 462 if (mPendingSavedState == null 463 || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION 464 || !mPendingSavedState.mHasSpanOffsets) { 465 // If item is visible, make it fully visible. 466 final View child = findViewByPosition(mPendingScrollPosition); 467 if (child != null) { 468 if (mPendingScrollPositionOffset != INVALID_OFFSET) { 469 // Use regular anchor position. 470 anchorItemPosition = mShouldReverseLayout ? getLastChildPosition() 471 : getFirstChildPosition(); 472 if (layoutFromEnd) { 473 final int target = mPrimaryOrientation.getEndAfterPadding() - 474 mPendingScrollPositionOffset; 475 anchorOffset = target - mPrimaryOrientation.getDecoratedEnd(child); 476 } else { 477 final int target = mPrimaryOrientation.getStartAfterPadding() + 478 mPendingScrollPositionOffset; 479 anchorOffset = target - mPrimaryOrientation.getDecoratedStart(child); 480 } 481 } else { 482 final int startGap = mPrimaryOrientation.getDecoratedStart(child) 483 - mPrimaryOrientation.getStartAfterPadding(); 484 final int endGap = mPrimaryOrientation.getEndAfterPadding() - 485 mPrimaryOrientation.getDecoratedEnd(child); 486 final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child); 487 // Use regular anchor item, just offset the layout. 488 anchorItemPosition = mShouldReverseLayout ? getLastChildPosition() 489 : getFirstChildPosition(); 490 if (childSize > mPrimaryOrientation.getTotalSpace()) { 491 // Item does not fit. Fix depending on layout direction. 492 anchorOffset = layoutFromEnd ? mPrimaryOrientation.getEndAfterPadding() 493 : mPrimaryOrientation.getStartAfterPadding(); 494 } else if (startGap < 0) { 495 anchorOffset = -startGap; 496 } else if (endGap < 0) { 497 anchorOffset = endGap; 498 } else { 499 // Nothing to do, just layout normal. 500 anchorItemPosition = mShouldReverseLayout ? getLastChildPosition() 501 : getFirstChildPosition(); 502 anchorOffset = INVALID_OFFSET; 503 } 504 } 505 } else { 506 // Child is not visible. Set anchor coordinate depending on in which direction 507 // child will be visible. 508 anchorItemPosition = mPendingScrollPosition; 509 if (mPendingScrollPositionOffset == INVALID_OFFSET) { 510 final int position = calculateScrollDirectionForPosition( 511 anchorItemPosition); 512 if (position == LAYOUT_START) { 513 anchorOffset = mPrimaryOrientation.getStartAfterPadding(); 514 layoutFromEnd = false; 515 } else { 516 anchorOffset = mPrimaryOrientation.getEndAfterPadding(); 517 layoutFromEnd = true; 518 } 519 } else { 520 if (layoutFromEnd) { 521 anchorOffset = mPrimaryOrientation.getEndAfterPadding() 522 - mPendingScrollPositionOffset; 523 } else { 524 anchorOffset = mPrimaryOrientation.getStartAfterPadding() 525 + mPendingScrollPositionOffset; 526 } 527 } 528 invalidateSpanOffsets = true; 529 } 530 } else { 531 anchorOffset = INVALID_OFFSET; 532 anchorItemPosition = mPendingScrollPosition; 533 } 534 535 } else { 536 // We don't recycle views out of adapter order. This way, we can rely on the first or 537 // last child as the anchor position. 538 anchorItemPosition = mShouldReverseLayout 539 ? findLastReferenceChildPosition(state.getItemCount()) 540 : findFirstReferenceChildPosition(state.getItemCount()); 541 anchorOffset = INVALID_OFFSET; 542 } 543 if (getChildCount() > 0 && (mPendingSavedState == null || 544 !mPendingSavedState.mHasSpanOffsets)) { 545 if (invalidateSpanOffsets || mHasGaps) { 546 for (int i = 0; i < mSpanCount; i++) { 547 // Scroll to position is set, clear. 548 mSpans[i].clear(); 549 if (anchorOffset != INVALID_OFFSET) { 550 mSpans[i].setLine(anchorOffset); 551 } 552 } 553 } else { 554 for (int i = 0; i < mSpanCount; i++) { 555 mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout, anchorOffset); 556 } 557 if (DEBUG) { 558 for (int i = 0; i < mSpanCount; i++) { 559 Log.d(TAG, "cached start-end lines for " + i + ":" + 560 mSpans[i].mCachedStart + ":" + mSpans[i].mCachedEnd); 561 } 562 } 563 } 564 } 565 mSizePerSpan = mSecondaryOrientation.getTotalSpace() / mSpanCount; 566 detachAndScrapAttachedViews(recycler); 567 // Layout start. 568 updateLayoutStateToFillStart(anchorItemPosition, state); 569 if (!layoutFromEnd) { 570 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 571 } 572 fill(recycler, mLayoutState, state); 573 574 // Layout end. 575 updateLayoutStateToFillEnd(anchorItemPosition, state); 576 if (layoutFromEnd) { 577 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 578 } 579 fill(recycler, mLayoutState, state); 580 581 if (getChildCount() > 0) { 582 if (mShouldReverseLayout) { 583 fixEndGap(recycler, state, true); 584 fixStartGap(recycler, state, false); 585 } else { 586 fixStartGap(recycler, state, true); 587 fixEndGap(recycler, state, false); 588 } 589 } 590 591 if (!state.isPreLayout()) { 592 mPendingScrollPosition = RecyclerView.NO_POSITION; 593 mPendingScrollPositionOffset = INVALID_OFFSET; 594 } 595 mLastLayoutFromEnd = layoutFromEnd; 596 mPendingSavedState = null; // we don't need this anymore 597 } 598 599 /** 600 * Checks if a child is assigned to the non-optimal span. 601 * 602 * @param startChildIndex Starts checking after this child, inclusive 603 * @param endChildIndex Starts checking until this child, exclusive 604 * @return The first View that is assigned to the wrong span. 605 */ 606 View hasGapsToFix(int startChildIndex, int endChildIndex) { 607 // quick reject 608 if (startChildIndex >= endChildIndex) { 609 return null; 610 } 611 final int firstChildIndex, childLimit; 612 final int nextSpanDiff = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1; 613 614 if (mShouldReverseLayout) { 615 firstChildIndex = endChildIndex - 1; 616 childLimit = startChildIndex - 1; 617 } else { 618 firstChildIndex = startChildIndex; 619 childLimit = endChildIndex; 620 } 621 final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1; 622 for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) { 623 View child = getChildAt(i); 624 final int start = mPrimaryOrientation.getDecoratedStart(child); 625 final int end = mPrimaryOrientation.getDecoratedEnd(child); 626 LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); 627 if (layoutParams.mFullSpan) { 628 continue; // quick reject 629 } 630 int nextSpanIndex = layoutParams.getSpanIndex() + nextSpanDiff; 631 while (nextSpanIndex >= 0 && nextSpanIndex < mSpanCount) { 632 Span nextSpan = mSpans[nextSpanIndex]; 633 if (nextSpan.isEmpty(start, end)) { 634 return child; 635 } 636 nextSpanIndex += nextSpanDiff; 637 } 638 } 639 // everything looks good 640 return null; 641 } 642 643 @Override 644 public boolean supportsPredictiveItemAnimations() { 645 return mPendingSavedState == null; 646 } 647 648 /** 649 * Returns the adapter position of the first visible view for each span. 650 * <p> 651 * Note that, this value is not affected by layout orientation or item order traversal. 652 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 653 * not in the layout. 654 * <p> 655 * If RecyclerView has item decorators, they will be considered in calculations as well. 656 * <p> 657 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 658 * views are ignored in this method. 659 * 660 * @return The adapter position of the first visible item in each span. If a span does not have 661 * any items, {@link RecyclerView#NO_POSITION} is returned for that span. 662 * 663 * @param into An array to put the results into. If you don't provide any, LayoutManager will 664 * create a new one. 665 * @see #findFirstCompletelyVisibleItemPositions(int[]) 666 * @see #findLastVisibleItemPositions(int[]) 667 */ 668 public int[] findFirstVisibleItemPositions(int[] into) { 669 if (into == null) { 670 into = new int[mSpanCount]; 671 } else if (into.length < mSpanCount) { 672 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 673 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 674 } 675 for (int i = 0; i < mSpanCount; i ++) { 676 into[i] = mSpans[i].findFirstVisibleItemPosition(); 677 } 678 return into; 679 } 680 681 /** 682 * Returns the adapter position of the first completely visible view for each span. 683 * <p> 684 * Note that, this value is not affected by layout orientation or item order traversal. 685 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 686 * not in the layout. 687 * <p> 688 * If RecyclerView has item decorators, they will be considered in calculations as well. 689 * <p> 690 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 691 * views are ignored in this method. 692 * 693 * @return The adapter position of the first fully visible item in each span. If a span does 694 * not have any items, {@link RecyclerView#NO_POSITION} is returned for that span. 695 * @param into An array to put the results into. If you don't provide any, LayoutManager will 696 * create a new one. 697 * @see #findFirstVisibleItemPositions(int[]) 698 * @see #findLastCompletelyVisibleItemPositions(int[]) 699 */ 700 public int[] findFirstCompletelyVisibleItemPositions(int[] into) { 701 if (into == null) { 702 into = new int[mSpanCount]; 703 } else if (into.length < mSpanCount) { 704 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 705 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 706 } 707 for (int i = 0; i < mSpanCount; i ++) { 708 into[i] = mSpans[i].findFirstCompletelyVisibleItemPosition(); 709 } 710 return into; 711 } 712 713 /** 714 * Returns the adapter position of the last visible view for each span. 715 * <p> 716 * Note that, this value is not affected by layout orientation or item order traversal. 717 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 718 * not in the layout. 719 * <p> 720 * If RecyclerView has item decorators, they will be considered in calculations as well. 721 * <p> 722 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 723 * views are ignored in this method. 724 * 725 * @return The adapter position of the last visible item in each span. If a span does not have 726 * any items, {@link RecyclerView#NO_POSITION} is returned for that span. 727 * 728 * @param into An array to put the results into. If you don't provide any, LayoutManager will 729 * create a new one. 730 * @see #findLastCompletelyVisibleItemPositions(int[]) 731 * @see #findFirstVisibleItemPositions(int[]) 732 */ 733 public int[] findLastVisibleItemPositions(int[] into) { 734 if (into == null) { 735 into = new int[mSpanCount]; 736 } else if (into.length < mSpanCount) { 737 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 738 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 739 } 740 for (int i = 0; i < mSpanCount; i ++) { 741 into[i] = mSpans[i].findLastVisibleItemPosition(); 742 } 743 return into; 744 } 745 746 /** 747 * Returns the adapter position of the last completely visible view for each span. 748 * <p> 749 * Note that, this value is not affected by layout orientation or item order traversal. 750 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 751 * not in the layout. 752 * <p> 753 * If RecyclerView has item decorators, they will be considered in calculations as well. 754 * <p> 755 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 756 * views are ignored in this method. 757 * 758 * @return The adapter position of the last fully visible item in each span. If a span does not 759 * have any items, {@link RecyclerView#NO_POSITION} is returned for that span. 760 * 761 * @param into An array to put the results into. If you don't provide any, LayoutManager will 762 * create a new one. 763 * @see #findFirstCompletelyVisibleItemPositions(int[]) 764 * @see #findLastVisibleItemPositions(int[]) 765 */ 766 public int[] findLastCompletelyVisibleItemPositions(int[] into) { 767 if (into == null) { 768 into = new int[mSpanCount]; 769 } else if (into.length < mSpanCount) { 770 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 771 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 772 } 773 for (int i = 0; i < mSpanCount; i ++) { 774 into[i] = mSpans[i].findLastCompletelyVisibleItemPosition(); 775 } 776 return into; 777 } 778 779 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, 780 int heightSpec) { 781 final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); 782 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 783 widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + insets.left, 784 lp.rightMargin + insets.right); 785 heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + insets.top, 786 lp.bottomMargin + insets.bottom); 787 child.measure(widthSpec, heightSpec); 788 } 789 790 private int updateSpecWithExtra(int spec, int startInset, int endInset) { 791 if (startInset == 0 && endInset == 0) { 792 return spec; 793 } 794 final int mode = View.MeasureSpec.getMode(spec); 795 if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { 796 return View.MeasureSpec.makeMeasureSpec( 797 View.MeasureSpec.getSize(spec) - startInset - endInset, mode); 798 } 799 return spec; 800 } 801 802 @Override 803 public void onRestoreInstanceState(Parcelable state) { 804 if (state instanceof SavedState) { 805 mPendingSavedState = (SavedState) state; 806 requestLayout(); 807 } else if (DEBUG) { 808 Log.d(TAG, "invalid saved state class"); 809 } 810 } 811 812 @Override 813 public Parcelable onSaveInstanceState() { 814 if (mPendingSavedState != null) { 815 return new SavedState(mPendingSavedState); 816 } 817 SavedState state = new SavedState(); 818 state.mOrientation = mOrientation; 819 state.mReverseLayout = mReverseLayout; 820 state.mSpanCount = mSpanCount; 821 state.mAnchorLayoutFromEnd = mLastLayoutFromEnd; 822 state.mGapStrategy = mGapStrategy; 823 824 if (mLazySpanLookup != null && mLazySpanLookup.mData != null) { 825 state.mSpanLookup = mLazySpanLookup.mData; 826 state.mSpanLookupSize = state.mSpanLookup.length; 827 } else { 828 state.mSpanLookupSize = 0; 829 } 830 831 if (getChildCount() > 0) { 832 state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition() 833 : getFirstChildPosition(); 834 state.mVisibleAnchorPosition = findFirstVisibleItemPositionInt(); 835 state.mHasSpanOffsets = true; 836 state.mSpanOffsets = new int[mSpanCount]; 837 for (int i = 0; i < mSpanCount; i++) { 838 state.mSpanOffsets[i] = mLastLayoutFromEnd ? mSpans[i].getEndLine(Span.INVALID_LINE) 839 : mSpans[i].getStartLine(Span.INVALID_LINE); 840 } 841 } else { 842 state.mAnchorPosition = RecyclerView.NO_POSITION; 843 state.mVisibleAnchorPosition = RecyclerView.NO_POSITION; 844 state.mHasSpanOffsets = false; 845 } 846 if (DEBUG) { 847 Log.d(TAG, "saved state:\n" + state); 848 } 849 return state; 850 } 851 852 /** 853 * Finds the first fully visible child to be used as an anchor child if span count changes when 854 * state is restored. 855 */ 856 int findFirstVisibleItemPositionInt() { 857 final int start, end, diff; 858 if (mLastLayoutFromEnd) { 859 start = getChildCount() - 1; 860 end = -1; 861 diff = -1; 862 } else { 863 start = 0; 864 end = getChildCount(); 865 diff = 1; 866 } 867 final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); 868 final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); 869 for (int i = start; i != end; i += diff) { 870 final View child = getChildAt(i); 871 if (mPrimaryOrientation.getDecoratedStart(child) >= boundsStart 872 && mPrimaryOrientation.getDecoratedEnd(child) <= boundsEnd) { 873 return getPosition(child); 874 } 875 } 876 return RecyclerView.NO_POSITION; 877 } 878 879 private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state, 880 boolean canOffsetChildren) { 881 final int maxEndLine = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); 882 int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine; 883 int fixOffset; 884 if (gap > 0) { 885 fixOffset = -scrollBy(-gap, recycler, state); 886 } else { 887 return; // nothing to fix 888 } 889 gap -= fixOffset; 890 if (canOffsetChildren && gap > 0) { 891 mPrimaryOrientation.offsetChildren(gap); 892 } 893 } 894 895 private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state, 896 boolean canOffsetChildren) { 897 final int minStartLine = getMinStart(mPrimaryOrientation.getStartAfterPadding()); 898 int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding(); 899 int fixOffset; 900 if (gap > 0) { 901 fixOffset = scrollBy(gap, recycler, state); 902 } else { 903 return; // nothing to fix 904 } 905 gap -= fixOffset; 906 if (canOffsetChildren && gap > 0) { 907 mPrimaryOrientation.offsetChildren(-gap); 908 } 909 } 910 911 private void updateLayoutStateToFillStart(int anchorPosition, RecyclerView.State state) { 912 mLayoutState.mAvailable = 0; 913 mLayoutState.mCurrentPosition = anchorPosition; 914 if (isSmoothScrolling()) { 915 final int targetPos = state.getTargetScrollPosition(); 916 if (mShouldReverseLayout == targetPos < anchorPosition) { 917 mLayoutState.mExtra = 0; 918 } else { 919 mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); 920 } 921 } else { 922 mLayoutState.mExtra = 0; 923 } 924 mLayoutState.mLayoutDirection = LAYOUT_START; 925 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL 926 : ITEM_DIRECTION_HEAD; 927 } 928 929 private void updateLayoutStateToFillEnd(int anchorPosition, RecyclerView.State state) { 930 mLayoutState.mAvailable = 0; 931 mLayoutState.mCurrentPosition = anchorPosition; 932 if (isSmoothScrolling()) { 933 final int targetPos = state.getTargetScrollPosition(); 934 if (mShouldReverseLayout == targetPos > anchorPosition) { 935 mLayoutState.mExtra = 0; 936 } else { 937 mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); 938 } 939 } else { 940 mLayoutState.mExtra = 0; 941 } 942 mLayoutState.mLayoutDirection = LAYOUT_END; 943 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD 944 : ITEM_DIRECTION_TAIL; 945 } 946 947 @Override 948 public void offsetChildrenHorizontal(int dx) { 949 super.offsetChildrenHorizontal(dx); 950 for (int i = 0; i < mSpanCount; i++) { 951 mSpans[i].onOffset(dx); 952 } 953 } 954 955 @Override 956 public void offsetChildrenVertical(int dy) { 957 super.offsetChildrenVertical(dy); 958 for (int i = 0; i < mSpanCount; i++) { 959 mSpans[i].onOffset(dy); 960 } 961 } 962 963 @Override 964 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 965 if (!considerSpanInvalidate(positionStart, itemCount)) { 966 // If positions are not invalidated, move span offsets. 967 mLazySpanLookup.offsetForRemoval(positionStart, itemCount); 968 } 969 } 970 971 @Override 972 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 973 if (!considerSpanInvalidate(positionStart, itemCount)) { 974 // If positions are not invalidated, move span offsets. 975 mLazySpanLookup.offsetForAddition(positionStart, itemCount); 976 } 977 } 978 979 /** 980 * Checks whether it should invalidate span assignments in response to an adapter change. 981 */ 982 private boolean considerSpanInvalidate(int positionStart, int itemCount) { 983 int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition(); 984 if (positionStart + itemCount <= minPosition) { 985 return false;// nothing to update. 986 } 987 int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition(); 988 mLazySpanLookup.invalidateAfter(positionStart); 989 if (positionStart <= maxPosition) { 990 requestLayout(); 991 } 992 return true; 993 } 994 995 private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, 996 RecyclerView.State state) { 997 mRemainingSpans.set(0, mSpanCount, true); 998 // The target position we are trying to reach. 999 final int targetLine; 1000 1001 /* 1002 * The line until which we can recycle, as long as we add views. 1003 * Keep in mind, it is still the line in layout direction which means; to calculate the 1004 * actual recycle line, we should subtract/add the size in orientation. 1005 */ 1006 final int recycleLine; 1007 // Line of the furthest row. 1008 if (layoutState.mLayoutDirection == LAYOUT_END) { 1009 // ignore padding for recycler 1010 recycleLine = mPrimaryOrientation.getEndAfterPadding() + mLayoutState.mAvailable; 1011 targetLine = recycleLine + mLayoutState.mExtra + mPrimaryOrientation.getEndPadding(); 1012 final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); 1013 for (int i = 0; i < mSpanCount; i++) { 1014 final Span span = mSpans[i]; 1015 final int line = span.getEndLine(defaultLine); 1016 if (line > targetLine) { 1017 mRemainingSpans.set(i, false); 1018 } 1019 } 1020 } else { // LAYOUT_START 1021 // ignore padding for recycler 1022 recycleLine = mPrimaryOrientation.getStartAfterPadding() - mLayoutState.mAvailable; 1023 targetLine = recycleLine - mLayoutState.mExtra - 1024 mPrimaryOrientation.getStartAfterPadding(); 1025 for (int i = 0; i < mSpanCount; i++) { 1026 final Span span = mSpans[i]; 1027 final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); 1028 final int line = span.getStartLine(defaultLine); 1029 if (line < targetLine) { 1030 mRemainingSpans.set(i, false); 1031 } 1032 } 1033 } 1034 1035 final int widthSpec, heightSpec; 1036 if (mOrientation == VERTICAL) { 1037 widthSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); 1038 heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 1039 } else { 1040 heightSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); 1041 widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 1042 } 1043 1044 while (layoutState.hasMore(state) && !mRemainingSpans.isEmpty()) { 1045 View view = layoutState.next(recycler); 1046 LayoutParams lp = ((LayoutParams) view.getLayoutParams()); 1047 if (layoutState.mLayoutDirection == LAYOUT_END) { 1048 addView(view); 1049 } else { 1050 addView(view, 0); 1051 } 1052 if (lp.mFullSpan) { 1053 final int fullSizeSpec = View.MeasureSpec.makeMeasureSpec( 1054 mSecondaryOrientation.getTotalSpace(), View.MeasureSpec.EXACTLY); 1055 if (mOrientation == VERTICAL) { 1056 measureChildWithDecorationsAndMargin(view, fullSizeSpec, heightSpec); 1057 } else { 1058 measureChildWithDecorationsAndMargin(view, widthSpec, fullSizeSpec); 1059 } 1060 } else { 1061 measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec); 1062 } 1063 1064 final int position = getPosition(view); 1065 final int spanIndex = mLazySpanLookup.getSpan(position); 1066 Span currentSpan; 1067 if (spanIndex == LayoutParams.INVALID_SPAN_ID) { 1068 if (lp.mFullSpan) { 1069 // assign full span items to first span 1070 currentSpan = mSpans[0]; 1071 } else { 1072 currentSpan = getNextSpan(layoutState); 1073 } 1074 mLazySpanLookup.setSpan(position, currentSpan); 1075 } else { 1076 currentSpan = mSpans[spanIndex]; 1077 } 1078 final int start; 1079 final int end; 1080 if (layoutState.mLayoutDirection == LAYOUT_END) { 1081 final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding() 1082 : mPrimaryOrientation.getStartAfterPadding(); 1083 start = lp.mFullSpan ? getMaxEnd(def) : currentSpan.getEndLine(def); 1084 end = start + mPrimaryOrientation.getDecoratedMeasurement(view); 1085 if (lp.mFullSpan) { 1086 for (int i = 0; i < mSpanCount; i++) { 1087 mSpans[i].appendToSpan(view); 1088 } 1089 } else { 1090 currentSpan.appendToSpan(view); 1091 } 1092 } else { 1093 final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding() 1094 : mPrimaryOrientation.getStartAfterPadding(); 1095 end = lp.mFullSpan ? getMinStart(def) : currentSpan.getStartLine(def); 1096 start = end - mPrimaryOrientation.getDecoratedMeasurement(view); 1097 if (lp.mFullSpan) { 1098 for (int i = 0; i < mSpanCount; i++) { 1099 mSpans[i].prependToSpan(view); 1100 } 1101 } else { 1102 currentSpan.prependToSpan(view); 1103 } 1104 1105 } 1106 lp.mSpan = currentSpan; 1107 1108 if (DEBUG) { 1109 Log.d(TAG, "adding view item " + lp.getViewPosition() + " between " + start + "," 1110 + end); 1111 } 1112 1113 final int otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding() 1114 : currentSpan.mIndex * mSizePerSpan + mSecondaryOrientation 1115 .getStartAfterPadding(); 1116 final int otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view); 1117 if (mOrientation == VERTICAL) { 1118 layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end); 1119 } else { 1120 layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd); 1121 } 1122 if (lp.mFullSpan) { 1123 for (int i = 0; i < mSpanCount; i++) { 1124 updateRemainingSpans(mSpans[i], mLayoutState.mLayoutDirection, targetLine); 1125 } 1126 } else { 1127 updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine); 1128 } 1129 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 1130 // calculate recycle line 1131 int maxStart = getMaxStart(currentSpan.getStartLine()); 1132 recycleFromEnd(recycler, Math.max(recycleLine, maxStart) + 1133 (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding())); 1134 } else { 1135 // calculate recycle line 1136 int minEnd = getMinEnd(currentSpan.getEndLine()); 1137 recycleFromStart(recycler, Math.min(recycleLine, minEnd) - 1138 (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding())); 1139 } 1140 } 1141 if (DEBUG) { 1142 Log.d(TAG, "fill, " + getChildCount()); 1143 } 1144 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 1145 final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding()); 1146 return Math.max(0, mLayoutState.mAvailable + (recycleLine - minStart)); 1147 } else { 1148 final int max = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); 1149 return Math.max(0, mLayoutState.mAvailable + (max - recycleLine)); 1150 } 1151 } 1152 1153 private void layoutDecoratedWithMargins(View child, int left, int top, int right, 1154 int bottom) { 1155 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1156 layoutDecorated(child, left + lp.leftMargin, top + lp.topMargin, right - lp.rightMargin 1157 , bottom - lp.bottomMargin); 1158 } 1159 1160 private void updateRemainingSpans(Span span, int layoutDir, int targetLine) { 1161 final int deletedSize = span.getDeletedSize(); 1162 if (layoutDir == LAYOUT_START) { 1163 final int line = span.getStartLine(); 1164 if (line + deletedSize < targetLine) { 1165 mRemainingSpans.set(span.mIndex, false); 1166 } 1167 } else { 1168 final int line = span.getEndLine(); 1169 if (line - deletedSize > targetLine) { 1170 mRemainingSpans.set(span.mIndex, false); 1171 } 1172 } 1173 } 1174 1175 private int getMaxStart(int def) { 1176 int maxStart = mSpans[0].getStartLine(def); 1177 for (int i = 1; i < mSpanCount; i++) { 1178 final int spanStart = mSpans[i].getStartLine(def); 1179 if (spanStart > maxStart) { 1180 maxStart = spanStart; 1181 } 1182 } 1183 return maxStart; 1184 } 1185 1186 private int getMinStart(int def) { 1187 int minStart = mSpans[0].getStartLine(def); 1188 for (int i = 1; i < mSpanCount; i++) { 1189 final int spanStart = mSpans[i].getStartLine(def); 1190 if (spanStart < minStart) { 1191 minStart = spanStart; 1192 } 1193 } 1194 return minStart; 1195 } 1196 1197 private int getMaxEnd(int def) { 1198 int maxEnd = mSpans[0].getEndLine(def); 1199 for (int i = 1; i < mSpanCount; i++) { 1200 final int spanEnd = mSpans[i].getEndLine(def); 1201 if (spanEnd > maxEnd) { 1202 maxEnd = spanEnd; 1203 } 1204 } 1205 return maxEnd; 1206 } 1207 1208 private int getMinEnd(int def) { 1209 int minEnd = mSpans[0].getEndLine(def); 1210 for (int i = 1; i < mSpanCount; i++) { 1211 final int spanEnd = mSpans[i].getEndLine(def); 1212 if (spanEnd < minEnd) { 1213 minEnd = spanEnd; 1214 } 1215 } 1216 return minEnd; 1217 } 1218 1219 private void recycleFromStart(RecyclerView.Recycler recycler, int line) { 1220 if (DEBUG) { 1221 Log.d(TAG, "recycling from start for line " + line); 1222 } 1223 while (getChildCount() > 0) { 1224 View child = getChildAt(0); 1225 if (mPrimaryOrientation.getDecoratedEnd(child) < line) { 1226 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1227 if (lp.mFullSpan) { 1228 for (int j = 0; j < mSpanCount; j++) { 1229 mSpans[j].popStart(); 1230 } 1231 } else { 1232 lp.mSpan.popStart(); 1233 } 1234 removeAndRecycleView(child, recycler); 1235 } else { 1236 return;// done 1237 } 1238 } 1239 } 1240 1241 private void recycleFromEnd(RecyclerView.Recycler recycler, int line) { 1242 final int childCount = getChildCount(); 1243 int i; 1244 for (i = childCount - 1; i >= 0; i--) { 1245 View child = getChildAt(i); 1246 if (mPrimaryOrientation.getDecoratedStart(child) > line) { 1247 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1248 if (lp.mFullSpan) { 1249 for (int j = 0; j < mSpanCount; j++) { 1250 mSpans[j].popEnd(); 1251 } 1252 } else { 1253 lp.mSpan.popEnd(); 1254 } 1255 removeAndRecycleView(child, recycler); 1256 } else { 1257 return;// done 1258 } 1259 } 1260 } 1261 1262 /** 1263 * Finds the span for the next view. 1264 */ 1265 private Span getNextSpan(LayoutState layoutState) { 1266 final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); 1267 if (layoutState.mLayoutDirection == LAYOUT_END) { 1268 Span min = mSpans[0]; 1269 int minLine = min.getEndLine(mPrimaryOrientation.getStartAfterPadding()); 1270 final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); 1271 for (int i = 1; i < mSpanCount; i++) { 1272 final Span other = mSpans[i]; 1273 final int otherLine = other.getEndLine(defaultLine); 1274 if (otherLine < minLine || (otherLine == minLine && preferLastSpan)) { 1275 min = other; 1276 minLine = otherLine; 1277 } 1278 } 1279 return min; 1280 } else { 1281 Span max = mSpans[0]; 1282 int maxLine = max.getStartLine(mPrimaryOrientation.getEndAfterPadding()); 1283 final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); 1284 for (int i = 1; i < mSpanCount; i++) { 1285 final Span other = mSpans[i]; 1286 final int otherLine = other.getStartLine(defaultLine); 1287 if (otherLine > maxLine || (otherLine == maxLine && !preferLastSpan)) { 1288 max = other; 1289 maxLine = otherLine; 1290 } 1291 } 1292 return max; 1293 } 1294 } 1295 1296 @Override 1297 public boolean canScrollVertically() { 1298 return mOrientation == VERTICAL; 1299 } 1300 1301 @Override 1302 public boolean canScrollHorizontally() { 1303 return mOrientation == HORIZONTAL; 1304 } 1305 1306 @Override 1307 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 1308 RecyclerView.State state) { 1309 return scrollBy(dx, recycler, state); 1310 } 1311 1312 @Override 1313 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 1314 RecyclerView.State state) { 1315 return scrollBy(dy, recycler, state); 1316 } 1317 1318 private int calculateScrollDirectionForPosition(int position) { 1319 if (getChildCount() == 0) { 1320 return mShouldReverseLayout ? LAYOUT_END : LAYOUT_START; 1321 } 1322 final int firstChildPos = getFirstChildPosition(); 1323 return position < firstChildPos != mShouldReverseLayout ? LAYOUT_START : LAYOUT_END; 1324 } 1325 1326 @Override 1327 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 1328 int position) { 1329 LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { 1330 @Override 1331 public PointF computeScrollVectorForPosition(int targetPosition) { 1332 final int direction = calculateScrollDirectionForPosition(targetPosition); 1333 if (direction == 0) { 1334 return null; 1335 } 1336 if (mOrientation == HORIZONTAL) { 1337 return new PointF(direction, 0); 1338 } else { 1339 return new PointF(0, direction); 1340 } 1341 } 1342 }; 1343 scroller.setTargetPosition(position); 1344 startSmoothScroll(scroller); 1345 } 1346 1347 @Override 1348 public void scrollToPosition(int position) { 1349 if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) { 1350 mPendingSavedState.invalidateAnchorPositionInfo(); 1351 } 1352 mPendingScrollPosition = position; 1353 mPendingScrollPositionOffset = INVALID_OFFSET; 1354 requestLayout(); 1355 } 1356 1357 /** 1358 * Scroll to the specified adapter position with the given offset from layout start. 1359 * <p> 1360 * Note that scroll position change will not be reflected until the next layout call. 1361 * <p> 1362 * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. 1363 * 1364 * @param position Index (starting at 0) of the reference item. 1365 * @param offset The distance (in pixels) between the start edge of the item view and 1366 * start edge of the RecyclerView. 1367 * @see #setReverseLayout(boolean) 1368 * @see #scrollToPosition(int) 1369 */ 1370 public void scrollToPositionWithOffset(int position, int offset) { 1371 if (mPendingSavedState != null) { 1372 mPendingSavedState.invalidateAnchorPositionInfo(); 1373 } 1374 mPendingScrollPosition = position; 1375 mPendingScrollPositionOffset = offset; 1376 requestLayout(); 1377 } 1378 1379 private int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { 1380 ensureOrientationHelper(); 1381 final int referenceChildPosition; 1382 if (dt > 0) { // layout towards end 1383 mLayoutState.mLayoutDirection = LAYOUT_END; 1384 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD 1385 : ITEM_DIRECTION_TAIL; 1386 referenceChildPosition = getLastChildPosition(); 1387 } else { 1388 mLayoutState.mLayoutDirection = LAYOUT_START; 1389 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL 1390 : ITEM_DIRECTION_HEAD; 1391 referenceChildPosition = getFirstChildPosition(); 1392 } 1393 mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; 1394 final int absDt = Math.abs(dt); 1395 mLayoutState.mAvailable = absDt; 1396 mLayoutState.mExtra = isSmoothScrolling() ? mPrimaryOrientation.getTotalSpace() : 0; 1397 int consumed = fill(recycler, mLayoutState, state); 1398 final int totalScroll; 1399 if (absDt < consumed) { 1400 totalScroll = dt; 1401 } else if (dt < 0) { 1402 totalScroll = -consumed; 1403 } else { // dt > 0 1404 totalScroll = consumed; 1405 } 1406 if (DEBUG) { 1407 Log.d(TAG, "asked " + dt + " scrolled" + totalScroll); 1408 } 1409 1410 if (mGapStrategy == GAP_HANDLING_LAZY 1411 && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD) { 1412 final int targetStart = mPrimaryOrientation.getStartAfterPadding(); 1413 final int targetEnd = mPrimaryOrientation.getEndAfterPadding(); 1414 lazyOffsetSpans(-totalScroll, targetStart, targetEnd); 1415 } else { 1416 mPrimaryOrientation.offsetChildren(-totalScroll); 1417 } 1418 // always reset this if we scroll for a proper save instance state 1419 mLastLayoutFromEnd = mShouldReverseLayout; 1420 1421 if (totalScroll != 0 && mGapStrategy != GAP_HANDLING_NONE 1422 && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD && !mHasGaps) { 1423 final int addedChildCount = Math.abs(mLayoutState.mCurrentPosition 1424 - (referenceChildPosition + mLayoutState.mItemDirection)); 1425 if (addedChildCount > 0) { 1426 // check if any child has been attached to wrong span. If so, trigger a re-layout 1427 // after scroll 1428 final View viewInWrongSpan; 1429 final View referenceView = findViewByPosition(referenceChildPosition); 1430 if (referenceView == null) { 1431 viewInWrongSpan = hasGapsToFix(0, getChildCount()); 1432 } else { 1433 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 1434 viewInWrongSpan = hasGapsToFix(0, addedChildCount); 1435 } else { 1436 viewInWrongSpan = hasGapsToFix(getChildCount() - addedChildCount, 1437 getChildCount()); 1438 } 1439 } 1440 mHasGaps = viewInWrongSpan != null; 1441 } 1442 } 1443 return totalScroll; 1444 } 1445 1446 /** 1447 * The actual method that implements {@link #GAP_HANDLING_LAZY} 1448 */ 1449 private void lazyOffsetSpans(int offset, int targetStart, int targetEnd) { 1450 // For each span offset children one by one. 1451 // When a fullSpan item is reached, stop and wait for other spans to reach to that span. 1452 // When all reach, offset fullSpan to max of others and continue. 1453 int childrenToOffset = getChildCount(); 1454 int[] indexPerSpan = new int[mSpanCount]; 1455 int[] offsetPerSpan = new int[mSpanCount]; 1456 1457 final int childOrder = offset > 0 ? ITEM_DIRECTION_TAIL : ITEM_DIRECTION_HEAD; 1458 if (offset > 0) { 1459 Arrays.fill(indexPerSpan, 0); 1460 } else { 1461 for (int i = 0; i < mSpanCount; i++) { 1462 indexPerSpan[i] = mSpans[i].mViews.size() - 1; 1463 } 1464 } 1465 1466 for (int i = 0; i < mSpanCount; i++) { 1467 offsetPerSpan[i] = mSpans[i].getNormalizedOffset(offset, targetStart, targetEnd); 1468 } 1469 if (DEBUG) { 1470 Log.d(TAG, "lazy offset start. normalized: " + Arrays.toString(offsetPerSpan)); 1471 } 1472 1473 while (childrenToOffset > 0) { 1474 View fullSpanView = null; 1475 for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) { 1476 Span span = mSpans[spanIndex]; 1477 int viewIndex; 1478 for (viewIndex = indexPerSpan[spanIndex]; 1479 viewIndex < span.mViews.size() && viewIndex >= 0; viewIndex += childOrder) { 1480 View view = span.mViews.get(viewIndex); 1481 if (DEBUG) { 1482 Log.d(TAG, "span " + spanIndex + ", view:" + viewIndex + ", pos:" 1483 + getPosition(view)); 1484 } 1485 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1486 if (lp.mFullSpan) { 1487 if (DEBUG) { 1488 Log.d(TAG, "stopping on full span view on index " + viewIndex 1489 + " in span " + spanIndex); 1490 } 1491 fullSpanView = view; 1492 viewIndex += childOrder;// move to next view 1493 break; 1494 } 1495 // offset this child normally 1496 mPrimaryOrientation.offsetChild(view, offsetPerSpan[spanIndex]); 1497 final int nextChildIndex = viewIndex + childOrder; 1498 if (nextChildIndex < span.mViews.size() && nextChildIndex >= 0) { 1499 View nextView = span.mViews.get(nextChildIndex); 1500 // find gap between, before offset 1501 if (childOrder == ITEM_DIRECTION_HEAD) {// negative 1502 offsetPerSpan[spanIndex] = Math 1503 .min(0, mPrimaryOrientation.getDecoratedStart(view) 1504 - mPrimaryOrientation.getDecoratedEnd(nextView)); 1505 } else { 1506 offsetPerSpan[spanIndex] = Math 1507 .max(0, mPrimaryOrientation.getDecoratedEnd(view) - 1508 mPrimaryOrientation.getDecoratedStart(nextView)); 1509 } 1510 if (DEBUG) { 1511 Log.d(TAG, "offset diff:" + offsetPerSpan[spanIndex] + " between " 1512 + getPosition(nextView) + " and " + getPosition(view)); 1513 } 1514 } 1515 childrenToOffset--; 1516 } 1517 indexPerSpan[spanIndex] = viewIndex; 1518 } 1519 if (fullSpanView != null) { 1520 // we have to offset this view. We'll offset it as the biggest amount necessary 1521 int winnerSpan = 0; 1522 int winnerSpanOffset = Math.abs(offsetPerSpan[winnerSpan]); 1523 for (int i = 1; i < mSpanCount; i++) { 1524 final int spanOffset = Math.abs(offsetPerSpan[i]); 1525 if (spanOffset > winnerSpanOffset) { 1526 winnerSpan = i; 1527 winnerSpanOffset = spanOffset; 1528 } 1529 } 1530 if (DEBUG) { 1531 Log.d(TAG, "winner offset:" + offsetPerSpan[winnerSpan] + " of " + winnerSpan); 1532 } 1533 mPrimaryOrientation.offsetChild(fullSpanView, offsetPerSpan[winnerSpan]); 1534 childrenToOffset--; 1535 1536 for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) { 1537 final int nextViewIndex = indexPerSpan[spanIndex]; 1538 final Span span = mSpans[spanIndex]; 1539 if (nextViewIndex < span.mViews.size() && nextViewIndex > 0) { 1540 View nextView = span.mViews.get(nextViewIndex); 1541 // find gap between, before offset 1542 if (childOrder == ITEM_DIRECTION_HEAD) {// negative 1543 offsetPerSpan[spanIndex] = Math 1544 .min(0, mPrimaryOrientation.getDecoratedStart(fullSpanView) 1545 - mPrimaryOrientation.getDecoratedEnd(nextView)); 1546 } else { 1547 offsetPerSpan[spanIndex] = Math 1548 .max(0, mPrimaryOrientation.getDecoratedEnd(fullSpanView) - 1549 mPrimaryOrientation.getDecoratedStart(nextView)); 1550 } 1551 } 1552 } 1553 } 1554 } 1555 for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) { 1556 mSpans[spanIndex].invalidateCache(); 1557 } 1558 } 1559 1560 private int getLastChildPosition() { 1561 final int childCount = getChildCount(); 1562 return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1)); 1563 } 1564 1565 private int getFirstChildPosition() { 1566 final int childCount = getChildCount(); 1567 return childCount == 0 ? 0 : getPosition(getChildAt(0)); 1568 } 1569 1570 /** 1571 * Finds the first View that can be used as an anchor View. 1572 * 1573 * @return Position of the View or 0 if it cannot find any such View. 1574 */ 1575 private int findFirstReferenceChildPosition(int itemCount) { 1576 final int limit = getChildCount(); 1577 for (int i = 0; i < limit; i++) { 1578 final View view = getChildAt(i); 1579 final int position = getPosition(view); 1580 if (position >= 0 && position < itemCount) { 1581 return position; 1582 } 1583 } 1584 return 0; 1585 } 1586 1587 /** 1588 * Finds the last View that can be used as an anchor View. 1589 * 1590 * @return Position of the View or 0 if it cannot find any such View. 1591 */ 1592 private int findLastReferenceChildPosition(int itemCount) { 1593 for (int i = getChildCount() - 1; i >= 0; i--) { 1594 final View view = getChildAt(i); 1595 final int position = getPosition(view); 1596 if (position >= 0 && position < itemCount) { 1597 return position; 1598 } 1599 } 1600 return 0; 1601 } 1602 1603 @Override 1604 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 1605 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 1606 ViewGroup.LayoutParams.WRAP_CONTENT); 1607 } 1608 1609 @Override 1610 public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 1611 return new LayoutParams(c, attrs); 1612 } 1613 1614 @Override 1615 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 1616 if (lp instanceof ViewGroup.MarginLayoutParams) { 1617 return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 1618 } else { 1619 return new LayoutParams(lp); 1620 } 1621 } 1622 1623 @Override 1624 public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 1625 return lp instanceof LayoutParams; 1626 } 1627 1628 public int getOrientation() { 1629 return mOrientation; 1630 } 1631 1632 1633 /** 1634 * LayoutParams used by StaggeredGridLayoutManager. 1635 */ 1636 public static class LayoutParams extends RecyclerView.LayoutParams { 1637 1638 /** 1639 * Span Id for Views that are not laid out yet. 1640 */ 1641 public static final int INVALID_SPAN_ID = -1; 1642 1643 // Package scope to be able to access from tests. 1644 Span mSpan; 1645 1646 boolean mFullSpan; 1647 1648 public LayoutParams(Context c, AttributeSet attrs) { 1649 super(c, attrs); 1650 } 1651 1652 public LayoutParams(int width, int height) { 1653 super(width, height); 1654 } 1655 1656 public LayoutParams(ViewGroup.MarginLayoutParams source) { 1657 super(source); 1658 } 1659 1660 public LayoutParams(ViewGroup.LayoutParams source) { 1661 super(source); 1662 } 1663 1664 public LayoutParams(RecyclerView.LayoutParams source) { 1665 super(source); 1666 } 1667 1668 /** 1669 * When set to true, the item will layout using all span area. That means, if orientation 1670 * is vertical, the view will have full width; if orientation is horizontal, the view will 1671 * have full height. 1672 * 1673 * @param fullSpan True if this item should traverse all spans. 1674 */ 1675 public void setFullSpan(boolean fullSpan) { 1676 mFullSpan = fullSpan; 1677 } 1678 1679 /** 1680 * Returns the Span index to which this View is assigned. 1681 * 1682 * @return The Span index of the View. If View is not yet assigned to any span, returns 1683 * {@link #INVALID_SPAN_ID}. 1684 */ 1685 public final int getSpanIndex() { 1686 if (mSpan == null) { 1687 return INVALID_SPAN_ID; 1688 } 1689 return mSpan.mIndex; 1690 } 1691 } 1692 1693 // Package scoped to access from tests. 1694 class Span { 1695 1696 static final int INVALID_LINE = Integer.MIN_VALUE; 1697 1698 private ArrayList<View> mViews = new ArrayList<View>(); 1699 1700 int mCachedStart = INVALID_LINE; 1701 1702 int mCachedEnd = INVALID_LINE; 1703 1704 int mDeletedSize = 0; 1705 1706 final int mIndex; 1707 1708 private Span(int index) { 1709 mIndex = index; 1710 } 1711 1712 int getStartLine(int def) { 1713 if (mCachedStart != INVALID_LINE) { 1714 return mCachedStart; 1715 } 1716 if (mViews.size() == 0) { 1717 return def; 1718 } 1719 mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0)); 1720 return mCachedStart; 1721 } 1722 1723 // Use this one when default value does not make sense and not having a value means a bug. 1724 int getStartLine() { 1725 if (mCachedStart != INVALID_LINE) { 1726 return mCachedStart; 1727 } 1728 mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0)); 1729 return mCachedStart; 1730 } 1731 1732 int getEndLine(int def) { 1733 if (mCachedEnd != INVALID_LINE) { 1734 return mCachedEnd; 1735 } 1736 final int size = mViews.size(); 1737 if (size == 0) { 1738 return def; 1739 } 1740 mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(size - 1)); 1741 return mCachedEnd; 1742 } 1743 1744 // Use this one when default value does not make sense and not having a value means a bug. 1745 int getEndLine() { 1746 if (mCachedEnd != INVALID_LINE) { 1747 return mCachedEnd; 1748 } 1749 mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(mViews.size() - 1)); 1750 return mCachedEnd; 1751 } 1752 1753 void prependToSpan(View view) { 1754 LayoutParams lp = getLayoutParams(view); 1755 lp.mSpan = this; 1756 mViews.add(0, view); 1757 mCachedStart = INVALID_LINE; 1758 if (mViews.size() == 1) { 1759 mCachedEnd = INVALID_LINE; 1760 } 1761 if (lp.isItemRemoved() || lp.isItemChanged()) { 1762 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 1763 } 1764 } 1765 1766 void appendToSpan(View view) { 1767 LayoutParams lp = getLayoutParams(view); 1768 lp.mSpan = this; 1769 mViews.add(view); 1770 mCachedEnd = INVALID_LINE; 1771 if (mViews.size() == 1) { 1772 mCachedStart = INVALID_LINE; 1773 } 1774 if (lp.isItemRemoved() || lp.isItemChanged()) { 1775 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 1776 } 1777 } 1778 1779 // Useful method to preserve positions on a re-layout. 1780 void cacheReferenceLineAndClear(boolean reverseLayout, int offset) { 1781 int reference; 1782 if (reverseLayout) { 1783 reference = getEndLine(INVALID_LINE); 1784 } else { 1785 reference = getStartLine(INVALID_LINE); 1786 } 1787 clear(); 1788 if (reference == INVALID_LINE) { 1789 return; 1790 } 1791 if (offset != INVALID_OFFSET) { 1792 reference += offset; 1793 } 1794 mCachedStart = mCachedEnd = reference; 1795 } 1796 1797 void clear() { 1798 mViews.clear(); 1799 invalidateCache(); 1800 mDeletedSize = 0; 1801 } 1802 1803 void invalidateCache() { 1804 mCachedStart = INVALID_LINE; 1805 mCachedEnd = INVALID_LINE; 1806 } 1807 1808 void setLine(int line) { 1809 mCachedEnd = mCachedStart = line; 1810 } 1811 1812 void popEnd() { 1813 final int size = mViews.size(); 1814 View end = mViews.remove(size - 1); 1815 final LayoutParams lp = getLayoutParams(end); 1816 lp.mSpan = null; 1817 if (lp.isItemRemoved() || lp.isItemChanged()) { 1818 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end); 1819 } 1820 if (size == 1) { 1821 mCachedStart = INVALID_LINE; 1822 } 1823 mCachedEnd = INVALID_LINE; 1824 } 1825 1826 void popStart() { 1827 View start = mViews.remove(0); 1828 final LayoutParams lp = getLayoutParams(start); 1829 lp.mSpan = null; 1830 if (mViews.size() == 0) { 1831 mCachedEnd = INVALID_LINE; 1832 } 1833 if (lp.isItemRemoved() || lp.isItemChanged()) { 1834 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start); 1835 } 1836 mCachedStart = INVALID_LINE; 1837 } 1838 1839 // TODO cache this. 1840 public int getDeletedSize() { 1841 return mDeletedSize; 1842 } 1843 1844 LayoutParams getLayoutParams(View view) { 1845 return (LayoutParams) view.getLayoutParams(); 1846 } 1847 1848 void onOffset(int dt) { 1849 if (mCachedStart != INVALID_LINE) { 1850 mCachedStart += dt; 1851 } 1852 if (mCachedEnd != INVALID_LINE) { 1853 mCachedEnd += dt; 1854 } 1855 } 1856 1857 // normalized offset is how much this span can scroll 1858 int getNormalizedOffset(int dt, int targetStart, int targetEnd) { 1859 if (mViews.size() == 0) { 1860 return 0; 1861 } 1862 if (dt < 0) { 1863 final int endSpace = getEndLine() - targetEnd; 1864 if (endSpace <= 0) { 1865 return 0; 1866 } 1867 return -dt > endSpace ? -endSpace : dt; 1868 } else { 1869 final int startSpace = targetStart - getStartLine(); 1870 if (startSpace <= 0) { 1871 return 0; 1872 } 1873 return startSpace < dt ? startSpace : dt; 1874 } 1875 } 1876 1877 /** 1878 * Returns if there is no child between start-end lines 1879 * 1880 * @param start The start line 1881 * @param end The end line 1882 * @return true if a new child can be added between start and end 1883 */ 1884 boolean isEmpty(int start, int end) { 1885 final int count = mViews.size(); 1886 for (int i = 0; i < count; i++) { 1887 final View view = mViews.get(i); 1888 if (mPrimaryOrientation.getDecoratedStart(view) < end && 1889 mPrimaryOrientation.getDecoratedEnd(view) > start) { 1890 return false; 1891 } 1892 } 1893 return true; 1894 } 1895 1896 public int findFirstVisibleItemPosition() { 1897 return mReverseLayout 1898 ? findOneVisibleChild(mViews.size() - 1, -1, false) 1899 : findOneVisibleChild(0, mViews.size(), false); 1900 } 1901 1902 public int findFirstCompletelyVisibleItemPosition() { 1903 return mReverseLayout 1904 ? findOneVisibleChild(mViews.size() -1, -1, true) 1905 : findOneVisibleChild(0, mViews.size(), true); 1906 } 1907 1908 public int findLastVisibleItemPosition() { 1909 return mReverseLayout 1910 ? findOneVisibleChild(0, mViews.size(), false) 1911 : findOneVisibleChild(mViews.size() - 1, -1, false); 1912 } 1913 1914 public int findLastCompletelyVisibleItemPosition() { 1915 return mReverseLayout 1916 ? findOneVisibleChild(0, mViews.size(), true) 1917 : findOneVisibleChild(mViews.size() - 1, -1, true); 1918 } 1919 1920 int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { 1921 final int start = mPrimaryOrientation.getStartAfterPadding(); 1922 final int end = mPrimaryOrientation.getEndAfterPadding(); 1923 final int next = toIndex > fromIndex ? 1 : -1; 1924 for (int i = fromIndex; i != toIndex; i+=next) { 1925 final View child = mViews.get(i); 1926 final int childStart = mPrimaryOrientation.getDecoratedStart(child); 1927 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); 1928 if (childStart < end && childEnd > start) { 1929 if (completelyVisible) { 1930 if (childStart >= start && childEnd <= end) { 1931 return getPosition(child); 1932 } 1933 } else { 1934 return getPosition(child); 1935 } 1936 } 1937 } 1938 return RecyclerView.NO_POSITION; 1939 } 1940 } 1941 1942 /** 1943 * An array of mappings from adapter position to span. 1944 * This only grows when a write happens and it grows up to the size of the adapter. 1945 */ 1946 static class LazySpanLookup { 1947 1948 private static final int MIN_SIZE = 10; 1949 1950 int[] mData; 1951 1952 int mAdapterSize; // we don't want to grow beyond that, unless it grows 1953 1954 void invalidateAfter(int position) { 1955 if (mData == null) { 1956 return; 1957 } 1958 if (position >= mData.length) { 1959 return; 1960 } 1961 Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID); 1962 } 1963 1964 int getSpan(int position) { 1965 if (mData == null || position >= mData.length) { 1966 return LayoutParams.INVALID_SPAN_ID; 1967 } else { 1968 return mData[position]; 1969 } 1970 } 1971 1972 void setSpan(int position, Span span) { 1973 ensureSize(position); 1974 mData[position] = span.mIndex; 1975 } 1976 1977 int sizeForPosition(int position) { 1978 int len = mData.length; 1979 while (len <= position) { 1980 len *= 2; 1981 } 1982 if (len > mAdapterSize) { 1983 len = mAdapterSize; 1984 } 1985 return len; 1986 } 1987 1988 void ensureSize(int position) { 1989 if (mData == null) { 1990 mData = new int[Math.max(position, MIN_SIZE) + 1]; 1991 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 1992 } else if (position >= mData.length) { 1993 int[] old = mData; 1994 mData = new int[sizeForPosition(position)]; 1995 System.arraycopy(old, 0, mData, 0, old.length); 1996 Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID); 1997 } 1998 } 1999 2000 void clear() { 2001 if (mData != null) { 2002 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 2003 } 2004 } 2005 2006 void offsetForRemoval(int positionStart, int itemCount) { 2007 ensureSize(positionStart + itemCount); 2008 System.arraycopy(mData, positionStart + itemCount, mData, positionStart, 2009 mData.length - positionStart - itemCount); 2010 Arrays.fill(mData, mData.length - itemCount, mData.length, 2011 LayoutParams.INVALID_SPAN_ID); 2012 } 2013 2014 void offsetForAddition(int positionStart, int itemCount) { 2015 ensureSize(positionStart + itemCount); 2016 System.arraycopy(mData, positionStart, mData, positionStart + itemCount, 2017 mData.length - positionStart - itemCount); 2018 Arrays.fill(mData, positionStart, positionStart + itemCount, 2019 LayoutParams.INVALID_SPAN_ID); 2020 } 2021 } 2022 2023 static class SavedState implements Parcelable { 2024 2025 int mOrientation; 2026 2027 int mSpanCount; 2028 2029 int mGapStrategy; 2030 2031 int mAnchorPosition; 2032 2033 int mVisibleAnchorPosition; // if span count changes (span offsets are invalidated), 2034 // we use this one instead 2035 2036 int[] mSpanOffsets; 2037 2038 int mSpanLookupSize; 2039 2040 int[] mSpanLookup; 2041 2042 boolean mReverseLayout; 2043 2044 boolean mAnchorLayoutFromEnd; 2045 2046 boolean mHasSpanOffsets; 2047 2048 public SavedState() { 2049 } 2050 2051 SavedState(Parcel in) { 2052 mOrientation = in.readInt(); 2053 mSpanCount = in.readInt(); 2054 mGapStrategy = in.readInt(); 2055 mAnchorPosition = in.readInt(); 2056 mVisibleAnchorPosition = in.readInt(); 2057 mHasSpanOffsets = in.readInt() == 1; 2058 if (mHasSpanOffsets) { 2059 mSpanOffsets = new int[mSpanCount]; 2060 in.readIntArray(mSpanOffsets); 2061 } 2062 2063 mSpanLookupSize = in.readInt(); 2064 if (mSpanLookupSize > 0) { 2065 mSpanLookup = new int[mSpanLookupSize]; 2066 in.readIntArray(mSpanLookup); 2067 } 2068 mReverseLayout = in.readInt() == 1; 2069 mAnchorLayoutFromEnd = in.readInt() == 1; 2070 } 2071 2072 public SavedState(SavedState other) { 2073 mOrientation = other.mOrientation; 2074 mSpanCount = other.mSpanCount; 2075 mGapStrategy = other.mGapStrategy; 2076 mAnchorPosition = other.mAnchorPosition; 2077 mVisibleAnchorPosition = other.mVisibleAnchorPosition; 2078 mHasSpanOffsets = other.mHasSpanOffsets; 2079 mSpanOffsets = other.mSpanOffsets; 2080 mSpanLookupSize = other.mSpanLookupSize; 2081 mSpanLookup = other.mSpanLookup; 2082 mReverseLayout = other.mReverseLayout; 2083 mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; 2084 } 2085 2086 void invalidateSpanInfo() { 2087 mSpanOffsets = null; 2088 mHasSpanOffsets = false; 2089 mSpanCount = -1; 2090 mSpanLookupSize = 0; 2091 mSpanLookup = null; 2092 } 2093 2094 void invalidateAnchorPositionInfo() { 2095 mSpanOffsets = null; 2096 mHasSpanOffsets = false; 2097 mAnchorPosition = RecyclerView.NO_POSITION; 2098 mVisibleAnchorPosition = RecyclerView.NO_POSITION; 2099 } 2100 2101 @Override 2102 public int describeContents() { 2103 return 0; 2104 } 2105 2106 @Override 2107 public void writeToParcel(Parcel dest, int flags) { 2108 dest.writeInt(mOrientation); 2109 dest.writeInt(mSpanCount); 2110 dest.writeInt(mGapStrategy); 2111 dest.writeInt(mAnchorPosition); 2112 dest.writeInt(mVisibleAnchorPosition); 2113 dest.writeInt(mHasSpanOffsets ? 1 : 0); 2114 if (mHasSpanOffsets) { 2115 dest.writeIntArray(mSpanOffsets); 2116 } 2117 dest.writeInt(mSpanLookupSize); 2118 if (mSpanLookupSize > 0) { 2119 dest.writeIntArray(mSpanLookup); 2120 } 2121 dest.writeInt(mReverseLayout ? 1 : 0); 2122 dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); 2123 } 2124 2125 @Override 2126 public String toString() { 2127 return "SavedState{" + 2128 "mOrientation=" + mOrientation + 2129 ", mSpanCount=" + mSpanCount + 2130 ", mGapStrategy=" + mGapStrategy + 2131 ", mAnchorPosition=" + mAnchorPosition + 2132 ", mVisibleAnchorPosition=" + mVisibleAnchorPosition + 2133 ", mSpanOffsets=" + Arrays.toString(mSpanOffsets) + 2134 ", mSpanLookupSize=" + mSpanLookupSize + 2135 ", mSpanLookup=" + Arrays.toString(mSpanLookup) + 2136 ", mReverseLayout=" + mReverseLayout + 2137 ", mAnchorLayoutFromEnd=" + mAnchorLayoutFromEnd + 2138 ", mHasSpanOffsets=" + mHasSpanOffsets + 2139 '}'; 2140 } 2141 2142 public static final Parcelable.Creator<SavedState> CREATOR 2143 = new Parcelable.Creator<SavedState>() { 2144 @Override 2145 public SavedState createFromParcel(Parcel in) { 2146 return new SavedState(in); 2147 } 2148 2149 @Override 2150 public SavedState[] newArray(int size) { 2151 return new SavedState[size]; 2152 } 2153 }; 2154 } 2155} 2156