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