StaggeredGridLayoutManager.java revision 7c7fba8365684e1ccfc4f39f286df4d100c6c81f
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 private int mPendingScrollPosition = RecyclerView.NO_POSITION; 148 149 /** 150 * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is 151 * called. 152 */ 153 private 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 (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 (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 mPendingScrollPosition = RecyclerView.NO_POSITION; 592 mPendingScrollPositionOffset = INVALID_OFFSET; 593 mLastLayoutFromEnd = layoutFromEnd; 594 mPendingSavedState = null; // we don't need this anymore 595 } 596 597 /** 598 * Checks if a child is assigned to the non-optimal span. 599 * 600 * @param startChildIndex Starts checking after this child, inclusive 601 * @param endChildIndex Starts checking until this child, exclusive 602 * @return The first View that is assigned to the wrong span. 603 */ 604 View hasGapsToFix(int startChildIndex, int endChildIndex) { 605 // quick reject 606 if (startChildIndex >= endChildIndex) { 607 return null; 608 } 609 final int firstChildIndex, childLimit; 610 final int nextSpanDiff = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1; 611 612 if (mShouldReverseLayout) { 613 firstChildIndex = endChildIndex - 1; 614 childLimit = startChildIndex - 1; 615 } else { 616 firstChildIndex = startChildIndex; 617 childLimit = endChildIndex; 618 } 619 final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1; 620 for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) { 621 View child = getChildAt(i); 622 final int start = mPrimaryOrientation.getDecoratedStart(child); 623 final int end = mPrimaryOrientation.getDecoratedEnd(child); 624 LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); 625 if (layoutParams.mFullSpan) { 626 continue; // quick reject 627 } 628 int nextSpanIndex = layoutParams.getSpanIndex() + nextSpanDiff; 629 while (nextSpanIndex >= 0 && nextSpanIndex < mSpanCount) { 630 Span nextSpan = mSpans[nextSpanIndex]; 631 if (nextSpan.isEmpty(start, end)) { 632 return child; 633 } 634 nextSpanIndex += nextSpanDiff; 635 } 636 } 637 // everything looks good 638 return null; 639 } 640 641 @Override 642 public boolean supportsPredictiveItemAnimations() { 643 return true; 644 } 645 646 /** 647 * Returns the adapter position of the first visible view for each span. 648 * <p> 649 * Note that, this value is not affected by layout orientation or item order traversal. 650 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 651 * not in the layout. 652 * <p> 653 * If RecyclerView has item decorators, they will be considered in calculations as well. 654 * <p> 655 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 656 * views are ignored in this method. 657 * 658 * @return The adapter position of the first visible item in each span. If a span does not have 659 * any items, {@link RecyclerView#NO_POSITION} is returned for that span. 660 * 661 * @param into An array to put the results into. If you don't provide any, LayoutManager will 662 * create a new one. 663 * @see #findFirstCompletelyVisibleItemPositions(int[]) 664 * @see #findLastVisibleItemPositions(int[]) 665 */ 666 public int[] findFirstVisibleItemPositions(int[] into) { 667 if (into == null) { 668 into = new int[mSpanCount]; 669 } else if (into.length < mSpanCount) { 670 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 671 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 672 } 673 for (int i = 0; i < mSpanCount; i ++) { 674 into[i] = mSpans[i].findFirstVisibleItemPosition(); 675 } 676 return into; 677 } 678 679 /** 680 * Returns the adapter position of the first completely visible view for each span. 681 * <p> 682 * Note that, this value is not affected by layout orientation or item order traversal. 683 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 684 * not in the layout. 685 * <p> 686 * If RecyclerView has item decorators, they will be considered in calculations as well. 687 * <p> 688 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 689 * views are ignored in this method. 690 * 691 * @return The adapter position of the first fully visible item in each span. If a span does 692 * not have any items, {@link RecyclerView#NO_POSITION} is returned for that span. 693 * @param into An array to put the results into. If you don't provide any, LayoutManager will 694 * create a new one. 695 * @see #findFirstVisibleItemPositions(int[]) 696 * @see #findLastCompletelyVisibleItemPositions(int[]) 697 */ 698 public int[] findFirstCompletelyVisibleItemPositions(int[] into) { 699 if (into == null) { 700 into = new int[mSpanCount]; 701 } else if (into.length < mSpanCount) { 702 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 703 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 704 } 705 for (int i = 0; i < mSpanCount; i ++) { 706 into[i] = mSpans[i].findFirstCompletelyVisibleItemPosition(); 707 } 708 return into; 709 } 710 711 /** 712 * Returns the adapter position of the last visible view for each span. 713 * <p> 714 * Note that, this value is not affected by layout orientation or item order traversal. 715 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 716 * not in the layout. 717 * <p> 718 * If RecyclerView has item decorators, they will be considered in calculations as well. 719 * <p> 720 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 721 * views are ignored in this method. 722 * 723 * @return The adapter position of the last visible item in each span. If a span does not have 724 * any items, {@link RecyclerView#NO_POSITION} is returned for that span. 725 * 726 * @param into An array to put the results into. If you don't provide any, LayoutManager will 727 * create a new one. 728 * @see #findLastCompletelyVisibleItemPositions(int[]) 729 * @see #findFirstVisibleItemPositions(int[]) 730 */ 731 public int[] findLastVisibleItemPositions(int[] into) { 732 if (into == null) { 733 into = new int[mSpanCount]; 734 } else if (into.length < mSpanCount) { 735 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 736 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 737 } 738 for (int i = 0; i < mSpanCount; i ++) { 739 into[i] = mSpans[i].findLastVisibleItemPosition(); 740 } 741 return into; 742 } 743 744 /** 745 * Returns the adapter position of the last completely visible view for each span. 746 * <p> 747 * Note that, this value is not affected by layout orientation or item order traversal. 748 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 749 * not in the layout. 750 * <p> 751 * If RecyclerView has item decorators, they will be considered in calculations as well. 752 * <p> 753 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those 754 * views are ignored in this method. 755 * 756 * @return The adapter position of the last fully visible item in each span. If a span does not 757 * have any items, {@link RecyclerView#NO_POSITION} is returned for that span. 758 * 759 * @param into An array to put the results into. If you don't provide any, LayoutManager will 760 * create a new one. 761 * @see #findFirstCompletelyVisibleItemPositions(int[]) 762 * @see #findLastVisibleItemPositions(int[]) 763 */ 764 public int[] findLastCompletelyVisibleItemPositions(int[] into) { 765 if (into == null) { 766 into = new int[mSpanCount]; 767 } else if (into.length < mSpanCount) { 768 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" 769 + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); 770 } 771 for (int i = 0; i < mSpanCount; i ++) { 772 into[i] = mSpans[i].findLastCompletelyVisibleItemPosition(); 773 } 774 return into; 775 } 776 777 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, 778 int heightSpec) { 779 final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); 780 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 781 widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + insets.left, 782 lp.rightMargin + insets.right); 783 heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + insets.top, 784 lp.bottomMargin + insets.bottom); 785 child.measure(widthSpec, heightSpec); 786 } 787 788 private int updateSpecWithExtra(int spec, int startInset, int endInset) { 789 if (startInset == 0 && endInset == 0) { 790 return spec; 791 } 792 final int mode = View.MeasureSpec.getMode(spec); 793 if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { 794 return View.MeasureSpec.makeMeasureSpec( 795 View.MeasureSpec.getSize(spec) - startInset - endInset, mode); 796 } 797 return spec; 798 } 799 800 @Override 801 public void onRestoreInstanceState(Parcelable state) { 802 if (state instanceof SavedState) { 803 mPendingSavedState = (SavedState) state; 804 requestLayout(); 805 } else if (DEBUG) { 806 Log.d(TAG, "invalid saved state class"); 807 } 808 } 809 810 @Override 811 public Parcelable onSaveInstanceState() { 812 if (mPendingSavedState != null) { 813 return new SavedState(mPendingSavedState); 814 } 815 SavedState state = new SavedState(); 816 state.mOrientation = mOrientation; 817 state.mReverseLayout = mReverseLayout; 818 state.mSpanCount = mSpanCount; 819 state.mAnchorLayoutFromEnd = mLastLayoutFromEnd; 820 state.mGapStrategy = mGapStrategy; 821 822 if (mLazySpanLookup != null && mLazySpanLookup.mData != null) { 823 state.mSpanLookup = mLazySpanLookup.mData; 824 state.mSpanLookupSize = state.mSpanLookup.length; 825 } else { 826 state.mSpanLookupSize = 0; 827 } 828 829 if (getChildCount() > 0) { 830 state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition() 831 : getFirstChildPosition(); 832 state.mVisibleAnchorPosition = findFirstVisibleItemPositionInt(); 833 state.mHasSpanOffsets = true; 834 state.mSpanOffsets = new int[mSpanCount]; 835 for (int i = 0; i < mSpanCount; i++) { 836 state.mSpanOffsets[i] = mLastLayoutFromEnd ? mSpans[i].getEndLine() 837 : mSpans[i].getStartLine(); 838 } 839 } else { 840 state.mAnchorPosition = RecyclerView.NO_POSITION; 841 state.mVisibleAnchorPosition = RecyclerView.NO_POSITION; 842 state.mHasSpanOffsets = false; 843 } 844 if (DEBUG) { 845 Log.d(TAG, "saved state:\n" + state); 846 } 847 return state; 848 } 849 850 /** 851 * Finds the first fully visible child to be used as an anchor child if span count changes when 852 * state is restored. 853 */ 854 int findFirstVisibleItemPositionInt() { 855 final int start, end, diff; 856 if (mLastLayoutFromEnd) { 857 start = getChildCount() - 1; 858 end = -1; 859 diff = -1; 860 } else { 861 start = 0; 862 end = getChildCount(); 863 diff = 1; 864 } 865 final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); 866 final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); 867 for (int i = start; i != end; i += diff) { 868 final View child = getChildAt(i); 869 if (mPrimaryOrientation.getDecoratedStart(child) >= boundsStart 870 && mPrimaryOrientation.getDecoratedEnd(child) <= boundsEnd) { 871 return getPosition(child); 872 } 873 } 874 return RecyclerView.NO_POSITION; 875 } 876 877 private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state, 878 boolean canOffsetChildren) { 879 final int maxEndLine = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); 880 int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine; 881 int fixOffset; 882 if (gap > 0) { 883 fixOffset = -scrollBy(-gap, recycler, state); 884 } else { 885 return; // nothing to fix 886 } 887 gap -= fixOffset; 888 if (canOffsetChildren && gap > 0) { 889 mPrimaryOrientation.offsetChildren(gap); 890 } 891 } 892 893 private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state, 894 boolean canOffsetChildren) { 895 final int minStartLine = getMinStart(mPrimaryOrientation.getStartAfterPadding()); 896 int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding(); 897 int fixOffset; 898 if (gap > 0) { 899 fixOffset = scrollBy(gap, recycler, state); 900 } else { 901 return; // nothing to fix 902 } 903 gap -= fixOffset; 904 if (canOffsetChildren && gap > 0) { 905 mPrimaryOrientation.offsetChildren(-gap); 906 } 907 } 908 909 private void updateLayoutStateToFillStart(int anchorPosition, RecyclerView.State state) { 910 mLayoutState.mAvailable = 0; 911 mLayoutState.mCurrentPosition = anchorPosition; 912 if (isSmoothScrolling()) { 913 final int targetPos = state.getTargetScrollPosition(); 914 if (mShouldReverseLayout == targetPos < anchorPosition) { 915 mLayoutState.mExtra = 0; 916 } else { 917 mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); 918 } 919 } else { 920 mLayoutState.mExtra = 0; 921 } 922 mLayoutState.mLayoutDirection = LAYOUT_START; 923 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL 924 : ITEM_DIRECTION_HEAD; 925 } 926 927 private void updateLayoutStateToFillEnd(int anchorPosition, RecyclerView.State state) { 928 mLayoutState.mAvailable = 0; 929 mLayoutState.mCurrentPosition = anchorPosition; 930 if (isSmoothScrolling()) { 931 final int targetPos = state.getTargetScrollPosition(); 932 if (mShouldReverseLayout == targetPos > anchorPosition) { 933 mLayoutState.mExtra = 0; 934 } else { 935 mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace(); 936 } 937 } else { 938 mLayoutState.mExtra = 0; 939 } 940 mLayoutState.mLayoutDirection = LAYOUT_END; 941 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD 942 : ITEM_DIRECTION_TAIL; 943 } 944 945 @Override 946 public void offsetChildrenHorizontal(int dx) { 947 super.offsetChildrenHorizontal(dx); 948 for (int i = 0; i < mSpanCount; i++) { 949 mSpans[i].onOffset(dx); 950 } 951 } 952 953 @Override 954 public void offsetChildrenVertical(int dy) { 955 super.offsetChildrenVertical(dy); 956 for (int i = 0; i < mSpanCount; i++) { 957 mSpans[i].onOffset(dy); 958 } 959 } 960 961 @Override 962 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 963 if (!considerSpanInvalidate(positionStart, itemCount)) { 964 // If positions are not invalidated, move span offsets. 965 mLazySpanLookup.offsetForRemoval(positionStart, itemCount); 966 } 967 } 968 969 @Override 970 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 971 if (!considerSpanInvalidate(positionStart, itemCount)) { 972 // If positions are not invalidated, move span offsets. 973 mLazySpanLookup.offsetForAddition(positionStart, itemCount); 974 } 975 } 976 977 /** 978 * Checks whether it should invalidate span assignments in response to an adapter change. 979 */ 980 private boolean considerSpanInvalidate(int positionStart, int itemCount) { 981 int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition(); 982 if (positionStart + itemCount <= minPosition) { 983 return false;// nothing to update. 984 } 985 int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition(); 986 mLazySpanLookup.invalidateAfter(positionStart); 987 if (positionStart <= maxPosition) { 988 requestLayout(); 989 } 990 return true; 991 } 992 993 private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, 994 RecyclerView.State state) { 995 mRemainingSpans.set(0, mSpanCount, true); 996 // The target position we are trying to reach. 997 final int targetLine; 998 999 /* 1000 * The line until which we can recycle, as long as we add views. 1001 * Keep in mind, it is still the line in layout direction which means; to calculate the 1002 * actual recycle line, we should subtract/add the size in orientation. 1003 */ 1004 final int recycleLine; 1005 // Line of the furthest row. 1006 if (layoutState.mLayoutDirection == LAYOUT_END) { 1007 // ignore padding for recycler 1008 recycleLine = mPrimaryOrientation.getEndAfterPadding() + mLayoutState.mAvailable; 1009 targetLine = recycleLine + mLayoutState.mExtra + mPrimaryOrientation.getEndPadding(); 1010 final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); 1011 for (int i = 0; i < mSpanCount; i++) { 1012 final Span span = mSpans[i]; 1013 final int line = span.getEndLine(defaultLine); 1014 if (line > targetLine) { 1015 mRemainingSpans.set(i, false); 1016 } 1017 } 1018 } else { // LAYOUT_START 1019 // ignore padding for recycler 1020 recycleLine = mPrimaryOrientation.getStartAfterPadding() - mLayoutState.mAvailable; 1021 targetLine = recycleLine - mLayoutState.mExtra - 1022 mPrimaryOrientation.getStartAfterPadding(); 1023 for (int i = 0; i < mSpanCount; i++) { 1024 final Span span = mSpans[i]; 1025 final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); 1026 final int line = span.getStartLine(defaultLine); 1027 if (line < targetLine) { 1028 mRemainingSpans.set(i, false); 1029 } 1030 } 1031 } 1032 1033 final int widthSpec, heightSpec; 1034 if (mOrientation == VERTICAL) { 1035 widthSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); 1036 heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 1037 } else { 1038 heightSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); 1039 widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 1040 } 1041 1042 while (layoutState.hasMore(state) && !mRemainingSpans.isEmpty()) { 1043 View view = layoutState.next(recycler); 1044 LayoutParams lp = ((LayoutParams) view.getLayoutParams()); 1045 if (layoutState.mLayoutDirection == LAYOUT_END) { 1046 addView(view); 1047 } else { 1048 addView(view, 0); 1049 } 1050 if (lp.mFullSpan) { 1051 final int fullSizeSpec = View.MeasureSpec.makeMeasureSpec( 1052 mSecondaryOrientation.getTotalSpace(), View.MeasureSpec.EXACTLY); 1053 if (mOrientation == VERTICAL) { 1054 measureChildWithDecorationsAndMargin(view, fullSizeSpec, heightSpec); 1055 } else { 1056 measureChildWithDecorationsAndMargin(view, widthSpec, fullSizeSpec); 1057 } 1058 } else { 1059 measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec); 1060 } 1061 1062 final int position = getPosition(view); 1063 final int spanIndex = mLazySpanLookup.getSpan(position); 1064 Span currentSpan; 1065 if (spanIndex == LayoutParams.INVALID_SPAN_ID) { 1066 if (lp.mFullSpan) { 1067 // assign full span items to first span 1068 currentSpan = mSpans[0]; 1069 } else { 1070 currentSpan = getNextSpan(layoutState); 1071 } 1072 mLazySpanLookup.setSpan(position, currentSpan); 1073 } else { 1074 currentSpan = mSpans[spanIndex]; 1075 } 1076 final int start; 1077 final int end; 1078 if (layoutState.mLayoutDirection == LAYOUT_END) { 1079 final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding() 1080 : mPrimaryOrientation.getStartAfterPadding(); 1081 start = lp.mFullSpan ? getMaxEnd(def) : currentSpan.getEndLine(def); 1082 end = start + mPrimaryOrientation.getDecoratedMeasurement(view); 1083 if (lp.mFullSpan) { 1084 for (int i = 0; i < mSpanCount; i++) { 1085 mSpans[i].appendToSpan(view); 1086 } 1087 } else { 1088 currentSpan.appendToSpan(view); 1089 } 1090 } else { 1091 final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding() 1092 : mPrimaryOrientation.getStartAfterPadding(); 1093 end = lp.mFullSpan ? getMinStart(def) : currentSpan.getStartLine(def); 1094 start = end - mPrimaryOrientation.getDecoratedMeasurement(view); 1095 if (lp.mFullSpan) { 1096 for (int i = 0; i < mSpanCount; i++) { 1097 mSpans[i].prependToSpan(view); 1098 } 1099 } else { 1100 currentSpan.prependToSpan(view); 1101 } 1102 1103 } 1104 lp.mSpan = currentSpan; 1105 1106 if (DEBUG) { 1107 Log.d(TAG, "adding view item " + lp.getViewPosition() + " between " + start + "," 1108 + end); 1109 } 1110 1111 final int otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding() 1112 : currentSpan.mIndex * mSizePerSpan + mSecondaryOrientation 1113 .getStartAfterPadding(); 1114 final int otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view); 1115 if (mOrientation == VERTICAL) { 1116 layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end); 1117 } else { 1118 layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd); 1119 } 1120 if (lp.mFullSpan) { 1121 for (int i = 0; i < mSpanCount; i++) { 1122 updateRemainingSpans(mSpans[i], mLayoutState.mLayoutDirection, targetLine); 1123 } 1124 } else { 1125 updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine); 1126 } 1127 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 1128 // calculate recycle line 1129 int maxStart = getMaxStart(currentSpan.getStartLine()); 1130 recycleFromEnd(recycler, Math.max(recycleLine, maxStart) + 1131 (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding())); 1132 } else { 1133 // calculate recycle line 1134 int minEnd = getMinEnd(currentSpan.getEndLine()); 1135 recycleFromStart(recycler, Math.min(recycleLine, minEnd) - 1136 (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding())); 1137 } 1138 } 1139 if (DEBUG) { 1140 Log.d(TAG, "fill, " + getChildCount()); 1141 } 1142 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 1143 final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding()); 1144 return Math.max(0, mLayoutState.mAvailable + (recycleLine - minStart)); 1145 } else { 1146 final int max = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); 1147 return Math.max(0, mLayoutState.mAvailable + (max - recycleLine)); 1148 } 1149 } 1150 1151 private void layoutDecoratedWithMargins(View child, int left, int top, int right, 1152 int bottom) { 1153 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1154 layoutDecorated(child, left + lp.leftMargin, top + lp.topMargin, right - lp.rightMargin 1155 , bottom - lp.bottomMargin); 1156 } 1157 1158 private void updateRemainingSpans(Span span, int layoutDir, int targetLine) { 1159 final int deletedSize = span.getDeletedSize(); 1160 if (layoutDir == LAYOUT_START) { 1161 final int line = span.getStartLine(); 1162 if (line + deletedSize < targetLine) { 1163 mRemainingSpans.set(span.mIndex, false); 1164 } 1165 } else { 1166 final int line = span.getEndLine(); 1167 if (line - deletedSize > targetLine) { 1168 mRemainingSpans.set(span.mIndex, false); 1169 } 1170 } 1171 } 1172 1173 private int getMaxStart(int def) { 1174 int maxStart = mSpans[0].getStartLine(def); 1175 for (int i = 1; i < mSpanCount; i++) { 1176 final int spanStart = mSpans[i].getStartLine(def); 1177 if (spanStart > maxStart) { 1178 maxStart = spanStart; 1179 } 1180 } 1181 return maxStart; 1182 } 1183 1184 private int getMinStart(int def) { 1185 int minStart = mSpans[0].getStartLine(def); 1186 for (int i = 1; i < mSpanCount; i++) { 1187 final int spanStart = mSpans[i].getStartLine(def); 1188 if (spanStart < minStart) { 1189 minStart = spanStart; 1190 } 1191 } 1192 return minStart; 1193 } 1194 1195 private int getMaxEnd(int def) { 1196 int maxEnd = mSpans[0].getEndLine(def); 1197 for (int i = 1; i < mSpanCount; i++) { 1198 final int spanEnd = mSpans[i].getEndLine(def); 1199 if (spanEnd > maxEnd) { 1200 maxEnd = spanEnd; 1201 } 1202 } 1203 return maxEnd; 1204 } 1205 1206 private int getMinEnd(int def) { 1207 int minEnd = mSpans[0].getEndLine(def); 1208 for (int i = 1; i < mSpanCount; i++) { 1209 final int spanEnd = mSpans[i].getEndLine(def); 1210 if (spanEnd < minEnd) { 1211 minEnd = spanEnd; 1212 } 1213 } 1214 return minEnd; 1215 } 1216 1217 private void recycleFromStart(RecyclerView.Recycler recycler, int line) { 1218 if (DEBUG) { 1219 Log.d(TAG, "recycling from start for line " + line); 1220 } 1221 while (getChildCount() > 0) { 1222 View child = getChildAt(0); 1223 if (mPrimaryOrientation.getDecoratedEnd(child) < line) { 1224 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1225 if (lp.mFullSpan) { 1226 for (int j = 0; j < mSpanCount; j++) { 1227 mSpans[j].popStart(); 1228 } 1229 } else { 1230 lp.mSpan.popStart(); 1231 } 1232 removeAndRecycleView(child, recycler); 1233 } else { 1234 return;// done 1235 } 1236 } 1237 } 1238 1239 private void recycleFromEnd(RecyclerView.Recycler recycler, int line) { 1240 final int childCount = getChildCount(); 1241 int i; 1242 for (i = childCount - 1; i >= 0; i--) { 1243 View child = getChildAt(i); 1244 if (mPrimaryOrientation.getDecoratedStart(child) > line) { 1245 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1246 if (lp.mFullSpan) { 1247 for (int j = 0; j < mSpanCount; j++) { 1248 mSpans[j].popEnd(); 1249 } 1250 } else { 1251 lp.mSpan.popEnd(); 1252 } 1253 removeAndRecycleView(child, recycler); 1254 } else { 1255 return;// done 1256 } 1257 } 1258 } 1259 1260 /** 1261 * Finds the span for the next view. 1262 */ 1263 private Span getNextSpan(LayoutState layoutState) { 1264 final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); 1265 if (layoutState.mLayoutDirection == LAYOUT_END) { 1266 Span min = mSpans[0]; 1267 int minLine = min.getEndLine(mPrimaryOrientation.getStartAfterPadding()); 1268 final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); 1269 for (int i = 1; i < mSpanCount; i++) { 1270 final Span other = mSpans[i]; 1271 final int otherLine = other.getEndLine(defaultLine); 1272 if (otherLine < minLine || (otherLine == minLine && preferLastSpan)) { 1273 min = other; 1274 minLine = otherLine; 1275 } 1276 } 1277 return min; 1278 } else { 1279 Span max = mSpans[0]; 1280 int maxLine = max.getStartLine(mPrimaryOrientation.getEndAfterPadding()); 1281 final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); 1282 for (int i = 1; i < mSpanCount; i++) { 1283 final Span other = mSpans[i]; 1284 final int otherLine = other.getStartLine(defaultLine); 1285 if (otherLine > maxLine || (otherLine == maxLine && !preferLastSpan)) { 1286 max = other; 1287 maxLine = otherLine; 1288 } 1289 } 1290 return max; 1291 } 1292 } 1293 1294 @Override 1295 public boolean canScrollVertically() { 1296 return mOrientation == VERTICAL; 1297 } 1298 1299 @Override 1300 public boolean canScrollHorizontally() { 1301 return mOrientation == HORIZONTAL; 1302 } 1303 1304 @Override 1305 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 1306 RecyclerView.State state) { 1307 return scrollBy(dx, recycler, state); 1308 } 1309 1310 @Override 1311 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 1312 RecyclerView.State state) { 1313 return scrollBy(dy, recycler, state); 1314 } 1315 1316 private int calculateScrollDirectionForPosition(int position) { 1317 if (getChildCount() == 0) { 1318 return mShouldReverseLayout ? LAYOUT_END : LAYOUT_START; 1319 } 1320 final int firstChildPos = getFirstChildPosition(); 1321 return position < firstChildPos != mShouldReverseLayout ? LAYOUT_START : LAYOUT_END; 1322 } 1323 1324 @Override 1325 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 1326 int position) { 1327 LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { 1328 @Override 1329 public PointF computeScrollVectorForPosition(int targetPosition) { 1330 final int direction = calculateScrollDirectionForPosition(targetPosition); 1331 if (direction == 0) { 1332 return null; 1333 } 1334 if (mOrientation == HORIZONTAL) { 1335 return new PointF(direction, 0); 1336 } else { 1337 return new PointF(0, direction); 1338 } 1339 } 1340 }; 1341 scroller.setTargetPosition(position); 1342 startSmoothScroll(scroller); 1343 } 1344 1345 @Override 1346 public void scrollToPosition(int position) { 1347 if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) { 1348 mPendingSavedState.invalidateAnchorPositionInfo(); 1349 } 1350 mPendingScrollPosition = position; 1351 mPendingScrollPositionOffset = INVALID_OFFSET; 1352 requestLayout(); 1353 } 1354 1355 /** 1356 * Scroll to the specified adapter position with the given offset from layout start. 1357 * <p> 1358 * Note that scroll position change will not be reflected until the next layout call. 1359 * <p> 1360 * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. 1361 * 1362 * @param position Index (starting at 0) of the reference item. 1363 * @param offset The distance (in pixels) between the start edge of the item view and 1364 * start edge of the RecyclerView. 1365 * @see #setReverseLayout(boolean) 1366 * @see #scrollToPosition(int) 1367 */ 1368 public void scrollToPositionWithOffset(int position, int offset) { 1369 if (mPendingSavedState != null) { 1370 mPendingSavedState.invalidateAnchorPositionInfo(); 1371 } 1372 mPendingScrollPosition = position; 1373 mPendingScrollPositionOffset = offset; 1374 requestLayout(); 1375 } 1376 1377 private int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { 1378 ensureOrientationHelper(); 1379 final int referenceChildPosition; 1380 if (dt > 0) { // layout towards end 1381 mLayoutState.mLayoutDirection = LAYOUT_END; 1382 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD 1383 : ITEM_DIRECTION_TAIL; 1384 referenceChildPosition = getLastChildPosition(); 1385 } else { 1386 mLayoutState.mLayoutDirection = LAYOUT_START; 1387 mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL 1388 : ITEM_DIRECTION_HEAD; 1389 referenceChildPosition = getFirstChildPosition(); 1390 } 1391 mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; 1392 final int absDt = Math.abs(dt); 1393 mLayoutState.mAvailable = absDt; 1394 mLayoutState.mExtra = isSmoothScrolling() ? mPrimaryOrientation.getTotalSpace() : 0; 1395 int consumed = fill(recycler, mLayoutState, state); 1396 final int totalScroll; 1397 if (absDt < consumed) { 1398 totalScroll = dt; 1399 } else if (dt < 0) { 1400 totalScroll = -consumed; 1401 } else { // dt > 0 1402 totalScroll = consumed; 1403 } 1404 if (DEBUG) { 1405 Log.d(TAG, "asked " + dt + " scrolled" + totalScroll); 1406 } 1407 1408 if (mGapStrategy == GAP_HANDLING_LAZY 1409 && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD) { 1410 final int targetStart = mPrimaryOrientation.getStartAfterPadding(); 1411 final int targetEnd = mPrimaryOrientation.getEndAfterPadding(); 1412 lazyOffsetSpans(-totalScroll, targetStart, targetEnd); 1413 } else { 1414 mPrimaryOrientation.offsetChildren(-totalScroll); 1415 } 1416 // always reset this if we scroll for a proper save instance state 1417 mLastLayoutFromEnd = mShouldReverseLayout; 1418 1419 if (totalScroll != 0 && mGapStrategy != GAP_HANDLING_NONE 1420 && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD && !mHasGaps) { 1421 final int addedChildCount = Math.abs(mLayoutState.mCurrentPosition 1422 - (referenceChildPosition + mLayoutState.mItemDirection)); 1423 if (addedChildCount > 0) { 1424 // check if any child has been attached to wrong span. If so, trigger a re-layout 1425 // after scroll 1426 final View viewInWrongSpan; 1427 final View referenceView = findViewByPosition(referenceChildPosition); 1428 if (referenceView == null) { 1429 viewInWrongSpan = hasGapsToFix(0, getChildCount()); 1430 } else { 1431 if (mLayoutState.mLayoutDirection == LAYOUT_START) { 1432 viewInWrongSpan = hasGapsToFix(0, addedChildCount); 1433 } else { 1434 viewInWrongSpan = hasGapsToFix(getChildCount() - addedChildCount, 1435 getChildCount()); 1436 } 1437 } 1438 mHasGaps = viewInWrongSpan != null; 1439 } 1440 } 1441 return totalScroll; 1442 } 1443 1444 /** 1445 * The actual method that implements {@link #GAP_HANDLING_LAZY} 1446 */ 1447 private void lazyOffsetSpans(int offset, int targetStart, int targetEnd) { 1448 // For each span offset children one by one. 1449 // When a fullSpan item is reached, stop and wait for other spans to reach to that span. 1450 // When all reach, offset fullSpan to max of others and continue. 1451 int childrenToOffset = getChildCount(); 1452 int[] indexPerSpan = new int[mSpanCount]; 1453 int[] offsetPerSpan = new int[mSpanCount]; 1454 1455 final int childOrder = offset > 0 ? ITEM_DIRECTION_TAIL : ITEM_DIRECTION_HEAD; 1456 if (offset > 0) { 1457 Arrays.fill(indexPerSpan, 0); 1458 } else { 1459 for (int i = 0; i < mSpanCount; i++) { 1460 indexPerSpan[i] = mSpans[i].mViews.size() - 1; 1461 } 1462 } 1463 1464 for (int i = 0; i < mSpanCount; i++) { 1465 offsetPerSpan[i] = mSpans[i].getNormalizedOffset(offset, targetStart, targetEnd); 1466 } 1467 if (DEBUG) { 1468 Log.d(TAG, "lazy offset start. normalized: " + Arrays.toString(offsetPerSpan)); 1469 } 1470 1471 while (childrenToOffset > 0) { 1472 View fullSpanView = null; 1473 for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) { 1474 Span span = mSpans[spanIndex]; 1475 int viewIndex; 1476 for (viewIndex = indexPerSpan[spanIndex]; 1477 viewIndex < span.mViews.size() && viewIndex >= 0; viewIndex += childOrder) { 1478 View view = span.mViews.get(viewIndex); 1479 if (DEBUG) { 1480 Log.d(TAG, "span " + spanIndex + ", view:" + viewIndex + ", pos:" 1481 + getPosition(view)); 1482 } 1483 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1484 if (lp.mFullSpan) { 1485 if (DEBUG) { 1486 Log.d(TAG, "stopping on full span view on index " + viewIndex 1487 + " in span " + spanIndex); 1488 } 1489 fullSpanView = view; 1490 viewIndex += childOrder;// move to next view 1491 break; 1492 } 1493 // offset this child normally 1494 mPrimaryOrientation.offsetChild(view, offsetPerSpan[spanIndex]); 1495 final int nextChildIndex = viewIndex + childOrder; 1496 if (nextChildIndex < span.mViews.size() && nextChildIndex >= 0) { 1497 View nextView = span.mViews.get(nextChildIndex); 1498 // find gap between, before offset 1499 if (childOrder == ITEM_DIRECTION_HEAD) {// negative 1500 offsetPerSpan[spanIndex] = Math 1501 .min(0, mPrimaryOrientation.getDecoratedStart(view) 1502 - mPrimaryOrientation.getDecoratedEnd(nextView)); 1503 } else { 1504 offsetPerSpan[spanIndex] = Math 1505 .max(0, mPrimaryOrientation.getDecoratedEnd(view) - 1506 mPrimaryOrientation.getDecoratedStart(nextView)); 1507 } 1508 if (DEBUG) { 1509 Log.d(TAG, "offset diff:" + offsetPerSpan[spanIndex] + " between " 1510 + getPosition(nextView) + " and " + getPosition(view)); 1511 } 1512 } 1513 childrenToOffset--; 1514 } 1515 indexPerSpan[spanIndex] = viewIndex; 1516 } 1517 if (fullSpanView != null) { 1518 // we have to offset this view. We'll offset it as the biggest amount necessary 1519 int winnerSpan = 0; 1520 int winnerSpanOffset = Math.abs(offsetPerSpan[winnerSpan]); 1521 for (int i = 1; i < mSpanCount; i++) { 1522 final int spanOffset = Math.abs(offsetPerSpan[i]); 1523 if (spanOffset > winnerSpanOffset) { 1524 winnerSpan = i; 1525 winnerSpanOffset = spanOffset; 1526 } 1527 } 1528 if (DEBUG) { 1529 Log.d(TAG, "winner offset:" + offsetPerSpan[winnerSpan] + " of " + winnerSpan); 1530 } 1531 mPrimaryOrientation.offsetChild(fullSpanView, offsetPerSpan[winnerSpan]); 1532 childrenToOffset--; 1533 1534 for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) { 1535 final int nextViewIndex = indexPerSpan[spanIndex]; 1536 final Span span = mSpans[spanIndex]; 1537 if (nextViewIndex < span.mViews.size() && nextViewIndex > 0) { 1538 View nextView = span.mViews.get(nextViewIndex); 1539 // find gap between, before offset 1540 if (childOrder == ITEM_DIRECTION_HEAD) {// negative 1541 offsetPerSpan[spanIndex] = Math 1542 .min(0, mPrimaryOrientation.getDecoratedStart(fullSpanView) 1543 - mPrimaryOrientation.getDecoratedEnd(nextView)); 1544 } else { 1545 offsetPerSpan[spanIndex] = Math 1546 .max(0, mPrimaryOrientation.getDecoratedEnd(fullSpanView) - 1547 mPrimaryOrientation.getDecoratedStart(nextView)); 1548 } 1549 } 1550 } 1551 } 1552 } 1553 for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) { 1554 mSpans[spanIndex].invalidateCache(); 1555 } 1556 } 1557 1558 private int getLastChildPosition() { 1559 final int childCount = getChildCount(); 1560 return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1)); 1561 } 1562 1563 private int getFirstChildPosition() { 1564 final int childCount = getChildCount(); 1565 return childCount == 0 ? 0 : getPosition(getChildAt(0)); 1566 } 1567 1568 /** 1569 * Finds the first View that can be used as an anchor View. 1570 * 1571 * @return Position of the View or 0 if it cannot find any such View. 1572 */ 1573 private int findFirstReferenceChildPosition(int itemCount) { 1574 final int limit = getChildCount(); 1575 for (int i = 0; i < limit; i++) { 1576 final View view = getChildAt(i); 1577 final int position = getPosition(view); 1578 if (position >= 0 && position < itemCount) { 1579 return position; 1580 } 1581 } 1582 return 0; 1583 } 1584 1585 /** 1586 * Finds the last View that can be used as an anchor View. 1587 * 1588 * @return Position of the View or 0 if it cannot find any such View. 1589 */ 1590 private int findLastReferenceChildPosition(int itemCount) { 1591 for (int i = getChildCount() - 1; i >= 0; i--) { 1592 final View view = getChildAt(i); 1593 final int position = getPosition(view); 1594 if (position >= 0 && position < itemCount) { 1595 return position; 1596 } 1597 } 1598 return 0; 1599 } 1600 1601 @Override 1602 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 1603 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 1604 ViewGroup.LayoutParams.WRAP_CONTENT); 1605 } 1606 1607 @Override 1608 public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 1609 return new LayoutParams(c, attrs); 1610 } 1611 1612 @Override 1613 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 1614 if (lp instanceof ViewGroup.MarginLayoutParams) { 1615 return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 1616 } else { 1617 return new LayoutParams(lp); 1618 } 1619 } 1620 1621 @Override 1622 public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 1623 return lp instanceof LayoutParams; 1624 } 1625 1626 public int getOrientation() { 1627 return mOrientation; 1628 } 1629 1630 1631 /** 1632 * LayoutParams used by StaggeredGridLayoutManager. 1633 */ 1634 public static class LayoutParams extends RecyclerView.LayoutParams { 1635 1636 /** 1637 * Span Id for Views that are not laid out yet. 1638 */ 1639 public static final int INVALID_SPAN_ID = -1; 1640 1641 // Package scope to be able to access from tests. 1642 Span mSpan; 1643 1644 boolean mFullSpan; 1645 1646 public LayoutParams(Context c, AttributeSet attrs) { 1647 super(c, attrs); 1648 } 1649 1650 public LayoutParams(int width, int height) { 1651 super(width, height); 1652 } 1653 1654 public LayoutParams(ViewGroup.MarginLayoutParams source) { 1655 super(source); 1656 } 1657 1658 public LayoutParams(ViewGroup.LayoutParams source) { 1659 super(source); 1660 } 1661 1662 public LayoutParams(RecyclerView.LayoutParams source) { 1663 super(source); 1664 } 1665 1666 /** 1667 * When set to true, the item will layout using all span area. That means, if orientation 1668 * is vertical, the view will have full width; if orientation is horizontal, the view will 1669 * have full height. 1670 * 1671 * @param fullSpan True if this item should traverse all spans. 1672 */ 1673 public void setFullSpan(boolean fullSpan) { 1674 mFullSpan = fullSpan; 1675 } 1676 1677 /** 1678 * Returns the Span index to which this View is assigned. 1679 * 1680 * @return The Span index of the View. If View is not yet assigned to any span, returns 1681 * {@link #INVALID_SPAN_ID}. 1682 */ 1683 public final int getSpanIndex() { 1684 if (mSpan == null) { 1685 return INVALID_SPAN_ID; 1686 } 1687 return mSpan.mIndex; 1688 } 1689 } 1690 1691 // Package scoped to access from tests. 1692 class Span { 1693 1694 final int INVALID_LINE = Integer.MIN_VALUE; 1695 1696 private ArrayList<View> mViews = new ArrayList<View>(); 1697 1698 int mCachedStart = INVALID_LINE; 1699 1700 int mCachedEnd = INVALID_LINE; 1701 1702 int mDeletedSize = 0; 1703 1704 final int mIndex; 1705 1706 private Span(int index) { 1707 mIndex = index; 1708 } 1709 1710 int getStartLine(int def) { 1711 if (mCachedStart != INVALID_LINE) { 1712 return mCachedStart; 1713 } 1714 if (mViews.size() == 0) { 1715 return def; 1716 } 1717 mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0)); 1718 return mCachedStart; 1719 } 1720 1721 // Use this one when default value does not make sense and not having a value means a bug. 1722 int getStartLine() { 1723 if (mCachedStart != INVALID_LINE) { 1724 return mCachedStart; 1725 } 1726 mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0)); 1727 return mCachedStart; 1728 } 1729 1730 int getEndLine(int def) { 1731 if (mCachedEnd != INVALID_LINE) { 1732 return mCachedEnd; 1733 } 1734 final int size = mViews.size(); 1735 if (size == 0) { 1736 return def; 1737 } 1738 mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(size - 1)); 1739 return mCachedEnd; 1740 } 1741 1742 // Use this one when default value does not make sense and not having a value means a bug. 1743 int getEndLine() { 1744 if (mCachedEnd != INVALID_LINE) { 1745 return mCachedEnd; 1746 } 1747 mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(mViews.size() - 1)); 1748 return mCachedEnd; 1749 } 1750 1751 void prependToSpan(View view) { 1752 LayoutParams lp = getLayoutParams(view); 1753 lp.mSpan = this; 1754 mViews.add(0, view); 1755 mCachedStart = INVALID_LINE; 1756 if (mViews.size() == 1) { 1757 mCachedEnd = INVALID_LINE; 1758 } 1759 if (lp.isItemRemoved() || lp.isItemChanged()) { 1760 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 1761 } 1762 } 1763 1764 void appendToSpan(View view) { 1765 LayoutParams lp = getLayoutParams(view); 1766 lp.mSpan = this; 1767 mViews.add(view); 1768 mCachedEnd = INVALID_LINE; 1769 if (mViews.size() == 1) { 1770 mCachedStart = INVALID_LINE; 1771 } 1772 if (lp.isItemRemoved() || lp.isItemChanged()) { 1773 mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); 1774 } 1775 } 1776 1777 // Useful method to preserve positions on a re-layout. 1778 void cacheReferenceLineAndClear(boolean reverseLayout, int offset) { 1779 int reference; 1780 if (reverseLayout) { 1781 reference = getEndLine(INVALID_LINE); 1782 } else { 1783 reference = getStartLine(INVALID_LINE); 1784 } 1785 clear(); 1786 if (reference == INVALID_LINE) { 1787 return; 1788 } 1789 if (offset != INVALID_OFFSET) { 1790 reference += offset; 1791 } 1792 mCachedStart = mCachedEnd = reference; 1793 } 1794 1795 void clear() { 1796 mViews.clear(); 1797 invalidateCache(); 1798 mDeletedSize = 0; 1799 } 1800 1801 void invalidateCache() { 1802 mCachedStart = INVALID_LINE; 1803 mCachedEnd = INVALID_LINE; 1804 } 1805 1806 void setLine(int line) { 1807 mCachedEnd = mCachedStart = line; 1808 } 1809 1810 void popEnd() { 1811 final int size = mViews.size(); 1812 View end = mViews.remove(size - 1); 1813 final LayoutParams lp = getLayoutParams(end); 1814 lp.mSpan = null; 1815 if (lp.isItemRemoved() || lp.isItemChanged()) { 1816 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end); 1817 } 1818 if (size == 1) { 1819 mCachedStart = INVALID_LINE; 1820 } 1821 mCachedEnd = INVALID_LINE; 1822 } 1823 1824 void popStart() { 1825 View start = mViews.remove(0); 1826 final LayoutParams lp = getLayoutParams(start); 1827 lp.mSpan = null; 1828 if (mViews.size() == 0) { 1829 mCachedEnd = INVALID_LINE; 1830 } 1831 if (lp.isItemRemoved() || lp.isItemChanged()) { 1832 mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start); 1833 } 1834 mCachedStart = INVALID_LINE; 1835 } 1836 1837 // TODO cache this. 1838 public int getDeletedSize() { 1839 return mDeletedSize; 1840 } 1841 1842 LayoutParams getLayoutParams(View view) { 1843 return (LayoutParams) view.getLayoutParams(); 1844 } 1845 1846 void onOffset(int dt) { 1847 if (mCachedStart != INVALID_LINE) { 1848 mCachedStart += dt; 1849 } 1850 if (mCachedEnd != INVALID_LINE) { 1851 mCachedEnd += dt; 1852 } 1853 } 1854 1855 // normalized offset is how much this span can scroll 1856 int getNormalizedOffset(int dt, int targetStart, int targetEnd) { 1857 if (mViews.size() == 0) { 1858 return 0; 1859 } 1860 if (dt < 0) { 1861 final int endSpace = getEndLine() - targetEnd; 1862 if (endSpace <= 0) { 1863 return 0; 1864 } 1865 return -dt > endSpace ? -endSpace : dt; 1866 } else { 1867 final int startSpace = targetStart - getStartLine(); 1868 if (startSpace <= 0) { 1869 return 0; 1870 } 1871 return startSpace < dt ? startSpace : dt; 1872 } 1873 } 1874 1875 /** 1876 * Returns if there is no child between start-end lines 1877 * 1878 * @param start The start line 1879 * @param end The end line 1880 * @return true if a new child can be added between start and end 1881 */ 1882 boolean isEmpty(int start, int end) { 1883 final int count = mViews.size(); 1884 for (int i = 0; i < count; i++) { 1885 final View view = mViews.get(i); 1886 if (mPrimaryOrientation.getDecoratedStart(view) < end && 1887 mPrimaryOrientation.getDecoratedEnd(view) > start) { 1888 return false; 1889 } 1890 } 1891 return true; 1892 } 1893 1894 public int findFirstVisibleItemPosition() { 1895 return mReverseLayout 1896 ? findOneVisibleChild(mViews.size() - 1, -1, false) 1897 : findOneVisibleChild(0, mViews.size(), false); 1898 } 1899 1900 public int findFirstCompletelyVisibleItemPosition() { 1901 return mReverseLayout 1902 ? findOneVisibleChild(mViews.size() -1, -1, true) 1903 : findOneVisibleChild(0, mViews.size(), true); 1904 } 1905 1906 public int findLastVisibleItemPosition() { 1907 return mReverseLayout 1908 ? findOneVisibleChild(0, mViews.size(), false) 1909 : findOneVisibleChild(mViews.size() - 1, -1, false); 1910 } 1911 1912 public int findLastCompletelyVisibleItemPosition() { 1913 return mReverseLayout 1914 ? findOneVisibleChild(0, mViews.size(), true) 1915 : findOneVisibleChild(mViews.size() - 1, -1, true); 1916 } 1917 1918 int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { 1919 final int start = mPrimaryOrientation.getStartAfterPadding(); 1920 final int end = mPrimaryOrientation.getEndAfterPadding(); 1921 final int next = toIndex > fromIndex ? 1 : -1; 1922 for (int i = fromIndex; i != toIndex; i+=next) { 1923 final View child = mViews.get(i); 1924 final int childStart = mPrimaryOrientation.getDecoratedStart(child); 1925 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); 1926 if (childStart < end && childEnd > start) { 1927 if (completelyVisible) { 1928 if (childStart >= start && childEnd <= end) { 1929 return getPosition(child); 1930 } 1931 } else { 1932 return getPosition(child); 1933 } 1934 } 1935 } 1936 return RecyclerView.NO_POSITION; 1937 } 1938 } 1939 1940 /** 1941 * An array of mappings from adapter position to span. 1942 * This only grows when a write happens and it grows up to the size of the adapter. 1943 */ 1944 static class LazySpanLookup { 1945 1946 private static final int MIN_SIZE = 10; 1947 1948 int[] mData; 1949 1950 int mAdapterSize; // we don't want to grow beyond that, unless it grows 1951 1952 void invalidateAfter(int position) { 1953 if (mData == null) { 1954 return; 1955 } 1956 if (position >= mData.length) { 1957 return; 1958 } 1959 Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID); 1960 } 1961 1962 int getSpan(int position) { 1963 if (mData == null || position >= mData.length) { 1964 return LayoutParams.INVALID_SPAN_ID; 1965 } else { 1966 return mData[position]; 1967 } 1968 } 1969 1970 void setSpan(int position, Span span) { 1971 ensureSize(position); 1972 mData[position] = span.mIndex; 1973 } 1974 1975 int sizeForPosition(int position) { 1976 int len = mData.length; 1977 while (len <= position) { 1978 len *= 2; 1979 } 1980 if (len > mAdapterSize) { 1981 len = mAdapterSize; 1982 } 1983 return len; 1984 } 1985 1986 void ensureSize(int position) { 1987 if (mData == null) { 1988 mData = new int[Math.max(position, MIN_SIZE) + 1]; 1989 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 1990 } else if (position >= mData.length) { 1991 int[] old = mData; 1992 mData = new int[sizeForPosition(position)]; 1993 System.arraycopy(old, 0, mData, 0, old.length); 1994 Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID); 1995 } 1996 } 1997 1998 void clear() { 1999 if (mData != null) { 2000 Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); 2001 } 2002 } 2003 2004 void offsetForRemoval(int positionStart, int itemCount) { 2005 ensureSize(positionStart + itemCount); 2006 System.arraycopy(mData, positionStart + itemCount, mData, positionStart, 2007 mData.length - positionStart - itemCount); 2008 Arrays.fill(mData, mData.length - itemCount, mData.length, 2009 LayoutParams.INVALID_SPAN_ID); 2010 } 2011 2012 void offsetForAddition(int positionStart, int itemCount) { 2013 ensureSize(positionStart + itemCount); 2014 System.arraycopy(mData, positionStart, mData, positionStart + itemCount, 2015 mData.length - positionStart - itemCount); 2016 Arrays.fill(mData, positionStart, positionStart + itemCount, 2017 LayoutParams.INVALID_SPAN_ID); 2018 } 2019 } 2020 2021 static class SavedState implements Parcelable { 2022 2023 int mOrientation; 2024 2025 int mSpanCount; 2026 2027 int mGapStrategy; 2028 2029 int mAnchorPosition; 2030 2031 int mVisibleAnchorPosition; // if span count changes (span offsets are invalidated), 2032 // we use this one instead 2033 2034 int[] mSpanOffsets; 2035 2036 int mSpanLookupSize; 2037 2038 int[] mSpanLookup; 2039 2040 boolean mReverseLayout; 2041 2042 boolean mAnchorLayoutFromEnd; 2043 2044 boolean mHasSpanOffsets; 2045 2046 public SavedState() { 2047 } 2048 2049 SavedState(Parcel in) { 2050 mOrientation = in.readInt(); 2051 mSpanCount = in.readInt(); 2052 mGapStrategy = in.readInt(); 2053 mAnchorPosition = in.readInt(); 2054 mVisibleAnchorPosition = in.readInt(); 2055 mHasSpanOffsets = in.readInt() == 1; 2056 if (mHasSpanOffsets) { 2057 mSpanOffsets = new int[mSpanCount]; 2058 in.readIntArray(mSpanOffsets); 2059 } 2060 2061 mSpanLookupSize = in.readInt(); 2062 if (mSpanLookupSize > 0) { 2063 mSpanLookup = new int[mSpanLookupSize]; 2064 in.readIntArray(mSpanLookup); 2065 } 2066 mReverseLayout = in.readInt() == 1; 2067 mAnchorLayoutFromEnd = in.readInt() == 1; 2068 } 2069 2070 public SavedState(SavedState other) { 2071 mOrientation = other.mOrientation; 2072 mSpanCount = other.mSpanCount; 2073 mGapStrategy = other.mGapStrategy; 2074 mAnchorPosition = other.mAnchorPosition; 2075 mVisibleAnchorPosition = other.mVisibleAnchorPosition; 2076 mHasSpanOffsets = other.mHasSpanOffsets; 2077 mSpanOffsets = other.mSpanOffsets; 2078 mSpanLookupSize = other.mSpanLookupSize; 2079 mSpanLookup = other.mSpanLookup; 2080 mReverseLayout = other.mReverseLayout; 2081 mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; 2082 } 2083 2084 void invalidateSpanInfo() { 2085 mSpanOffsets = null; 2086 mHasSpanOffsets = false; 2087 mSpanCount = -1; 2088 mSpanLookupSize = 0; 2089 mSpanLookup = null; 2090 } 2091 2092 void invalidateAnchorPositionInfo() { 2093 mSpanOffsets = null; 2094 mHasSpanOffsets = false; 2095 mAnchorPosition = RecyclerView.NO_POSITION; 2096 mVisibleAnchorPosition = RecyclerView.NO_POSITION; 2097 } 2098 2099 @Override 2100 public int describeContents() { 2101 return 0; 2102 } 2103 2104 @Override 2105 public void writeToParcel(Parcel dest, int flags) { 2106 dest.writeInt(mOrientation); 2107 dest.writeInt(mSpanCount); 2108 dest.writeInt(mGapStrategy); 2109 dest.writeInt(mAnchorPosition); 2110 dest.writeInt(mVisibleAnchorPosition); 2111 dest.writeInt(mHasSpanOffsets ? 1 : 0); 2112 if (mHasSpanOffsets) { 2113 dest.writeIntArray(mSpanOffsets); 2114 } 2115 dest.writeInt(mSpanLookupSize); 2116 if (mSpanLookupSize > 0) { 2117 dest.writeIntArray(mSpanLookup); 2118 } 2119 dest.writeInt(mReverseLayout ? 1 : 0); 2120 dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); 2121 } 2122 2123 @Override 2124 public String toString() { 2125 return "SavedState{" + 2126 "mOrientation=" + mOrientation + 2127 ", mSpanCount=" + mSpanCount + 2128 ", mGapStrategy=" + mGapStrategy + 2129 ", mAnchorPosition=" + mAnchorPosition + 2130 ", mVisibleAnchorPosition=" + mVisibleAnchorPosition + 2131 ", mSpanOffsets=" + Arrays.toString(mSpanOffsets) + 2132 ", mSpanLookupSize=" + mSpanLookupSize + 2133 ", mSpanLookup=" + Arrays.toString(mSpanLookup) + 2134 ", mReverseLayout=" + mReverseLayout + 2135 ", mAnchorLayoutFromEnd=" + mAnchorLayoutFromEnd + 2136 ", mHasSpanOffsets=" + mHasSpanOffsets + 2137 '}'; 2138 } 2139 2140 public static final Parcelable.Creator<SavedState> CREATOR 2141 = new Parcelable.Creator<SavedState>() { 2142 @Override 2143 public SavedState createFromParcel(Parcel in) { 2144 return new SavedState(in); 2145 } 2146 2147 @Override 2148 public SavedState[] newArray(int size) { 2149 return new SavedState[size]; 2150 } 2151 }; 2152 } 2153} 2154