LinearLayoutManager.java revision 310e95e1c6dfe4f26ef594233e65e1ff83e0f1ff
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 languag`e governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.widget; 18 19import android.content.Context; 20import android.os.Parcel; 21import android.os.Parcelable; 22import android.graphics.PointF; 23import android.support.v4.view.ViewCompat; 24import android.util.Log; 25import android.view.View; 26import android.view.ViewGroup; 27 28import java.util.List; 29 30/** 31 * A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation which provides 32 * similar functionality to {@link android.widget.ListView}. 33 */ 34public class LinearLayoutManager extends RecyclerView.LayoutManager { 35 36 private static final String TAG = "LinearLayoutManager"; 37 38 private static final boolean DEBUG = false; 39 40 public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; 41 42 public static final int VERTICAL = OrientationHelper.VERTICAL; 43 44 public static final int INVALID_OFFSET = Integer.MIN_VALUE; 45 46 47 /** 48 * While trying to find next view to focus, LinearLayoutManager will not try to scroll more 49 * than 50 * this factor times the total space of the list. If layout is vertical, total space is the 51 * height minus padding, if layout is horizontal, total space is the width minus padding. 52 */ 53 private static final float MAX_SCROLL_FACTOR = 0.33f; 54 55 56 /** 57 * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} 58 */ 59 private int mOrientation; 60 61 /** 62 * Helper class that keeps temporary layout state. 63 * It does not keep state after layout is complete but we still keep a reference to re-use 64 * the same object. 65 */ 66 private LayoutState mLayoutState; 67 68 /** 69 * Many calculations are made depending on orientation. To keep it clean, this interface 70 * helps {@link LinearLayoutManager} make those decisions. 71 * Based on {@link #mOrientation}, an implementation is lazily created in 72 * {@link #ensureLayoutState} method. 73 */ 74 OrientationHelper mOrientationHelper; 75 76 /** 77 * We need to track this so that we can ignore current position when it changes. 78 */ 79 private boolean mLastStackFromEnd; 80 81 82 /** 83 * Defines if layout should be calculated from end to start. 84 * 85 * @see #mShouldReverseLayout 86 */ 87 private boolean mReverseLayout = false; 88 89 /** 90 * This keeps the final value for how LayoutManager should start laying out views. 91 * It is calculated by checking {@link #getReverseLayout()} and View's layout direction. 92 * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run. 93 */ 94 private boolean mShouldReverseLayout = false; 95 96 /** 97 * Works the same way as {@link android.widget.AbsListView#setStackFromBottom(boolean)} and 98 * it supports both orientations. 99 * see {@link android.widget.AbsListView#setStackFromBottom(boolean)} 100 */ 101 private boolean mStackFromEnd = false; 102 103 /** 104 * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. 105 * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} 106 */ 107 private boolean mSmoothScrollbarEnabled = true; 108 109 /** 110 * When LayoutManager needs to scroll to a position, it sets this variable and requests a 111 * layout which will check this variable and re-layout accordingly. 112 */ 113 int mPendingScrollPosition = RecyclerView.NO_POSITION; 114 115 /** 116 * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is 117 * called. 118 */ 119 int mPendingScrollPositionOffset = INVALID_OFFSET; 120 121 private boolean mRecycleChildrenOnDetach; 122 123 SavedState mPendingSavedState = null; 124 125 /** 126 * Re-used variable to keep anchor information on re-layout. 127 * Anchor position and coordinate defines the reference point for LLM while doing a layout. 128 * */ 129 final AnchorInfo mAnchorInfo; 130 131 /** 132 * Creates a vertical LinearLayoutManager 133 * 134 * @param context Current context, will be used to access resources. 135 */ 136 public LinearLayoutManager(Context context) { 137 this(context, VERTICAL, false); 138 } 139 140 /** 141 * @param context Current context, will be used to access resources. 142 * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link 143 * #VERTICAL}. 144 * @param reverseLayout When set to true, layouts from end to start. 145 */ 146 public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) { 147 mAnchorInfo = new AnchorInfo(); 148 setOrientation(orientation); 149 setReverseLayout(reverseLayout); 150 } 151 152 /** 153 * {@inheritDoc} 154 */ 155 @Override 156 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 157 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 158 ViewGroup.LayoutParams.WRAP_CONTENT); 159 } 160 161 /** 162 * Returns whether LinearLayoutManager will recycle its children when it is detached from 163 * RecyclerView. 164 * 165 * @return true if LinearLayoutManager will recycle its children when it is detached from 166 * RecyclerView. 167 */ 168 public boolean getRecycleChildrenOnDetach() { 169 return mRecycleChildrenOnDetach; 170 } 171 172 /** 173 * Set whether LinearLayoutManager will recycle its children when it is detached from 174 * RecyclerView. 175 * <p> 176 * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set 177 * this flag to <code>true</code> so that views will be avilable to other RecyclerViews 178 * immediately. 179 * <p> 180 * Note that, setting this flag will result in a performance drop if RecyclerView 181 * is restored. 182 * 183 * @param recycleChildrenOnDetach Whether children should be recycled in detach or not. 184 */ 185 public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) { 186 if (mPendingSavedState != null && 187 mPendingSavedState.mRecycleChildrenOnDetach != recycleChildrenOnDetach) { 188 // override pending state 189 mPendingSavedState.mRecycleChildrenOnDetach = recycleChildrenOnDetach; 190 } 191 mRecycleChildrenOnDetach = recycleChildrenOnDetach; 192 } 193 194 @Override 195 public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 196 super.onDetachedFromWindow(view, recycler); 197 if (mRecycleChildrenOnDetach) { 198 removeAndRecycleAllViews(recycler); 199 recycler.clear(); 200 } 201 } 202 203 @Override 204 public Parcelable onSaveInstanceState() { 205 if (mPendingSavedState != null) { 206 return new SavedState(mPendingSavedState); 207 } 208 SavedState state = new SavedState(); 209 state.mRecycleChildrenOnDetach = mRecycleChildrenOnDetach; 210 if (getChildCount() > 0) { 211 boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout; 212 state.mAnchorLayoutFromEnd = didLayoutFromEnd; 213 if (didLayoutFromEnd) { 214 final View refChild = getChildClosestToEnd(); 215 state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() - 216 mOrientationHelper.getDecoratedEnd(refChild); 217 state.mAnchorPosition = getPosition(refChild); 218 } else { 219 final View refChild = getChildClosestToStart(); 220 state.mAnchorPosition = getPosition(refChild); 221 state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild) - 222 mOrientationHelper.getStartAfterPadding(); 223 } 224 } else { 225 state.invalidateAnchor(); 226 } 227 state.mStackFromEnd = mStackFromEnd; 228 state.mReverseLayout = mReverseLayout; 229 state.mOrientation = mOrientation; 230 state.mSmoothScrollbarEnabled = mSmoothScrollbarEnabled; 231 return state; 232 } 233 234 @Override 235 public void onRestoreInstanceState(Parcelable state) { 236 if (state instanceof SavedState) { 237 mPendingSavedState = (SavedState) state; 238 requestLayout(); 239 if (DEBUG) { 240 Log.d(TAG, "loaded saved state"); 241 } 242 } else if (DEBUG) { 243 Log.d(TAG, "invalid saved state class"); 244 } 245 } 246 247 /** 248 * @return true if {@link #getOrientation()} is {@link #HORIZONTAL} 249 */ 250 @Override 251 public boolean canScrollHorizontally() { 252 return mOrientation == HORIZONTAL; 253 } 254 255 /** 256 * @return true if {@link #getOrientation()} is {@link #VERTICAL} 257 */ 258 @Override 259 public boolean canScrollVertically() { 260 return mOrientation == VERTICAL; 261 } 262 263 /** 264 * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)} 265 */ 266 public void setStackFromEnd(boolean stackFromEnd) { 267 assertNotInLayoutOrScroll(null); 268 if (mPendingSavedState != null && mPendingSavedState.mStackFromEnd != stackFromEnd) { 269 // override pending state 270 mPendingSavedState.mStackFromEnd = stackFromEnd; 271 } 272 if (mStackFromEnd == stackFromEnd) { 273 return; 274 } 275 mStackFromEnd = stackFromEnd; 276 requestLayout(); 277 } 278 279 public boolean getStackFromEnd() { 280 return mStackFromEnd; 281 } 282 283 /** 284 * Returns the current orientaion of the layout. 285 * 286 * @return Current orientation. 287 * @see #mOrientation 288 * @see #setOrientation(int) 289 */ 290 public int getOrientation() { 291 return mOrientation; 292 } 293 294 /** 295 * Sets the orientation of the layout. {@link android.support.v7.widget.LinearLayoutManager} 296 * will do its best to keep scroll position. 297 * 298 * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} 299 */ 300 public void setOrientation(int orientation) { 301 if (orientation != HORIZONTAL && orientation != VERTICAL) { 302 throw new IllegalArgumentException("invalid orientation:" + orientation); 303 } 304 assertNotInLayoutOrScroll(null); 305 if (mPendingSavedState != null && mPendingSavedState.mOrientation != orientation) { 306 // override pending state 307 mPendingSavedState.mOrientation = orientation; 308 } 309 if (orientation == mOrientation) { 310 return; 311 } 312 mOrientation = orientation; 313 mOrientationHelper = null; 314 requestLayout(); 315 } 316 317 /** 318 * Calculates the view layout order. (e.g. from end to start or start to end) 319 * RTL layout support is applied automatically. So if layout is RTL and 320 * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. 321 */ 322 private void resolveShouldLayoutReverse() { 323 // A == B is the same result, but we rather keep it readable 324 if (mOrientation == VERTICAL || !isLayoutRTL()) { 325 mShouldReverseLayout = mReverseLayout; 326 } else { 327 mShouldReverseLayout = !mReverseLayout; 328 } 329 } 330 331 /** 332 * Returns if views are laid out from the opposite direction of the layout. 333 * 334 * @return If layout is reversed or not. 335 * @see {@link #setReverseLayout(boolean)} 336 */ 337 public boolean getReverseLayout() { 338 return mReverseLayout; 339 } 340 341 /** 342 * Used to reverse item traversal and layout order. 343 * This behaves similar to the layout change for RTL views. When set to true, first item is 344 * laid out at the end of the UI, second item is laid out before it etc. 345 * 346 * For horizontal layouts, it depends on the layout direction. 347 * When set to true, If {@link android.support.v7.widget.RecyclerView} is LTR, than it will 348 * layout from RTL, if {@link android.support.v7.widget.RecyclerView}} is RTL, it will layout 349 * from LTR. 350 * 351 * If you are looking for the exact same behavior of 352 * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use 353 * {@link #setStackFromEnd(boolean)} 354 */ 355 public void setReverseLayout(boolean reverseLayout) { 356 assertNotInLayoutOrScroll(null); 357 if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) { 358 // override pending state 359 mPendingSavedState.mReverseLayout = reverseLayout; 360 } 361 if (reverseLayout == mReverseLayout) { 362 return; 363 } 364 mReverseLayout = reverseLayout; 365 requestLayout(); 366 } 367 368 /** 369 * {@inheritDoc} 370 */ 371 @Override 372 public View findViewByPosition(int position) { 373 final int childCount = getChildCount(); 374 if (childCount == 0) { 375 return null; 376 } 377 final int firstChild = getPosition(getChildAt(0)); 378 final int viewPosition = position - firstChild; 379 if (viewPosition >= 0 && viewPosition < childCount) { 380 return getChildAt(viewPosition); 381 } 382 return null; 383 } 384 385 /** 386 * <p>Returns the amount of extra space that should be laid out by LinearLayoutManager. 387 * By default, {@link android.support.v7.widget.LinearLayoutManager} lays out 1 extra page of 388 * items while smooth scrolling and 0 otherwise. You can override this method to implement your 389 * custom layout pre-cache logic.</p> 390 * <p>Laying out invisible elements will eventually come with performance cost. On the other 391 * hand, in places like smooth scrolling to an unknown location, this extra content helps 392 * LayoutManager to calculate a much smoother scrolling; which improves user experience.</p> 393 * <p>You can also use this if you are trying to pre-layout your upcoming views.</p> 394 * 395 * @return The extra space that should be laid out (in pixels). 396 */ 397 protected int getExtraLayoutSpace(RecyclerView.State state) { 398 if (state.hasTargetScrollPosition()) { 399 return mOrientationHelper.getTotalSpace(); 400 } else { 401 return 0; 402 } 403 } 404 405 @Override 406 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 407 int position) { 408 LinearSmoothScroller linearSmoothScroller = 409 new LinearSmoothScroller(recyclerView.getContext()) { 410 @Override 411 public PointF computeScrollVectorForPosition(int targetPosition) { 412 return LinearLayoutManager.this 413 .computeScrollVectorForPosition(targetPosition); 414 } 415 }; 416 linearSmoothScroller.setTargetPosition(position); 417 startSmoothScroll(linearSmoothScroller); 418 } 419 420 public PointF computeScrollVectorForPosition(int targetPosition) { 421 if (getChildCount() == 0) { 422 return null; 423 } 424 final int firstChildPos = getPosition(getChildAt(0)); 425 final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1; 426 if (mOrientation == HORIZONTAL) { 427 return new PointF(direction, 0); 428 } else { 429 return new PointF(0, direction); 430 } 431 } 432 433 /** 434 * {@inheritDoc} 435 */ 436 @Override 437 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 438 // layout algorithm: 439 // 1) by checking children and other variables, find an anchor coordinate and an anchor 440 // item position. 441 // 2) fill towards start, stacking from bottom 442 // 3) fill towards end, stacking from top 443 // 4) scroll to fulfill requirements like stack from bottom. 444 // create layout state 445 if (DEBUG) { 446 Log.d(TAG, "is pre layout:" + state.isPreLayout()); 447 } 448 if (mPendingSavedState != null) { 449 setOrientation(mPendingSavedState.mOrientation); 450 setReverseLayout(mPendingSavedState.mReverseLayout); 451 setStackFromEnd(mPendingSavedState.mStackFromEnd); 452 setRecycleChildrenOnDetach(mPendingSavedState.mRecycleChildrenOnDetach); 453 setSmoothScrollbarEnabled(mPendingSavedState.mSmoothScrollbarEnabled); 454 if (mPendingSavedState.hasValidAnchor()) { 455 mPendingScrollPosition = mPendingSavedState.mAnchorPosition; 456 } 457 } 458 459 ensureLayoutState(); 460 // resolve layout direction 461 resolveShouldLayoutReverse(); 462 463 mAnchorInfo.reset(); 464 mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; 465 // calculate anchor position and coordinate 466 updateAnchorInfoForLayout(state, mAnchorInfo); 467 if (DEBUG) { 468 Log.d(TAG, "Anchor info:" + mAnchorInfo); 469 } 470 471 detachAndScrapAttachedViews(recycler); 472 473 // LLM may decide to layout items for "extra" pixels to account for scrolling target, 474 // caching or predictive animations. 475 int extraForStart; 476 int extraForEnd; 477 final int extra = getExtraLayoutSpace(state); 478 boolean before = state.getTargetScrollPosition() < mAnchorInfo.mPosition; 479 if (before == mShouldReverseLayout) { 480 extraForEnd = extra; 481 extraForStart = 0; 482 } else { 483 extraForStart = extra; 484 extraForEnd = 0; 485 } 486 extraForStart += mOrientationHelper.getStartAfterPadding(); 487 extraForEnd += mOrientationHelper.getEndPadding(); 488 int startOffset; 489 int endOffset; 490 if (mAnchorInfo.mLayoutFromEnd) { 491 // fill towards start 492 updateLayoutStateToFillStart(mAnchorInfo); 493 mLayoutState.mExtra = extraForStart; 494 fill(recycler, mLayoutState, state, false); 495 startOffset = mLayoutState.mOffset; 496 if (mLayoutState.mAvailable > 0) { 497 extraForEnd += mLayoutState.mAvailable; 498 } 499 // fill towards end 500 updateLayoutStateToFillEnd(mAnchorInfo); 501 mLayoutState.mExtra = extraForEnd; 502 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 503 fill(recycler, mLayoutState, state, false); 504 endOffset = mLayoutState.mOffset; 505 } else { 506 // fill towards end 507 updateLayoutStateToFillEnd(mAnchorInfo); 508 mLayoutState.mExtra = extraForEnd; 509 fill(recycler, mLayoutState, state, false); 510 endOffset = mLayoutState.mOffset; 511 if (mLayoutState.mAvailable > 0) { 512 extraForStart += mLayoutState.mAvailable; 513 } 514 // fill towards start 515 updateLayoutStateToFillStart(mAnchorInfo); 516 mLayoutState.mExtra = extraForStart; 517 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 518 fill(recycler, mLayoutState, state, false); 519 startOffset = mLayoutState.mOffset; 520 } 521 522 // changes may cause gaps on the UI, try to fix them. 523 // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have 524 // changed 525 if (getChildCount() > 0) { 526 // because layout from end may be changed by scroll to position 527 // we re-calculate it. 528 // find which side we should check for gaps. 529 if (mShouldReverseLayout ^ mStackFromEnd) { 530 int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true); 531 startOffset += fixOffset; 532 endOffset += fixOffset; 533 fixOffset = fixLayoutStartGap(startOffset, recycler, state, false); 534 startOffset += fixOffset; 535 endOffset += fixOffset; 536 } else { 537 int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true); 538 startOffset += fixOffset; 539 endOffset += fixOffset; 540 fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); 541 startOffset += fixOffset; 542 endOffset += fixOffset; 543 } 544 } 545 layoutForPredictiveAnimations(recycler, state, startOffset, endOffset); 546 if (!state.isPreLayout()) { 547 mPendingScrollPosition = RecyclerView.NO_POSITION; 548 mPendingScrollPositionOffset = INVALID_OFFSET; 549 mOrientationHelper.onLayoutComplete(); 550 } 551 mLastStackFromEnd = mStackFromEnd; 552 mPendingSavedState = null; // we don't need this anymore 553 if (DEBUG) { 554 validateChildOrder(); 555 } 556 } 557 558 /** 559 * If necessary, layouts new items for predictive animations 560 */ 561 private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler, 562 RecyclerView.State state, int startOffset, int endOffset) { 563 // If there are scrap children that we did not layout, we need to find where they did go 564 // and layout them accordingly so that animations can work as expected. 565 // This case may happen if new views are added or an existing view expands and pushes 566 // another view out of bounds. 567 if (!state.willRunPredictiveAnimations() || getChildCount() == 0 || state.isPreLayout()) { 568 return; 569 } 570 571 // to make the logic simpler, we calculate the size of children and call fill. 572 int scrapExtraStart = 0, scrapExtraEnd = 0; 573 final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList(); 574 final int scrapSize = scrapList.size(); 575 final int firstChildPos = getPosition(getChildAt(0)); 576 for (int i = 0; i < scrapSize; i++) { 577 RecyclerView.ViewHolder scrap = scrapList.get(i); 578 final int position = scrap.getPosition(); 579 final int direction = position < firstChildPos != mShouldReverseLayout 580 ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; 581 if (direction == LayoutState.LAYOUT_START) { 582 scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); 583 } else { 584 scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); 585 } 586 } 587 588 if (DEBUG) { 589 Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart 590 + " towards start and " + scrapExtraEnd + " towards end"); 591 } 592 mLayoutState.mScrapList = scrapList; 593 if (scrapExtraStart > 0) { 594 View anchor = getChildClosestToStart(); 595 updateLayoutStateToFillStart(getPosition(anchor), startOffset); 596 mLayoutState.mExtra = scrapExtraStart; 597 mLayoutState.mAvailable = 0; 598 mLayoutState.mCurrentPosition += mShouldReverseLayout ? 1 : -1; 599 fill(recycler, mLayoutState, state, false); 600 } 601 602 if (scrapExtraEnd > 0) { 603 View anchor = getChildClosestToEnd(); 604 updateLayoutStateToFillEnd(getPosition(anchor), endOffset); 605 mLayoutState.mExtra = scrapExtraEnd; 606 mLayoutState.mAvailable = 0; 607 mLayoutState.mCurrentPosition += mShouldReverseLayout ? -1 : 1; 608 fill(recycler, mLayoutState, state, false); 609 } 610 mLayoutState.mScrapList = null; 611 } 612 613 private void updateAnchorInfoForLayout(RecyclerView.State state, AnchorInfo anchorInfo) { 614 if (updateAnchorFromPendingData(state, anchorInfo)) { 615 if (DEBUG) { 616 Log.d(TAG, "updated anchor info from pending information"); 617 } 618 return; 619 } 620 621 if (updateAnchorFromChildren(state, anchorInfo)) { 622 if (DEBUG) { 623 Log.d(TAG, "updated anchor info from existing children"); 624 } 625 return; 626 } 627 if (DEBUG) { 628 Log.d(TAG, "deciding anchor info for fresh state"); 629 } 630 anchorInfo.assignCoordinateFromPadding(); 631 anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; 632 } 633 634 /** 635 * Finds an anchor child from existing Views. Most of the time, this is the view closest to 636 * start or end that has a valid position (e.g. not removed). 637 * <p> 638 * If a child has focus, it is given priority. 639 */ 640 private boolean updateAnchorFromChildren(RecyclerView.State state, AnchorInfo anchorInfo) { 641 if (getChildCount() == 0) { 642 return false; 643 } 644 View focused = findItemWhichHasFocus(); 645 if (focused != null && anchorInfo.assignFromViewIfValid(focused, state)) { 646 if (DEBUG) { 647 Log.d(TAG, "decided anchor child from focused view"); 648 } 649 return true; 650 } 651 652 if (mLastStackFromEnd != mStackFromEnd) { 653 return false; 654 } 655 656 View referenceChild = anchorInfo.mLayoutFromEnd ? findReferenceChildClosestToEnd(state) 657 : findReferenceChildClosestToStart(state); 658 if (referenceChild != null) { 659 anchorInfo.assignFromView(referenceChild); 660 return true; 661 } 662 return false; 663 } 664 665 /** 666 * If there is a pending scroll position or saved states, updates the anchor info from that 667 * data and returns true 668 */ 669 private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { 670 if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) { 671 return false; 672 } 673 // validate scroll position 674 if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { 675 mPendingScrollPosition = RecyclerView.NO_POSITION; 676 mPendingScrollPositionOffset = INVALID_OFFSET; 677 if (DEBUG) { 678 Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition); 679 } 680 return false; 681 } 682 683 // if child is visible, try to make it a reference child and ensure it is fully visible. 684 // if child is not visible, align it depending on its virtual position. 685 anchorInfo.mPosition = mPendingScrollPosition; 686 if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { 687 // Anchor offset depends on how that child was laid out. Here, we update it 688 // according to our current view bounds 689 anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; 690 if (anchorInfo.mLayoutFromEnd) { 691 anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - 692 mPendingSavedState.mAnchorOffset; 693 } else { 694 anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + 695 mPendingSavedState.mAnchorOffset; 696 } 697 return true; 698 } 699 700 if (mPendingScrollPositionOffset == INVALID_OFFSET) { 701 View child = findViewByPosition(mPendingScrollPosition); 702 if (child != null) { 703 final int childSize = mOrientationHelper.getDecoratedMeasurement(child); 704 if (childSize > mOrientationHelper.getTotalSpace()) { 705 // item does not fit. fix depending on layout direction 706 anchorInfo.assignCoordinateFromPadding(); 707 return true; 708 } 709 final int startGap = mOrientationHelper.getDecoratedStart(child) 710 - mOrientationHelper.getStartAfterPadding(); 711 if (startGap < 0) { 712 anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding(); 713 anchorInfo.mLayoutFromEnd = false; 714 return true; 715 } 716 final int endGap = mOrientationHelper.getEndAfterPadding() - 717 mOrientationHelper.getDecoratedEnd(child); 718 if (endGap < 0) { 719 anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding(); 720 anchorInfo.mLayoutFromEnd = true; 721 return true; 722 } 723 anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd 724 ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper 725 .getTotalSpaceChange()) 726 : mOrientationHelper.getDecoratedStart(child); 727 } else { // item is not visible. 728 if (getChildCount() > 0) { 729 // get position of any child, does not matter 730 int pos = getPosition(getChildAt(0)); 731 anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos 732 == mShouldReverseLayout; 733 } 734 anchorInfo.assignCoordinateFromPadding(); 735 } 736 return true; 737 } 738 // override layout from end values for consistency 739 anchorInfo.mLayoutFromEnd = mShouldReverseLayout; 740 anchorInfo.mCoordinate = mPendingScrollPositionOffset; 741 return true; 742 } 743 744 /** 745 * @return The final offset amount for children 746 */ 747 private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler, 748 RecyclerView.State state, boolean canOffsetChildren) { 749 int gap = mOrientationHelper.getEndAfterPadding() - endOffset; 750 int fixOffset = 0; 751 if (gap > 0) { 752 fixOffset = -scrollBy(-gap, recycler, state); 753 } else { 754 return 0; // nothing to fix 755 } 756 // move offset according to scroll amount 757 endOffset += fixOffset; 758 if (canOffsetChildren) { 759 // re-calculate gap, see if we could fix it 760 gap = mOrientationHelper.getEndAfterPadding() - endOffset; 761 if (gap > 0) { 762 mOrientationHelper.offsetChildren(gap); 763 return gap + fixOffset; 764 } 765 } 766 return fixOffset; 767 } 768 769 /** 770 * @return The final offset amount for children 771 */ 772 private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler, 773 RecyclerView.State state, boolean canOffsetChildren) { 774 int gap = startOffset - mOrientationHelper.getStartAfterPadding(); 775 int fixOffset = 0; 776 if (gap > 0) { 777 // check if we should fix this gap. 778 fixOffset = -scrollBy(gap, recycler, state); 779 } else { 780 return 0; // nothing to fix 781 } 782 startOffset += fixOffset; 783 if (canOffsetChildren) { 784 // re-calculate gap, see if we could fix it 785 gap = startOffset - mOrientationHelper.getStartAfterPadding(); 786 if (gap > 0) { 787 mOrientationHelper.offsetChildren(-gap); 788 return fixOffset - gap; 789 } 790 } 791 return fixOffset; 792 } 793 794 private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) { 795 updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate); 796 } 797 798 private void updateLayoutStateToFillEnd(int itemPosition, int offset) { 799 mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset; 800 mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : 801 LayoutState.ITEM_DIRECTION_TAIL; 802 mLayoutState.mCurrentPosition = itemPosition; 803 mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END; 804 mLayoutState.mOffset = offset; 805 mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN; 806 } 807 808 private void updateLayoutStateToFillStart(AnchorInfo anchorInfo) { 809 updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate); 810 } 811 812 private void updateLayoutStateToFillStart(int itemPosition, int offset) { 813 mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding(); 814 mLayoutState.mCurrentPosition = itemPosition; 815 mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : 816 LayoutState.ITEM_DIRECTION_HEAD; 817 mLayoutState.mLayoutDirection = LayoutState.LAYOUT_START; 818 mLayoutState.mOffset = offset; 819 mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN; 820 821 } 822 823 824 private boolean isLayoutRTL() { 825 return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; 826 } 827 828 void ensureLayoutState() { 829 if (mLayoutState == null) { 830 mLayoutState = new LayoutState(); 831 } 832 if (mOrientationHelper == null) { 833 mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); 834 } 835 } 836 837 /** 838 * <p>Scroll the RecyclerView to make the position visible.</p> 839 * 840 * <p>RecyclerView will scroll the minimum amount that is necessary to make the 841 * target position visible. If you are looking for a similar behavior to 842 * {@link android.widget.ListView#setSelection(int)} or 843 * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use 844 * {@link #scrollToPositionWithOffset(int, int)}.</p> 845 * 846 * <p>Note that scroll position change will not be reflected until the next layout call.</p> 847 * 848 * @param position Scroll to this adapter position 849 * @see #scrollToPositionWithOffset(int, int) 850 */ 851 @Override 852 public void scrollToPosition(int position) { 853 mPendingScrollPosition = position; 854 mPendingScrollPositionOffset = INVALID_OFFSET; 855 if (mPendingSavedState != null) { 856 mPendingSavedState.invalidateAnchor(); 857 } 858 requestLayout(); 859 } 860 861 /** 862 * <p>Scroll to the specified adapter position with the given offset from layout start.</p> 863 * 864 * <p>Note that scroll position change will not be reflected until the next layout call.</p> 865 * 866 * <p>If you are just trying to make a position visible, use {@link 867 * #scrollToPosition(int)}.</p> 868 * 869 * @param position Index (starting at 0) of the reference item. 870 * @param offset The distance (in pixels) between the start edge of the item view and 871 * start edge of the RecyclerView. 872 * @see #setReverseLayout(boolean) 873 * @see #scrollToPosition(int) 874 */ 875 public void scrollToPositionWithOffset(int position, int offset) { 876 mPendingScrollPosition = position; 877 mPendingScrollPositionOffset = offset; 878 if (mPendingSavedState != null) { 879 mPendingSavedState.invalidateAnchor(); 880 } 881 requestLayout(); 882 } 883 884 885 /** 886 * {@inheritDoc} 887 */ 888 @Override 889 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 890 RecyclerView.State state) { 891 if (mOrientation == VERTICAL) { 892 return 0; 893 } 894 return scrollBy(dx, recycler, state); 895 } 896 897 /** 898 * {@inheritDoc} 899 */ 900 @Override 901 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 902 RecyclerView.State state) { 903 if (mOrientation == HORIZONTAL) { 904 return 0; 905 } 906 return scrollBy(dy, recycler, state); 907 } 908 909 @Override 910 public int computeHorizontalScrollOffset(RecyclerView.State state) { 911 return computeScrollOffset(state); 912 } 913 914 @Override 915 public int computeVerticalScrollOffset(RecyclerView.State state) { 916 return computeScrollOffset(state); 917 } 918 919 @Override 920 public int computeHorizontalScrollExtent(RecyclerView.State state) { 921 return computeScrollExtent(state); 922 } 923 924 @Override 925 public int computeVerticalScrollExtent(RecyclerView.State state) { 926 return computeScrollExtent(state); 927 } 928 929 @Override 930 public int computeHorizontalScrollRange(RecyclerView.State state) { 931 return computeScrollRange(state); 932 } 933 934 @Override 935 public int computeVerticalScrollRange(RecyclerView.State state) { 936 return computeScrollRange(state); 937 } 938 939 private int computeScrollOffset(RecyclerView.State state) { 940 if (getChildCount() == 0 || state.getItemCount() == 0) { 941 return 0; 942 } 943 final View startChild = getChildClosestToStart(); 944 final int itemsBefore = mShouldReverseLayout 945 ? Math.max(0, state.getItemCount() - getPosition(startChild) - 1) 946 : Math.max(0, getPosition(startChild) - 1); 947 if (!mSmoothScrollbarEnabled) { 948 return itemsBefore; 949 } 950 final View endChild = getChildClosestToEnd(); 951 final int laidOutArea = mOrientationHelper.getDecoratedEnd(endChild) - 952 mOrientationHelper.getDecoratedStart(startChild); 953 final int laidOutRange = Math.abs(getPosition(startChild) - getPosition(endChild)) + 1; 954 final float avgSizePerRow = (float) laidOutArea / laidOutRange; 955 956 return Math.round(itemsBefore * avgSizePerRow + (mOrientationHelper.getStartAfterPadding() 957 - mOrientationHelper.getDecoratedStart(startChild))); 958 } 959 960 private int computeScrollExtent(RecyclerView.State state) { 961 if (state.getItemCount() == 0 || getChildCount() == 0) { 962 return 1; 963 } 964 if (!mSmoothScrollbarEnabled) { 965 return findLastVisibleItemPosition() - findFirstVisibleItemPosition() + 1; 966 } 967 final View firstVisible = findOneVisibleChild(0, getChildCount(), false); 968 final View lastVisible = findOneVisibleChild(getChildCount() - 1, -1, false); 969 if (firstVisible == null || lastVisible == null) { 970 return 1; 971 } 972 final int extend; 973 if (mShouldReverseLayout) { 974 // first visible is below 975 extend = mOrientationHelper.getDecoratedEnd(firstVisible) - 976 mOrientationHelper.getDecoratedStart(lastVisible); 977 } else { 978 extend = mOrientationHelper.getDecoratedEnd(lastVisible) - 979 mOrientationHelper.getDecoratedStart(firstVisible); 980 } 981 return Math.min(mOrientationHelper.getTotalSpace(), extend); 982 } 983 984 private int computeScrollRange(RecyclerView.State state) { 985 if (state.getItemCount() == 0 || getChildCount() == 0) { 986 return 0; 987 } 988 if (!mSmoothScrollbarEnabled) { 989 return state.getItemCount(); 990 } 991 // smooth scrollbar enabled. try to estimate better. 992 final View startChild = getChildClosestToStart(); 993 final View endChild = getChildClosestToEnd(); 994 final int laidOutArea = mOrientationHelper.getDecoratedEnd(endChild) - 995 mOrientationHelper.getDecoratedStart(startChild); 996 final int laidOutRange = Math.abs(getPosition(startChild) - getPosition(endChild)) + 1; 997 // estimate a size for full list. 998 return (int) ((float)laidOutArea / laidOutRange * state.getItemCount()); 999 } 1000 1001 /** 1002 * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed 1003 * based on the number of visible pixels in the visible items. This however assumes that all 1004 * list items have similar or equal widths or heights (depending on list orientation). 1005 * If you use a list in which items have different dimensions, the scrollbar will change 1006 * appearance as the user scrolls through the list. To avoid this issue, you need to disable 1007 * this property. 1008 * 1009 * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based 1010 * solely on the number of items in the adapter and the position of the visible items inside 1011 * the adapter. This provides a stable scrollbar as the user navigates through a list of items 1012 * with varying widths / heights. 1013 * 1014 * @param enabled Whether or not to enable smooth scrollbar. 1015 * 1016 * @see #setSmoothScrollbarEnabled(boolean) 1017 */ 1018 public void setSmoothScrollbarEnabled(boolean enabled) { 1019 mSmoothScrollbarEnabled = enabled; 1020 } 1021 1022 /** 1023 * Returns the current state of the smooth scrollbar feature. It is enabled by default. 1024 * 1025 * @return True if smooth scrollbar is enabled, false otherwise. 1026 * 1027 * @see #setSmoothScrollbarEnabled(boolean) 1028 */ 1029 public boolean isSmoothScrollbarEnabled() { 1030 return mSmoothScrollbarEnabled; 1031 } 1032 1033 private void updateLayoutState(int layoutDirection, int requiredSpace, 1034 boolean canUseExistingSpace, RecyclerView.State state) { 1035 mLayoutState.mExtra = getExtraLayoutSpace(state); 1036 mLayoutState.mLayoutDirection = layoutDirection; 1037 int fastScrollSpace; 1038 if (layoutDirection == LayoutState.LAYOUT_END) { 1039 mLayoutState.mExtra += mOrientationHelper.getEndPadding(); 1040 // get the first child in the direction we are going 1041 final View child = getChildClosestToEnd(); 1042 // the direction in which we are traversing children 1043 mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD 1044 : LayoutState.ITEM_DIRECTION_TAIL; 1045 mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; 1046 mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); 1047 // calculate how much we can scroll without adding new children (independent of layout) 1048 fastScrollSpace = mOrientationHelper.getDecoratedEnd(child) 1049 - mOrientationHelper.getEndAfterPadding(); 1050 1051 } else { 1052 final View child = getChildClosestToStart(); 1053 mLayoutState.mExtra += mOrientationHelper.getStartAfterPadding(); 1054 mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL 1055 : LayoutState.ITEM_DIRECTION_HEAD; 1056 mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; 1057 mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child); 1058 fastScrollSpace = -mOrientationHelper.getDecoratedStart(child) 1059 + mOrientationHelper.getStartAfterPadding(); 1060 } 1061 mLayoutState.mAvailable = requiredSpace; 1062 if (canUseExistingSpace) { 1063 mLayoutState.mAvailable -= fastScrollSpace; 1064 } 1065 mLayoutState.mScrollingOffset = fastScrollSpace; 1066 } 1067 1068 private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { 1069 if (getChildCount() == 0 || dy == 0) { 1070 return 0; 1071 } 1072 ensureLayoutState(); 1073 final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; 1074 final int absDy = Math.abs(dy); 1075 updateLayoutState(layoutDirection, absDy, true, state); 1076 final int freeScroll = mLayoutState.mScrollingOffset; 1077 final int consumed = freeScroll + fill(recycler, mLayoutState, state, false); 1078 if (consumed < 0) { 1079 if (DEBUG) { 1080 Log.d(TAG, "Don't have any more elements to scroll"); 1081 } 1082 return 0; 1083 } 1084 final int scrolled = absDy > consumed ? layoutDirection * consumed : dy; 1085 mOrientationHelper.offsetChildren(-scrolled); 1086 if (DEBUG) { 1087 Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled); 1088 } 1089 return scrolled; 1090 } 1091 1092 @Override 1093 public void assertNotInLayoutOrScroll(String message) { 1094 if (mPendingSavedState == null) { 1095 super.assertNotInLayoutOrScroll(message); 1096 } 1097 } 1098 1099 /** 1100 * Recycles children between given indices. 1101 * 1102 * @param startIndex inclusive 1103 * @param endIndex exclusive 1104 */ 1105 private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { 1106 if (startIndex == endIndex) { 1107 return; 1108 } 1109 if (DEBUG) { 1110 Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items"); 1111 } 1112 if (endIndex > startIndex) { 1113 for (int i = endIndex - 1; i >= startIndex; i--) { 1114 removeAndRecycleViewAt(i, recycler); 1115 } 1116 } else { 1117 for (int i = startIndex; i > endIndex; i--) { 1118 removeAndRecycleViewAt(i, recycler); 1119 } 1120 } 1121 } 1122 1123 /** 1124 * Recycles views that went out of bounds after scrolling towards the end of the layout. 1125 * 1126 * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView} 1127 * @param dt This can be used to add additional padding to the visible area. This is used 1128 * to 1129 * detect children that will go out of bounds after scrolling, without actually 1130 * moving them. 1131 */ 1132 private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) { 1133 if (dt < 0) { 1134 if (DEBUG) { 1135 Log.d(TAG, "Called recycle from start with a negative value. This might happen" 1136 + " during layout changes but may be sign of a bug"); 1137 } 1138 return; 1139 } 1140 // ignore padding, ViewGroup may not clip children. 1141 final int limit = dt; 1142 final int childCount = getChildCount(); 1143 if (mShouldReverseLayout) { 1144 for (int i = childCount - 1; i >= 0; i--) { 1145 View child = getChildAt(i); 1146 if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here 1147 recycleChildren(recycler, childCount - 1, i); 1148 return; 1149 } 1150 } 1151 } else { 1152 for (int i = 0; i < childCount; i++) { 1153 View child = getChildAt(i); 1154 if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here 1155 recycleChildren(recycler, 0, i); 1156 return; 1157 } 1158 } 1159 } 1160 } 1161 1162 1163 /** 1164 * Recycles views that went out of bounds after scrolling towards the start of the layout. 1165 * 1166 * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView} 1167 * @param dt This can be used to add additional padding to the visible area. This is used 1168 * to detect children that will go out of bounds after scrolling, without 1169 * actually moving them. 1170 */ 1171 private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) { 1172 final int childCount = getChildCount(); 1173 if (dt < 0) { 1174 if (DEBUG) { 1175 Log.d(TAG, "Called recycle from end with a negative value. This might happen" 1176 + " during layout changes but may be sign of a bug"); 1177 } 1178 return; 1179 } 1180 final int limit = mOrientationHelper.getEnd() - dt; 1181 if (mShouldReverseLayout) { 1182 for (int i = 0; i < childCount; i++) { 1183 View child = getChildAt(i); 1184 if (mOrientationHelper.getDecoratedStart(child) < limit) {// stop here 1185 recycleChildren(recycler, 0, i); 1186 return; 1187 } 1188 } 1189 } else { 1190 for (int i = childCount - 1; i >= 0; i--) { 1191 View child = getChildAt(i); 1192 if (mOrientationHelper.getDecoratedStart(child) < limit) {// stop here 1193 recycleChildren(recycler, childCount - 1, i); 1194 return; 1195 } 1196 } 1197 } 1198 1199 } 1200 1201 /** 1202 * Helper method to call appropriate recycle method depending on current layout direction 1203 * 1204 * @param recycler Current recycler that is attached to RecyclerView 1205 * @param layoutState Current layout state. Right now, this object does not change but 1206 * we may consider moving it out of this view so passing around as a 1207 * parameter for now, rather than accessing {@link #mLayoutState} 1208 * @see #recycleViewsFromStart(android.support.v7.widget.RecyclerView.Recycler, int) 1209 * @see #recycleViewsFromEnd(android.support.v7.widget.RecyclerView.Recycler, int) 1210 * @see android.support.v7.widget.LinearLayoutManager.LayoutState#mLayoutDirection 1211 */ 1212 private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { 1213 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1214 recycleViewsFromEnd(recycler, layoutState.mScrollingOffset); 1215 } else { 1216 recycleViewsFromStart(recycler, layoutState.mScrollingOffset); 1217 } 1218 } 1219 1220 /** 1221 * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly 1222 * independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager} 1223 * and with little change, can be made publicly available as a helper class. 1224 * 1225 * @param recycler Current recycler that is attached to RecyclerView 1226 * @param layoutState Configuration on how we should fill out the available space. 1227 * @param state Context passed by the RecyclerView to control scroll steps. 1228 * @param stopOnFocusable If true, filling stops in the first focusable new child 1229 * @return Number of pixels that it added. Useful for scoll functions. 1230 */ 1231 private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, 1232 RecyclerView.State state, boolean stopOnFocusable) { 1233 // max offset we should set is mFastScroll + available 1234 final int start = layoutState.mAvailable; 1235 if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) { 1236 // TODO ugly bug fix. should not happen 1237 if (layoutState.mAvailable < 0) { 1238 layoutState.mScrollingOffset += layoutState.mAvailable; 1239 } 1240 recycleByLayoutState(recycler, layoutState); 1241 } 1242 int remainingSpace = layoutState.mAvailable + layoutState.mExtra; 1243 while (remainingSpace > 0 && layoutState.hasMore(state)) { 1244 View view = layoutState.next(recycler); 1245 if (view == null) { 1246 if (DEBUG && layoutState.mScrapList == null) { 1247 throw new RuntimeException("received null view when unexpected"); 1248 } 1249 // if we are laying out views in scrap, this may return null which means there is 1250 // no more items to layout. 1251 break; 1252 } 1253 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); 1254 if (mLayoutState.mScrapList == null) { 1255 if (mShouldReverseLayout == (layoutState.mLayoutDirection 1256 == LayoutState.LAYOUT_START)) { 1257 addView(view); 1258 } else { 1259 addView(view, 0); 1260 } 1261 } else { 1262 if (mShouldReverseLayout == (layoutState.mLayoutDirection 1263 == LayoutState.LAYOUT_START)) { 1264 addDisappearingView(view); 1265 } else { 1266 addDisappearingView(view, 0); 1267 } 1268 } 1269 measureChildWithMargins(view, 0, 0); 1270 int consumed = mOrientationHelper.getDecoratedMeasurement(view); 1271 int left, top, right, bottom; 1272 if (mOrientation == VERTICAL) { 1273 if (isLayoutRTL()) { 1274 right = getWidth() - getPaddingRight(); 1275 left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); 1276 } else { 1277 left = getPaddingLeft(); 1278 right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); 1279 } 1280 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1281 bottom = layoutState.mOffset; 1282 top = layoutState.mOffset - consumed; 1283 } else { 1284 top = layoutState.mOffset; 1285 bottom = layoutState.mOffset + consumed; 1286 } 1287 } else { 1288 top = getPaddingTop(); 1289 bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); 1290 1291 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1292 right = layoutState.mOffset; 1293 left = layoutState.mOffset - consumed; 1294 } else { 1295 left = layoutState.mOffset; 1296 right = layoutState.mOffset + consumed; 1297 } 1298 } 1299 // We calculate everything with View's bounding box (which includes decor and margins) 1300 // To calculate correct layout position, we subtract margins. 1301 layoutDecorated(view, left + params.leftMargin, top + params.topMargin 1302 , right - params.rightMargin, bottom - params.bottomMargin); 1303 if (DEBUG) { 1304 Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" 1305 + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" 1306 + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)); 1307 } 1308 layoutState.mOffset += consumed * layoutState.mLayoutDirection; 1309 1310 /** 1311 * Consume the available space if: 1312 * * view is not removed OR changed 1313 * * OR we are laying out scrap children 1314 * * OR we are not doing pre-layout 1315 */ 1316 if (!(params.isItemRemoved() || params.isItemChanged()) 1317 || mLayoutState.mScrapList != null || !state.isPreLayout()) { 1318 layoutState.mAvailable -= consumed; 1319 // we keep a separate remaining space because mAvailable is important for recycling 1320 remainingSpace -= consumed; 1321 } 1322 1323 if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) { 1324 layoutState.mScrollingOffset += consumed; 1325 if (layoutState.mAvailable < 0) { 1326 layoutState.mScrollingOffset += layoutState.mAvailable; 1327 } 1328 recycleByLayoutState(recycler, layoutState); 1329 } 1330 if (stopOnFocusable && view.isFocusable()) { 1331 break; 1332 } 1333 1334 // some deleted views may have position -1. Make sure state has a real target scroll 1335 // position 1336 if (state.getTargetScrollPosition() != RecyclerView.NO_POSITION && 1337 state.getTargetScrollPosition() == getPosition(view)) { 1338 break; 1339 } 1340 } 1341 if (DEBUG) { 1342 validateChildOrder(); 1343 } 1344 return start - layoutState.mAvailable; 1345 } 1346 1347 /** 1348 * Converts a focusDirection to orientation. 1349 * 1350 * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, 1351 * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, 1352 * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} 1353 * or 0 for not applicable 1354 * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction 1355 * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise. 1356 */ 1357 private int convertFocusDirectionToLayoutDirection(int focusDirection) { 1358 switch (focusDirection) { 1359 case View.FOCUS_BACKWARD: 1360 return LayoutState.LAYOUT_START; 1361 case View.FOCUS_FORWARD: 1362 return LayoutState.LAYOUT_END; 1363 case View.FOCUS_UP: 1364 return mOrientation == VERTICAL ? LayoutState.LAYOUT_START 1365 : LayoutState.INVALID_LAYOUT; 1366 case View.FOCUS_DOWN: 1367 return mOrientation == VERTICAL ? LayoutState.LAYOUT_END 1368 : LayoutState.INVALID_LAYOUT; 1369 case View.FOCUS_LEFT: 1370 return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START 1371 : LayoutState.INVALID_LAYOUT; 1372 case View.FOCUS_RIGHT: 1373 return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END 1374 : LayoutState.INVALID_LAYOUT; 1375 default: 1376 if (DEBUG) { 1377 Log.d(TAG, "Unknown focus request:" + focusDirection); 1378 } 1379 return LayoutState.INVALID_LAYOUT; 1380 } 1381 1382 } 1383 1384 /** 1385 * Convenience method to find the child closes to start. Caller should check it has enough 1386 * children. 1387 * 1388 * @return The child closes to start of the layout from user's perspective. 1389 */ 1390 private View getChildClosestToStart() { 1391 return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0); 1392 } 1393 1394 /** 1395 * Convenience method to find the child closes to end. Caller should check it has enough 1396 * children. 1397 * 1398 * @return The child closes to end of the layout from user's perspective. 1399 */ 1400 private View getChildClosestToEnd() { 1401 return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1); 1402 } 1403 1404 1405 /** 1406 * Among the children that are suitable to be considered as an anchor child, returns the one 1407 * closest to the end of the layout. 1408 * <p> 1409 * Due to ambiguous adapter updates or children being removed, some children's positions may be 1410 * invalid. This method is a best effort to find a position within adapter bounds if possible. 1411 * 1412 * @return A View that can be used an an anchor View. 1413 */ 1414 private View findReferenceChildClosestToEnd(RecyclerView.State state) { 1415 return mShouldReverseLayout ? findFirstReferenceChild(state.getItemCount()) : 1416 findLastReferenceChild(state.getItemCount()); 1417 } 1418 1419 /** 1420 * Among the children that are suitable to be considered as an anchor child, returns the one 1421 * closest to the start of the layout. 1422 * <p> 1423 * Due to ambiguous adapter updates or children being removed, some children's positions may be 1424 * invalid. This method is a best effort to find a position within adapter bounds if possible. 1425 * 1426 * @return A View that can be used an an anchor View. 1427 */ 1428 private View findReferenceChildClosestToStart(RecyclerView.State state) { 1429 return mShouldReverseLayout ? findLastReferenceChild(state.getItemCount()) : 1430 findFirstReferenceChild(state.getItemCount()); 1431 } 1432 1433 1434 private View findFirstReferenceChild(int itemCount) { 1435 final int limit = getChildCount(); 1436 for (int i = 0; i < limit; i++) { 1437 final View view = getChildAt(i); 1438 final int position = getPosition(view); 1439 if (position >= 0 && position < itemCount) { 1440 return view; 1441 } 1442 } 1443 return null; 1444 } 1445 1446 private View findLastReferenceChild(int itemCount) { 1447 for (int i = getChildCount() - 1; i >= 0; i--) { 1448 final View view = getChildAt(i); 1449 final int position = getPosition(view); 1450 if (position >= 0 && position < itemCount) { 1451 return view; 1452 } 1453 } 1454 return null; 1455 } 1456 1457 /** 1458 * Returns the adapter position of the first visible view. 1459 * <p> 1460 * Note that, this value is not affected by layout orientation or item order traversal. 1461 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 1462 * not in the layout. 1463 * <p> 1464 * If RecyclerView has item decorators, they will be considered in calculations as well. 1465 * <p> 1466 * LinearLayoutManager may pre-cache some views that are not necessarily visible. Those views 1467 * are ignored in this method. 1468 * 1469 * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if 1470 * there aren't any visible items. 1471 * @see #findFirstCompletelyVisibleItemPosition() 1472 * @see #findLastVisibleItemPosition() 1473 */ 1474 public int findFirstVisibleItemPosition() { 1475 final View child = findOneVisibleChild(0, getChildCount(), false); 1476 return child == null ? RecyclerView.NO_POSITION : getPosition(child); 1477 } 1478 1479 /** 1480 * Returns the adapter position of the first fully visible view. 1481 * <p> 1482 * Note that bounds check is only performed in the current orientation. That means, if 1483 * LinearLayoutManager is horizontal, it will only check the view's left and right edges. 1484 * 1485 * @return The adapter position of the first fully visible item or 1486 * {@link RecyclerView#NO_POSITION} if there aren't any visible items. 1487 * @see #findFirstVisibleItemPosition() 1488 * @see #findLastCompletelyVisibleItemPosition() 1489 */ 1490 public int findFirstCompletelyVisibleItemPosition() { 1491 final View child = findOneVisibleChild(0, getChildCount(), true); 1492 return child == null ? RecyclerView.NO_POSITION : getPosition(child); 1493 } 1494 1495 /** 1496 * Returns the adapter position of the last visible view. 1497 * <p> 1498 * Note that, this value is not affected by layout orientation or item order traversal. 1499 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 1500 * not in the layout. 1501 * <p> 1502 * If RecyclerView has item decorators, they will be considered in calculations as well. 1503 * <p> 1504 * LinearLayoutManager may pre-cache some views that are not necessarily visible. Those views 1505 * are ignored in this method. 1506 * 1507 * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if 1508 * there aren't any visible items. 1509 * @see #findLastCompletelyVisibleItemPosition() 1510 * @see #findFirstVisibleItemPosition() 1511 */ 1512 public int findLastVisibleItemPosition() { 1513 final View child = findOneVisibleChild(getChildCount() - 1, -1, false); 1514 return child == null ? RecyclerView.NO_POSITION : getPosition(child); 1515 } 1516 1517 /** 1518 * Returns the adapter position of the last fully visible view. 1519 * <p> 1520 * Note that bounds check is only performed in the current orientation. That means, if 1521 * LinearLayoutManager is horizontal, it will only check the view's left and right edges. 1522 * 1523 * @return The adapter position of the last fully visible view or 1524 * {@link RecyclerView#NO_POSITION} if there aren't any visible items. 1525 * @see #findLastVisibleItemPosition() 1526 * @see #findFirstCompletelyVisibleItemPosition() 1527 */ 1528 public int findLastCompletelyVisibleItemPosition() { 1529 final View child = findOneVisibleChild(getChildCount() - 1, -1, true); 1530 return child == null ? RecyclerView.NO_POSITION : getPosition(child); 1531 } 1532 1533 View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { 1534 final int start = mOrientationHelper.getStartAfterPadding(); 1535 final int end = mOrientationHelper.getEndAfterPadding(); 1536 final int next = toIndex > fromIndex ? 1 : -1; 1537 for (int i = fromIndex; i != toIndex; i+=next) { 1538 final View child = getChildAt(i); 1539 final int childStart = mOrientationHelper.getDecoratedStart(child); 1540 final int childEnd = mOrientationHelper.getDecoratedEnd(child); 1541 if (childStart < end && childEnd > start) { 1542 if (completelyVisible) { 1543 if (childStart >= start && childEnd <= end) { 1544 return child; 1545 } 1546 } else { 1547 return child; 1548 } 1549 } 1550 } 1551 return null; 1552 } 1553 1554 @Override 1555 public View onFocusSearchFailed(View focused, int focusDirection, 1556 RecyclerView.Recycler recycler, RecyclerView.State state) { 1557 resolveShouldLayoutReverse(); 1558 if (getChildCount() == 0) { 1559 return null; 1560 } 1561 1562 final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection); 1563 if (layoutDir == LayoutState.INVALID_LAYOUT) { 1564 return null; 1565 } 1566 final View referenceChild; 1567 if (layoutDir == LayoutState.LAYOUT_START) { 1568 referenceChild = findReferenceChildClosestToStart(state); 1569 } else { 1570 referenceChild = findReferenceChildClosestToEnd(state); 1571 } 1572 if (referenceChild == null) { 1573 if (DEBUG) { 1574 Log.d(TAG, 1575 "Cannot find a child with a valid position to be used for focus search."); 1576 } 1577 return null; 1578 } 1579 ensureLayoutState(); 1580 final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace()); 1581 updateLayoutState(layoutDir, maxScroll, false, state); 1582 mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN; 1583 fill(recycler, mLayoutState, state, true); 1584 final View nextFocus; 1585 if (layoutDir == LayoutState.LAYOUT_START) { 1586 nextFocus = getChildClosestToStart(); 1587 } else { 1588 nextFocus = getChildClosestToEnd(); 1589 } 1590 if (nextFocus == referenceChild || !nextFocus.isFocusable()) { 1591 return null; 1592 } 1593 return nextFocus; 1594 } 1595 1596 /** 1597 * Used for debugging. 1598 * Logs the internal representation of children to default logger. 1599 */ 1600 private void logChildren() { 1601 Log.d(TAG, "internal representation of views on the screen"); 1602 for (int i = 0; i < getChildCount(); i++) { 1603 View child = getChildAt(i); 1604 Log.d(TAG, "item " + getPosition(child) + ", coord:" 1605 + mOrientationHelper.getDecoratedStart(child)); 1606 } 1607 Log.d(TAG, "=============="); 1608 } 1609 1610 /** 1611 * Used for debugging. 1612 * Validates that child views are laid out in correct order. This is important because rest of 1613 * the algorithm relies on this constraint. 1614 * 1615 * In default layout, child 0 should be closest to screen position 0 and last child should be 1616 * closest to position WIDTH or HEIGHT. 1617 * In reverse layout, last child should be closes to screen position 0 and first child should 1618 * be closest to position WIDTH or HEIGHT 1619 */ 1620 private void validateChildOrder() { 1621 Log.d(TAG, "validating child count " + getChildCount()); 1622 if (getChildCount() < 1) { 1623 return; 1624 } 1625 int lastPos = getPosition(getChildAt(0)); 1626 int lastScreenLoc = mOrientationHelper.getDecoratedStart(getChildAt(0)); 1627 if (mShouldReverseLayout) { 1628 for (int i = 1; i < getChildCount(); i++) { 1629 View child = getChildAt(i); 1630 int pos = getPosition(child); 1631 int screenLoc = mOrientationHelper.getDecoratedStart(child); 1632 if (pos < lastPos) { 1633 logChildren(); 1634 throw new RuntimeException("detected invalid position. loc invalid? " + 1635 (screenLoc < lastScreenLoc)); 1636 } 1637 if (screenLoc > lastScreenLoc) { 1638 logChildren(); 1639 throw new RuntimeException("detected invalid location"); 1640 } 1641 } 1642 } else { 1643 for (int i = 1; i < getChildCount(); i++) { 1644 View child = getChildAt(i); 1645 int pos = getPosition(child); 1646 int screenLoc = mOrientationHelper.getDecoratedStart(child); 1647 if (pos < lastPos) { 1648 logChildren(); 1649 throw new RuntimeException("detected invalid position. loc invalid? " + 1650 (screenLoc < lastScreenLoc)); 1651 } 1652 if (screenLoc < lastScreenLoc) { 1653 logChildren(); 1654 throw new RuntimeException("detected invalid location"); 1655 } 1656 } 1657 } 1658 } 1659 1660 @Override 1661 public boolean supportsPredictiveItemAnimations() { 1662 return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd; 1663 } 1664 1665 /** 1666 * Helper class that keeps temporary state while {LayoutManager} is filling out the empty 1667 * space. 1668 */ 1669 private static class LayoutState { 1670 1671 final static String TAG = "LinearLayoutManager#LayoutState"; 1672 1673 final static int LAYOUT_START = -1; 1674 1675 final static int LAYOUT_END = 1; 1676 1677 final static int INVALID_LAYOUT = Integer.MIN_VALUE; 1678 1679 final static int ITEM_DIRECTION_HEAD = -1; 1680 1681 final static int ITEM_DIRECTION_TAIL = 1; 1682 1683 final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE; 1684 1685 /** 1686 * Pixel offset where layout should start 1687 */ 1688 int mOffset; 1689 1690 /** 1691 * Number of pixels that we should fill, in the layout direction. 1692 */ 1693 int mAvailable; 1694 1695 /** 1696 * Current position on the adapter to get the next item. 1697 */ 1698 int mCurrentPosition; 1699 1700 /** 1701 * Defines the direction in which the data adapter is traversed. 1702 * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL} 1703 */ 1704 int mItemDirection; 1705 1706 /** 1707 * Defines the direction in which the layout is filled. 1708 * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END} 1709 */ 1710 int mLayoutDirection; 1711 1712 /** 1713 * Used when LayoutState is constructed in a scrolling state. 1714 * It should be set the amount of scrolling we can make without creating a new view. 1715 * Settings this is required for efficient view recycling. 1716 */ 1717 int mScrollingOffset; 1718 1719 /** 1720 * Used if you want to pre-layout items that are not yet visible. 1721 * The difference with {@link #mAvailable} is that, when recycling, distance laid out for 1722 * {@link #mExtra} is not considered to avoid recycling visible children. 1723 */ 1724 int mExtra = 0; 1725 1726 /** 1727 * When LLM needs to layout particular views, it sets this list in which case, LayoutState 1728 * will only return views from this list and return null if it cannot find an item. 1729 */ 1730 List<RecyclerView.ViewHolder> mScrapList = null; 1731 1732 /** 1733 * @return true if there are more items in the data adapter 1734 */ 1735 boolean hasMore(RecyclerView.State state) { 1736 return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount(); 1737 } 1738 1739 /** 1740 * Gets the view for the next element that we should layout. 1741 * Also updates current item index to the next item, based on {@link #mItemDirection} 1742 * 1743 * @return The next element that we should layout. 1744 */ 1745 View next(RecyclerView.Recycler recycler) { 1746 if (mScrapList != null) { 1747 return nextFromLimitedList(); 1748 } 1749 final View view = recycler.getViewForPosition(mCurrentPosition); 1750 mCurrentPosition += mItemDirection; 1751 return view; 1752 } 1753 1754 /** 1755 * Returns next item from limited list. 1756 * <p> 1757 * Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection 1758 * 1759 * @return View if an item in the current position or direction exists if not null. 1760 */ 1761 private View nextFromLimitedList() { 1762 int size = mScrapList.size(); 1763 RecyclerView.ViewHolder closest = null; 1764 int closestDistance = Integer.MAX_VALUE; 1765 for (int i = 0; i < size; i++) { 1766 RecyclerView.ViewHolder viewHolder = mScrapList.get(i); 1767 final int distance = (viewHolder.getPosition() - mCurrentPosition) * mItemDirection; 1768 if (distance < 0) { 1769 continue; // item is not in current direction 1770 } 1771 if (distance < closestDistance) { 1772 closest = viewHolder; 1773 closestDistance = distance; 1774 if (distance == 0) { 1775 break; 1776 } 1777 } 1778 } 1779 if (DEBUG) { 1780 Log.d(TAG, "layout from scrap. found view:?" + (closest != null)); 1781 } 1782 if (closest != null) { 1783 mCurrentPosition = closest.getPosition() + mItemDirection; 1784 return closest.itemView; 1785 } 1786 return null; 1787 } 1788 1789 void log() { 1790 Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" + 1791 mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection); 1792 } 1793 } 1794 1795 static class SavedState implements Parcelable { 1796 1797 int mOrientation; 1798 1799 int mAnchorPosition; 1800 1801 int mAnchorOffset; 1802 1803 boolean mReverseLayout; 1804 1805 boolean mStackFromEnd; 1806 1807 boolean mAnchorLayoutFromEnd; 1808 1809 boolean mRecycleChildrenOnDetach; 1810 1811 boolean mSmoothScrollbarEnabled; 1812 1813 1814 public SavedState() { 1815 1816 } 1817 1818 SavedState(Parcel in) { 1819 mOrientation = in.readInt(); 1820 mAnchorPosition = in.readInt(); 1821 mAnchorOffset = in.readInt(); 1822 mReverseLayout = in.readInt() == 1; 1823 mStackFromEnd = in.readInt() == 1; 1824 mAnchorLayoutFromEnd = in.readInt() == 1; 1825 mRecycleChildrenOnDetach = in.readInt() == 1; 1826 mSmoothScrollbarEnabled = in.readInt() == 1; 1827 } 1828 1829 public SavedState(SavedState other) { 1830 mOrientation = other.mOrientation; 1831 mAnchorPosition = other.mAnchorPosition; 1832 mAnchorOffset = other.mAnchorOffset; 1833 mReverseLayout = other.mReverseLayout; 1834 mStackFromEnd = other.mStackFromEnd; 1835 mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; 1836 mRecycleChildrenOnDetach = other.mRecycleChildrenOnDetach; 1837 mSmoothScrollbarEnabled = other.mSmoothScrollbarEnabled; 1838 } 1839 1840 boolean hasValidAnchor() { 1841 return mAnchorPosition >= 0; 1842 } 1843 1844 void invalidateAnchor() { 1845 mAnchorPosition = RecyclerView.NO_POSITION; 1846 } 1847 1848 @Override 1849 public int describeContents() { 1850 return 0; 1851 } 1852 1853 @Override 1854 public void writeToParcel(Parcel dest, int flags) { 1855 dest.writeInt(mOrientation); 1856 dest.writeInt(mAnchorPosition); 1857 dest.writeInt(mAnchorOffset); 1858 dest.writeInt(mReverseLayout ? 1 : 0); 1859 dest.writeInt(mStackFromEnd ? 1 : 0); 1860 dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); 1861 dest.writeInt(mRecycleChildrenOnDetach ? 1 : 0); 1862 dest.writeInt(mSmoothScrollbarEnabled ? 1 : 0); 1863 } 1864 1865 public static final Parcelable.Creator<SavedState> CREATOR 1866 = new Parcelable.Creator<SavedState>() { 1867 @Override 1868 public SavedState createFromParcel(Parcel in) { 1869 return new SavedState(in); 1870 } 1871 1872 @Override 1873 public SavedState[] newArray(int size) { 1874 return new SavedState[size]; 1875 } 1876 }; 1877 } 1878 1879 /** 1880 * Simple data class to keep Anchor information 1881 */ 1882 private final class AnchorInfo { 1883 int mPosition; 1884 int mCoordinate; 1885 boolean mLayoutFromEnd; 1886 void reset() { 1887 mPosition = RecyclerView.NO_POSITION; 1888 mCoordinate = INVALID_OFFSET; 1889 mLayoutFromEnd = false; 1890 } 1891 1892 /** 1893 * assigns anchor coordinate from the RecyclerView's padding depending on current 1894 * layoutFromEnd value 1895 */ 1896 void assignCoordinateFromPadding() { 1897 mCoordinate = mLayoutFromEnd 1898 ? mOrientationHelper.getEndAfterPadding() 1899 : mOrientationHelper.getStartAfterPadding(); 1900 } 1901 1902 @Override 1903 public String toString() { 1904 return "AnchorInfo{" + 1905 "mPosition=" + mPosition + 1906 ", mCoordinate=" + mCoordinate + 1907 ", mLayoutFromEnd=" + mLayoutFromEnd + 1908 '}'; 1909 } 1910 1911 /** 1912 * Assign anchor position information from the provided view if it is valid as a reference 1913 * child. 1914 */ 1915 public boolean assignFromViewIfValid(View child, RecyclerView.State state) { 1916 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); 1917 if (!lp.isItemRemoved() && lp.getViewPosition() >= 0 1918 && lp.getViewPosition() < state.getItemCount()) { 1919 assignFromView(child); 1920 return true; 1921 } 1922 return false; 1923 } 1924 1925 public void assignFromView(View child) { 1926 if (mLayoutFromEnd) { 1927 mCoordinate = mOrientationHelper.getDecoratedEnd(child) + 1928 mOrientationHelper.getTotalSpaceChange(); 1929 } else { 1930 mCoordinate = mOrientationHelper.getDecoratedStart(child); 1931 } 1932 1933 mPosition = getPosition(child); 1934 } 1935 } 1936} 1937