LinearLayoutManager.java revision 49c83b12201dde5b93d4eca3d44478e0c967a2e6
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 shouls 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 * When LayoutManager needs to scroll to a position, it sets this variable and requests a 105 * layout which will check this variable and re-layout accordingly. 106 */ 107 private int mPendingScrollPosition = RecyclerView.NO_POSITION; 108 109 /** 110 * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is 111 * called. 112 */ 113 private int mPendingScrollPositionOffset = INVALID_OFFSET; 114 115 private boolean mRecycleChildrenOnDetach; 116 117 private SavedState mPendingSavedState = null; 118 119 /** 120 * Creates a vertical LinearLayoutManager 121 * 122 * @param context Current context, will be used to access resources. 123 */ 124 public LinearLayoutManager(Context context) { 125 this(context, VERTICAL, false); 126 } 127 128 /** 129 * @param context Current context, will be used to access resources. 130 * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link 131 * #VERTICAL}. 132 * @param reverseLayout When set to true, layouts from end to start. 133 */ 134 public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) { 135 setOrientation(orientation); 136 setReverseLayout(reverseLayout); 137 } 138 139 /** 140 * {@inheritDoc} 141 */ 142 @Override 143 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 144 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 145 ViewGroup.LayoutParams.WRAP_CONTENT); 146 } 147 148 /** 149 * Returns whether LinearLayoutManager will recycle its children when it is detached from 150 * RecyclerView. 151 * 152 * @return true if LinearLayoutManager will recycle its children when it is detached from 153 * RecyclerView. 154 */ 155 public boolean getRecycleChildrenOnDetach() { 156 return mRecycleChildrenOnDetach; 157 } 158 159 /** 160 * Set whether LinearLayoutManager will recycle its children when it is detached from 161 * RecyclerView. 162 * <p> 163 * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set 164 * this flag to <code>true</code> so that views will be avilable to other RecyclerViews 165 * immediately. 166 * <p> 167 * Note that, setting this flag will result in a performance drop if RecyclerView 168 * is restored. 169 * 170 * @param recycleChildrenOnDetach Whether children should be recycled in detach or not. 171 */ 172 public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) { 173 if (mPendingSavedState != null && 174 mPendingSavedState.mRecycleChildrenOnDetach != recycleChildrenOnDetach) { 175 // override pending state 176 mPendingSavedState.mRecycleChildrenOnDetach = recycleChildrenOnDetach; 177 } 178 mRecycleChildrenOnDetach = recycleChildrenOnDetach; 179 } 180 181 @Override 182 public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 183 super.onDetachedFromWindow(view, recycler); 184 if (mRecycleChildrenOnDetach) { 185 removeAndRecycleAllViews(recycler); 186 recycler.clear(); 187 } 188 } 189 190 @Override 191 public Parcelable onSaveInstanceState() { 192 if (mPendingSavedState != null) { 193 return new SavedState(mPendingSavedState); 194 } 195 SavedState state = new SavedState(); 196 state.mRecycleChildrenOnDetach = mRecycleChildrenOnDetach; 197 if (getChildCount() > 0) { 198 boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout; 199 state.mAnchorLayoutFromEnd = didLayoutFromEnd; 200 if (didLayoutFromEnd) { 201 final View refChild = getChildClosestToEnd(); 202 state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() - 203 mOrientationHelper.getDecoratedEnd(refChild); 204 state.mAnchorPosition = getPosition(refChild); 205 } else { 206 final View refChild = getChildClosestToStart(); 207 state.mAnchorPosition = getPosition(refChild); 208 state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild) - 209 mOrientationHelper.getStartAfterPadding(); 210 } 211 } else { 212 state.invalidateAnchor(); 213 } 214 state.mStackFromEnd = mStackFromEnd; 215 state.mReverseLayout = mReverseLayout; 216 state.mOrientation = mOrientation; 217 return state; 218 } 219 220 @Override 221 public void onRestoreInstanceState(Parcelable state) { 222 if (state instanceof SavedState) { 223 mPendingSavedState = (SavedState) state; 224 requestLayout(); 225 if (DEBUG) { 226 Log.d(TAG, "loaded saved state"); 227 } 228 } else if (DEBUG) { 229 Log.d(TAG, "invalid saved state class"); 230 } 231 } 232 233 /** 234 * @return true if {@link #getOrientation()} is {@link #HORIZONTAL} 235 */ 236 @Override 237 public boolean canScrollHorizontally() { 238 return mOrientation == HORIZONTAL; 239 } 240 241 /** 242 * @return true if {@link #getOrientation()} is {@link #VERTICAL} 243 */ 244 @Override 245 public boolean canScrollVertically() { 246 return mOrientation == VERTICAL; 247 } 248 249 /** 250 * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)} 251 */ 252 public void setStackFromEnd(boolean stackFromEnd) { 253 if (mPendingSavedState != null && mPendingSavedState.mStackFromEnd != stackFromEnd) { 254 // override pending state 255 mPendingSavedState.mStackFromEnd = stackFromEnd; 256 } 257 if (mStackFromEnd == stackFromEnd) { 258 return; 259 } 260 mStackFromEnd = stackFromEnd; 261 requestLayout(); 262 } 263 264 public boolean getStackFromEnd() { 265 return mStackFromEnd; 266 } 267 268 /** 269 * Returns the current orientaion of the layout. 270 * 271 * @return Current orientation. 272 * @see #mOrientation 273 * @see #setOrientation(int) 274 */ 275 public int getOrientation() { 276 return mOrientation; 277 } 278 279 /** 280 * Sets the orientation of the layout. {@link android.support.v7.widget.LinearLayoutManager} 281 * will do its best to keep scroll position. 282 * 283 * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} 284 */ 285 public void setOrientation(int orientation) { 286 if (orientation != HORIZONTAL && orientation != VERTICAL) { 287 throw new IllegalArgumentException("invalid orientation:" + orientation); 288 } 289 if (mPendingSavedState != null && mPendingSavedState.mOrientation != orientation) { 290 // override pending state 291 mPendingSavedState.mOrientation = orientation; 292 } 293 if (orientation == mOrientation) { 294 return; 295 } 296 mOrientation = orientation; 297 mOrientationHelper = null; 298 requestLayout(); 299 } 300 301 /** 302 * Calculates the view layout order. (e.g. from end to start or start to end) 303 * RTL layout support is applied automatically. So if layout is RTL and 304 * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. 305 */ 306 private void resolveShouldLayoutReverse() { 307 // A == B is the same result, but we rather keep it readable 308 if (mOrientation == VERTICAL || !isLayoutRTL()) { 309 mShouldReverseLayout = mReverseLayout; 310 } else { 311 mShouldReverseLayout = !mReverseLayout; 312 } 313 } 314 315 /** 316 * Returns if views are laid out from the opposite direction of the layout. 317 * 318 * @return If layout is reversed or not. 319 * @see {@link #setReverseLayout(boolean)} 320 */ 321 public boolean getReverseLayout() { 322 return mReverseLayout; 323 } 324 325 /** 326 * Used to reverse item traversal and layout order. 327 * This behaves similar to the layout change for RTL views. When set to true, first item is 328 * laid out at the end of the UI, second item is laid out before it etc. 329 * 330 * For horizontal layouts, it depends on the layout direction. 331 * When set to true, If {@link android.support.v7.widget.RecyclerView} is LTR, than it will 332 * layout from RTL, if {@link android.support.v7.widget.RecyclerView}} is RTL, it will layout 333 * from LTR. 334 * 335 * If you are looking for the exact same behavior of 336 * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use 337 * {@link #setStackFromEnd(boolean)} 338 */ 339 public void setReverseLayout(boolean reverseLayout) { 340 if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) { 341 // override pending state 342 mPendingSavedState.mReverseLayout = reverseLayout; 343 } 344 if (reverseLayout == mReverseLayout) { 345 return; 346 } 347 mReverseLayout = reverseLayout; 348 requestLayout(); 349 } 350 351 /** 352 * {@inheritDoc} 353 */ 354 @Override 355 public View findViewByPosition(int position) { 356 final int childCount = getChildCount(); 357 if (childCount == 0) { 358 return null; 359 } 360 final int firstChild = getPosition(getChildAt(0)); 361 final int viewPosition = position - firstChild; 362 if (viewPosition >= 0 && viewPosition < childCount) { 363 return getChildAt(viewPosition); 364 } 365 return null; 366 } 367 368 /** 369 * <p>Returns the amount of extra space that should be laid out by LinearLayoutManager. 370 * By default, {@link android.support.v7.widget.LinearLayoutManager} lays out 1 extra page of 371 * items while smooth scrolling and 0 otherwise. You can override this method to implement your 372 * custom layout pre-cache logic.</p> 373 * <p>Laying out invisible elements will eventually come with performance cost. On the other 374 * hand, in places like smooth scrolling to an unknown location, this extra content helps 375 * LayoutManager to calculate a much smoother scrolling; which improves user experience.</p> 376 * <p>You can also use this if you are trying to pre-layout your upcoming views.</p> 377 * 378 * @return The extra space that should be laid out (in pixels). 379 */ 380 protected int getExtraLayoutSpace(RecyclerView.State state) { 381 if (state.hasTargetScrollPosition()) { 382 return mOrientationHelper.getTotalSpace(); 383 } else { 384 return 0; 385 } 386 } 387 388 @Override 389 public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, 390 int position) { 391 LinearSmoothScroller linearSmoothScroller = 392 new LinearSmoothScroller(recyclerView.getContext()) { 393 @Override 394 public PointF computeScrollVectorForPosition(int targetPosition) { 395 return LinearLayoutManager.this 396 .computeScrollVectorForPosition(targetPosition); 397 } 398 }; 399 linearSmoothScroller.setTargetPosition(position); 400 startSmoothScroll(linearSmoothScroller); 401 } 402 403 public PointF computeScrollVectorForPosition(int targetPosition) { 404 if (getChildCount() == 0) { 405 return null; 406 } 407 final int firstChildPos = getPosition(getChildAt(0)); 408 final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1; 409 if (mOrientation == HORIZONTAL) { 410 return new PointF(direction, 0); 411 } else { 412 return new PointF(0, direction); 413 } 414 } 415 416 /** 417 * {@inheritDoc} 418 */ 419 @Override 420 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 421 // layout algorithm: 422 // 1) by checking children and other variables, find an anchor coordinate and an anchor 423 // item position. 424 // 2) fill towards start, stacking from bottom 425 // 3) fill towards end, stacking from top 426 // 4) scroll to fulfill requirements like stack from bottom. 427 // create layout state 428 if (DEBUG) { 429 Log.d(TAG, "is pre layout:" + state.isPreLayout()); 430 } 431 if (mPendingSavedState != null) { 432 setOrientation(mPendingSavedState.mOrientation); 433 setReverseLayout(mPendingSavedState.mReverseLayout); 434 setStackFromEnd(mPendingSavedState.mStackFromEnd); 435 setRecycleChildrenOnDetach(mPendingSavedState.mRecycleChildrenOnDetach); 436 if (mPendingSavedState.hasValidAnchor()) { 437 mPendingScrollPosition = mPendingSavedState.mAnchorPosition; 438 } 439 } 440 441 ensureLayoutState(); 442 // resolve layout direction 443 resolveShouldLayoutReverse(); 444 445 // validate scroll position if exists 446 if (mPendingScrollPosition != RecyclerView.NO_POSITION) { 447 // validate it 448 if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { 449 mPendingScrollPosition = RecyclerView.NO_POSITION; 450 mPendingScrollPositionOffset = INVALID_OFFSET; 451 if (DEBUG) { 452 Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition); 453 } 454 } 455 } 456 // this value might be updated if there is a target scroll position without an offset 457 boolean layoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; 458 459 final boolean stackFromEndChanged = mLastStackFromEnd != mStackFromEnd; 460 461 int anchorCoordinate, anchorItemPosition; 462 if (mPendingScrollPosition != RecyclerView.NO_POSITION) { 463 // if child is visible, try to make it a reference child and ensure it is fully visible. 464 // if child is not visible, align it depending on its virtual position. 465 anchorItemPosition = mPendingScrollPosition; 466 if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { 467 // Anchor offset depends on how that child was laid out. Here, we update it 468 // according to our current view bounds 469 layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; 470 if (layoutFromEnd) { 471 anchorCoordinate = mOrientationHelper.getEndAfterPadding() - 472 mPendingSavedState.mAnchorOffset; 473 } else { 474 anchorCoordinate = mOrientationHelper.getStartAfterPadding() + 475 mPendingSavedState.mAnchorOffset; 476 } 477 } else if (mPendingScrollPositionOffset == INVALID_OFFSET) { 478 View child = findViewByPosition(mPendingScrollPosition); 479 if (child != null) { 480 final int startGap = mOrientationHelper.getDecoratedStart(child) 481 - mOrientationHelper.getStartAfterPadding(); 482 final int endGap = mOrientationHelper.getEndAfterPadding() - 483 mOrientationHelper.getDecoratedEnd(child); 484 final int childSize = mOrientationHelper.getDecoratedMeasurement(child); 485 if (childSize > mOrientationHelper.getTotalSpace()) { 486 // item does not fit. fix depending on layout direction 487 anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() 488 : mOrientationHelper.getStartAfterPadding(); 489 } else if (startGap < 0) { 490 anchorCoordinate = mOrientationHelper.getStartAfterPadding(); 491 layoutFromEnd = false; 492 } else if (endGap < 0) { 493 anchorCoordinate = mOrientationHelper.getEndAfterPadding(); 494 layoutFromEnd = true; 495 } else { 496 anchorCoordinate = layoutFromEnd 497 ? mOrientationHelper.getDecoratedEnd(child) 498 : mOrientationHelper.getDecoratedStart(child); 499 } 500 } else { // item is not visible. 501 if (getChildCount() > 0) { 502 // get position of any child, does not matter 503 int pos = getPosition(getChildAt(0)); 504 if (mPendingScrollPosition < pos == mShouldReverseLayout) { 505 anchorCoordinate = mOrientationHelper.getEndAfterPadding(); 506 layoutFromEnd = true; 507 } else { 508 anchorCoordinate = mOrientationHelper.getStartAfterPadding(); 509 layoutFromEnd = false; 510 } 511 } else { 512 anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() 513 : mOrientationHelper.getStartAfterPadding(); 514 } 515 } 516 } else { 517 // override layout from end values for consistency 518 if (mShouldReverseLayout) { 519 anchorCoordinate = mOrientationHelper.getEndAfterPadding() 520 - mPendingScrollPositionOffset; 521 layoutFromEnd = true; 522 } else { 523 anchorCoordinate = mOrientationHelper.getStartAfterPadding() 524 + mPendingScrollPositionOffset; 525 layoutFromEnd = false; 526 } 527 } 528 } else if (getChildCount() > 0 && !stackFromEndChanged) { 529 if (layoutFromEnd) { 530 View referenceChild = getChildClosestToEnd(); 531 anchorCoordinate = mOrientationHelper.getDecoratedEnd(referenceChild); 532 anchorItemPosition = getPosition(referenceChild); 533 } else { 534 View referenceChild = getChildClosestToStart(); 535 anchorCoordinate = mOrientationHelper.getDecoratedStart(referenceChild); 536 anchorItemPosition = getPosition(referenceChild); 537 } 538 } else { 539 anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() : 540 mOrientationHelper.getStartAfterPadding(); 541 anchorItemPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; 542 } 543 544 detachAndScrapAttachedViews(recycler); 545 int extraForStart; 546 int extraForEnd; 547 final int extra = getExtraLayoutSpace(state); 548 boolean before = state.getTargetScrollPosition() < anchorItemPosition; 549 if (before == mShouldReverseLayout) { 550 extraForEnd = extra; 551 extraForStart = 0; 552 } else { 553 extraForStart = extra; 554 extraForEnd = 0; 555 } 556 int startOffset; 557 int endOffset; 558 if (layoutFromEnd) { 559 // fill towards start 560 updateLayoutStateToFillStart(anchorItemPosition, anchorCoordinate); 561 mLayoutState.mExtra = extraForStart; 562 fill(recycler, mLayoutState, state, false); 563 startOffset = mLayoutState.mOffset; 564 extraForEnd += mLayoutState.mAvailable; 565 // fill towards end 566 updateLayoutStateToFillEnd(anchorItemPosition, anchorCoordinate); 567 mLayoutState.mExtra = extraForEnd; 568 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 569 fill(recycler, mLayoutState, state, false); 570 endOffset = mLayoutState.mOffset; 571 } else { 572 // fill towards end 573 updateLayoutStateToFillEnd(anchorItemPosition, anchorCoordinate); 574 mLayoutState.mExtra = extraForEnd; 575 fill(recycler, mLayoutState, state, false); 576 endOffset = mLayoutState.mOffset; 577 extraForStart += mLayoutState.mAvailable; 578 // fill towards start 579 updateLayoutStateToFillStart(anchorItemPosition, anchorCoordinate); 580 mLayoutState.mExtra = extraForStart; 581 mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; 582 fill(recycler, mLayoutState, state, false); 583 startOffset = mLayoutState.mOffset; 584 } 585 586 // changes may cause gaps on the UI, try to fix them. 587 if (getChildCount() > 0) { 588 // because layout from end may be changed by scroll to position 589 // we re-calculate it. 590 // find which side we should check for gaps. 591 if (mShouldReverseLayout ^ mStackFromEnd) { 592 int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true); 593 startOffset += fixOffset; 594 endOffset += fixOffset; 595 fixOffset = fixLayoutStartGap(startOffset, recycler, state, false); 596 startOffset += fixOffset; 597 endOffset += fixOffset; 598 } else { 599 int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true); 600 startOffset += fixOffset; 601 endOffset += fixOffset; 602 fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); 603 startOffset += fixOffset; 604 endOffset += fixOffset; 605 } 606 } 607 608 // If there are scrap children that we did not layout, we need to find where they did go 609 // and layout them accordingly so that animations can work as expected. 610 // This case may happen if new views are added or an existing view expands and pushes 611 // another view out of bounds. 612 if (state.willRunPredictiveAnimations() && getChildCount() > 0 && !state.isPreLayout()) { 613 // to make the logic simpler, we calculate the size of children and call fill. 614 int scrapExtraStart = 0, scrapExtraEnd = 0; 615 final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList(); 616 final int scrapSize = scrapList.size(); 617 final int firstChildPos = getPosition(getChildAt(0)); 618 for (int i = 0; i < scrapSize; i++) { 619 RecyclerView.ViewHolder scrap = scrapList.get(i); 620 final int position = scrap.getPosition(); 621 final int direction = position < firstChildPos != mShouldReverseLayout 622 ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; 623 if (direction == LayoutState.LAYOUT_START) { 624 scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); 625 } else { 626 scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); 627 } 628 } 629 630 if (DEBUG) { 631 Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart 632 + " towards start and " + scrapExtraEnd + " towards end"); 633 } 634 mLayoutState.mScrapList = scrapList; 635 if (scrapExtraStart > 0) { 636 View anchor = getChildClosestToStart(); 637 updateLayoutStateToFillStart(getPosition(anchor), startOffset); 638 mLayoutState.mExtra = scrapExtraStart; 639 mLayoutState.mAvailable = 0; 640 mLayoutState.mCurrentPosition += mShouldReverseLayout ? 1 : -1; 641 fill(recycler, mLayoutState, state, false); 642 } 643 644 if (scrapExtraEnd > 0) { 645 View anchor = getChildClosestToEnd(); 646 updateLayoutStateToFillEnd(getPosition(anchor), endOffset); 647 mLayoutState.mExtra = scrapExtraEnd; 648 mLayoutState.mAvailable = 0; 649 mLayoutState.mCurrentPosition += mShouldReverseLayout ? -1 : 1; 650 fill(recycler, mLayoutState, state, false); 651 } 652 mLayoutState.mScrapList = null; 653 } 654 655 mPendingScrollPosition = RecyclerView.NO_POSITION; 656 mPendingScrollPositionOffset = INVALID_OFFSET; 657 mLastStackFromEnd = mStackFromEnd; 658 mPendingSavedState = null; // we don't need this anymore 659 if (DEBUG) { 660 validateChildOrder(); 661 } 662 } 663 664 /** 665 * @return The final offset amount for children 666 */ 667 private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler, 668 RecyclerView.State state, boolean canOffsetChildren) { 669 int gap = mOrientationHelper.getEndAfterPadding() - endOffset; 670 int fixOffset = 0; 671 if (gap > 0) { 672 fixOffset = -scrollBy(-gap, recycler, state); 673 } else { 674 return 0; // nothing to fix 675 } 676 // move offset according to scroll amount 677 endOffset += fixOffset; 678 if (canOffsetChildren) { 679 // re-calculate gap, see if we could fix it 680 gap = mOrientationHelper.getEndAfterPadding() - endOffset; 681 if (gap > 0) { 682 mOrientationHelper.offsetChildren(gap); 683 return gap + fixOffset; 684 } 685 } 686 return fixOffset; 687 } 688 689 /** 690 * @return The final offset amount for children 691 */ 692 private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler, 693 RecyclerView.State state, boolean canOffsetChildren) { 694 int gap = startOffset - mOrientationHelper.getStartAfterPadding(); 695 int fixOffset = 0; 696 if (gap > 0) { 697 // check if we should fix this gap. 698 fixOffset = -scrollBy(gap, recycler, state); 699 } else { 700 return 0; // nothing to fix 701 } 702 startOffset += fixOffset; 703 if (canOffsetChildren) { 704 // re-calculate gap, see if we could fix it 705 gap = startOffset - mOrientationHelper.getStartAfterPadding(); 706 if (gap > 0) { 707 mOrientationHelper.offsetChildren(-gap); 708 return fixOffset - gap; 709 } 710 } 711 return fixOffset; 712 } 713 714 private void updateLayoutStateToFillEnd(int itemPosition, int offset) { 715 mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset; 716 mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : 717 LayoutState.ITEM_DIRECTION_TAIL; 718 mLayoutState.mCurrentPosition = itemPosition; 719 mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END; 720 mLayoutState.mOffset = offset; 721 mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN; 722 } 723 724 private void updateLayoutStateToFillStart(int itemPosition, int offset) { 725 mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding(); 726 mLayoutState.mCurrentPosition = itemPosition; 727 mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : 728 LayoutState.ITEM_DIRECTION_HEAD; 729 mLayoutState.mLayoutDirection = LayoutState.LAYOUT_START; 730 mLayoutState.mOffset = offset; 731 mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN; 732 733 } 734 735 736 private boolean isLayoutRTL() { 737 return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; 738 } 739 740 void ensureLayoutState() { 741 if (mLayoutState == null) { 742 mLayoutState = new LayoutState(); 743 } 744 if (mOrientationHelper == null) { 745 mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); 746 } 747 } 748 749 /** 750 * <p>Scroll the RecyclerView to make the position visible.</p> 751 * 752 * <p>RecyclerView will scroll the minimum amount that is necessary to make the 753 * target position visible. If you are looking for a similar behavior to 754 * {@link android.widget.ListView#setSelection(int)} or 755 * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use 756 * {@link #scrollToPositionWithOffset(int, int)}.</p> 757 * 758 * <p>Note that scroll position change will not be reflected until the next layout call.</p> 759 * 760 * @param position Scroll to this adapter position 761 * @see #scrollToPositionWithOffset(int, int) 762 */ 763 @Override 764 public void scrollToPosition(int position) { 765 mPendingScrollPosition = position; 766 mPendingScrollPositionOffset = INVALID_OFFSET; 767 if (mPendingSavedState != null) { 768 mPendingSavedState.invalidateAnchor(); 769 } 770 requestLayout(); 771 } 772 773 /** 774 * <p>Scroll to the specified adapter position with the given offset from layout start.</p> 775 * 776 * <p>Note that scroll position change will not be reflected until the next layout call.</p> 777 * 778 * <p>If you are just trying to make a position visible, use {@link 779 * #scrollToPosition(int)}.</p> 780 * 781 * @param position Index (starting at 0) of the reference item. 782 * @param offset The distance (in pixels) between the start edge of the item view and 783 * start edge of the RecyclerView. 784 * @see #setReverseLayout(boolean) 785 * @see #scrollToPosition(int) 786 */ 787 public void scrollToPositionWithOffset(int position, int offset) { 788 mPendingScrollPosition = position; 789 mPendingScrollPositionOffset = offset; 790 if (mPendingSavedState != null) { 791 mPendingSavedState.invalidateAnchor(); 792 } 793 requestLayout(); 794 } 795 796 797 /** 798 * {@inheritDoc} 799 */ 800 @Override 801 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 802 RecyclerView.State state) { 803 if (mOrientation == VERTICAL) { 804 return 0; 805 } 806 return scrollBy(dx, recycler, state); 807 } 808 809 /** 810 * {@inheritDoc} 811 */ 812 @Override 813 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 814 RecyclerView.State state) { 815 if (mOrientation == HORIZONTAL) { 816 return 0; 817 } 818 return scrollBy(dy, recycler, state); 819 } 820 821 @Override 822 public int computeHorizontalScrollOffset(RecyclerView.State state) { 823 if (getChildCount() == 0) { 824 return 0; 825 } 826 final int topPosition = getPosition(getChildClosestToStart()); 827 return mShouldReverseLayout ? state.getItemCount() - 1 - topPosition : topPosition; 828 } 829 830 @Override 831 public int computeVerticalScrollOffset(RecyclerView.State state) { 832 if (getChildCount() == 0) { 833 return 0; 834 } 835 final int topPosition = getPosition(getChildClosestToStart()); 836 return mShouldReverseLayout ? state.getItemCount() - 1 - topPosition : topPosition; 837 } 838 839 @Override 840 public int computeHorizontalScrollExtent(RecyclerView.State state) { 841 return getChildCount(); 842 } 843 844 @Override 845 public int computeVerticalScrollExtent(RecyclerView.State state) { 846 return getChildCount(); 847 } 848 849 @Override 850 public int computeHorizontalScrollRange(RecyclerView.State state) { 851 return state.getItemCount(); 852 } 853 854 @Override 855 public int computeVerticalScrollRange(RecyclerView.State state) { 856 return state.getItemCount(); 857 } 858 859 private void updateLayoutState(int layoutDirection, int requiredSpace, 860 boolean canUseExistingSpace, RecyclerView.State state) { 861 mLayoutState.mExtra = getExtraLayoutSpace(state); 862 mLayoutState.mLayoutDirection = layoutDirection; 863 int fastScrollSpace; 864 if (layoutDirection == LayoutState.LAYOUT_END) { 865 // get the first child in the direction we are going 866 final View child = getChildClosestToEnd(); 867 // the direction in which we are traversing children 868 mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD 869 : LayoutState.ITEM_DIRECTION_TAIL; 870 mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; 871 mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); 872 // calculate how much we can scroll without adding new children (independent of layout) 873 fastScrollSpace = mOrientationHelper.getDecoratedEnd(child) 874 - mOrientationHelper.getEndAfterPadding(); 875 876 } else { 877 final View child = getChildClosestToStart(); 878 mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL 879 : LayoutState.ITEM_DIRECTION_HEAD; 880 mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; 881 mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child); 882 fastScrollSpace = -mOrientationHelper.getDecoratedStart(child) 883 + mOrientationHelper.getStartAfterPadding(); 884 } 885 mLayoutState.mAvailable = requiredSpace; 886 if (canUseExistingSpace) { 887 mLayoutState.mAvailable -= fastScrollSpace; 888 } 889 mLayoutState.mScrollingOffset = fastScrollSpace; 890 } 891 892 private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { 893 if (getChildCount() == 0 || dy == 0) { 894 return 0; 895 } 896 ensureLayoutState(); 897 final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; 898 final int absDy = Math.abs(dy); 899 updateLayoutState(layoutDirection, absDy, true, state); 900 final int freeScroll = mLayoutState.mScrollingOffset; 901 final int consumed = freeScroll + fill(recycler, mLayoutState, state, false); 902 if (consumed < 0) { 903 if (DEBUG) { 904 Log.d(TAG, "Don't have any more elements to scroll"); 905 } 906 return 0; 907 } 908 final int scrolled = absDy > consumed ? layoutDirection * consumed : dy; 909 mOrientationHelper.offsetChildren(-scrolled); 910 if (DEBUG) { 911 Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled); 912 } 913 return scrolled; 914 } 915 916 /** 917 * Recycles children between given indices. 918 * 919 * @param startIndex inclusive 920 * @param endIndex exclusive 921 */ 922 private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { 923 if (startIndex == endIndex) { 924 return; 925 } 926 if (DEBUG) { 927 Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items"); 928 } 929 if (endIndex > startIndex) { 930 for (int i = endIndex - 1; i >= startIndex; i--) { 931 removeAndRecycleViewAt(i, recycler); 932 } 933 } else { 934 for (int i = startIndex; i > endIndex; i--) { 935 removeAndRecycleViewAt(i, recycler); 936 } 937 } 938 } 939 940 /** 941 * Recycles views that went out of bounds after scrolling towards the end of the layout. 942 * 943 * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView} 944 * @param dt This can be used to add additional padding to the visible area. This is used 945 * to 946 * detect children that will go out of bounds after scrolling, without actually 947 * moving them. 948 */ 949 private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) { 950 if (dt < 0) { 951 if (DEBUG) { 952 Log.d(TAG, "Called recycle from start with a negative value. This might happen" 953 + " during layout changes but may be sign of a bug"); 954 } 955 return; 956 } 957 // ignore padding, ViewGroup may not clip children. 958 final int limit = dt; 959 final int childCount = getChildCount(); 960 if (mShouldReverseLayout) { 961 for (int i = childCount - 1; i >= 0; i--) { 962 View child = getChildAt(i); 963 if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here 964 recycleChildren(recycler, childCount - 1, i); 965 return; 966 } 967 } 968 } else { 969 for (int i = 0; i < childCount; i++) { 970 View child = getChildAt(i); 971 if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here 972 recycleChildren(recycler, 0, i); 973 return; 974 } 975 } 976 } 977 } 978 979 980 /** 981 * Recycles views that went out of bounds after scrolling towards the start of the layout. 982 * 983 * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView} 984 * @param dt This can be used to add additional padding to the visible area. This is used 985 * to detect children that will go out of bounds after scrolling, without 986 * actually moving them. 987 */ 988 private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) { 989 final int childCount = getChildCount(); 990 if (dt < 0) { 991 if (DEBUG) { 992 Log.d(TAG, "Called recycle from end with a negative value. This might happen" 993 + " during layout changes but may be sign of a bug"); 994 } 995 return; 996 } 997 final int limit = mOrientationHelper.getEnd() - dt; 998 if (mShouldReverseLayout) { 999 for (int i = 0; i < childCount; i++) { 1000 View child = getChildAt(i); 1001 if (mOrientationHelper.getDecoratedStart(child) < limit) {// stop here 1002 recycleChildren(recycler, 0, i); 1003 return; 1004 } 1005 } 1006 } else { 1007 for (int i = childCount - 1; i >= 0; i--) { 1008 View child = getChildAt(i); 1009 if (mOrientationHelper.getDecoratedStart(child) < limit) {// stop here 1010 recycleChildren(recycler, childCount - 1, i); 1011 return; 1012 } 1013 } 1014 } 1015 1016 } 1017 1018 /** 1019 * Helper method to call appropriate recycle method depending on current layout direction 1020 * 1021 * @param recycler Current recycler that is attached to RecyclerView 1022 * @param layoutState Current layout state. Right now, this object does not change but 1023 * we may consider moving it out of this view so passing around as a 1024 * parameter for now, rather than accessing {@link #mLayoutState} 1025 * @see #recycleViewsFromStart(android.support.v7.widget.RecyclerView.Recycler, int) 1026 * @see #recycleViewsFromEnd(android.support.v7.widget.RecyclerView.Recycler, int) 1027 * @see android.support.v7.widget.LinearLayoutManager.LayoutState#mLayoutDirection 1028 */ 1029 private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { 1030 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1031 recycleViewsFromEnd(recycler, layoutState.mScrollingOffset); 1032 } else { 1033 recycleViewsFromStart(recycler, layoutState.mScrollingOffset); 1034 } 1035 } 1036 1037 /** 1038 * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly 1039 * independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager} 1040 * and with little change, can be made publicly available as a helper class. 1041 * 1042 * @param recycler Current recycler that is attached to RecyclerView 1043 * @param layoutState Configuration on how we should fill out the available space. 1044 * @param state Context passed by the RecyclerView to control scroll steps. 1045 * @param stopOnFocusable If true, filling stops in the first focusable new child 1046 * @return Number of pixels that it added. Useful for scoll functions. 1047 */ 1048 private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, 1049 RecyclerView.State state, 1050 boolean stopOnFocusable) { 1051 // max offset we should set is mFastScroll + available 1052 final int start = layoutState.mAvailable; 1053 if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) { 1054 // TODO ugly bug fix. should not happen 1055 if (layoutState.mAvailable < 0) { 1056 layoutState.mScrollingOffset += layoutState.mAvailable; 1057 } 1058 recycleByLayoutState(recycler, layoutState); 1059 } 1060 int remainingSpace = layoutState.mAvailable + layoutState.mExtra; 1061 while (remainingSpace > 0 && layoutState.hasMore(state)) { 1062 View view = layoutState.next(recycler); 1063 if (view == null) { 1064 if (DEBUG && layoutState.mScrapList == null) { 1065 throw new RuntimeException("received null view when unexpected"); 1066 } 1067 // if we are laying out views in scrap, this may return null which means there is 1068 // no more items to layout. 1069 break; 1070 } 1071 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); 1072 if (mLayoutState.mScrapList == null) { 1073 if (mShouldReverseLayout == (layoutState.mLayoutDirection 1074 == LayoutState.LAYOUT_START)) { 1075 addView(view); 1076 } else { 1077 addView(view, 0); 1078 } 1079 } else { 1080 if (mShouldReverseLayout == (layoutState.mLayoutDirection 1081 == LayoutState.LAYOUT_START)) { 1082 addDisappearingView(view); 1083 } else { 1084 addDisappearingView(view, 0); 1085 } 1086 } 1087 measureChildWithMargins(view, 0, 0); 1088 int consumed = mOrientationHelper.getDecoratedMeasurement(view); 1089 int left, top, right, bottom; 1090 if (mOrientation == VERTICAL) { 1091 if (isLayoutRTL()) { 1092 right = getWidth() - getPaddingRight(); 1093 left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); 1094 } else { 1095 left = getPaddingLeft(); 1096 right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); 1097 } 1098 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1099 bottom = layoutState.mOffset; 1100 top = layoutState.mOffset - consumed; 1101 } else { 1102 top = layoutState.mOffset; 1103 bottom = layoutState.mOffset + consumed; 1104 } 1105 } else { 1106 top = getPaddingTop(); 1107 bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); 1108 1109 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1110 right = layoutState.mOffset; 1111 left = layoutState.mOffset - consumed; 1112 } else { 1113 left = layoutState.mOffset; 1114 right = layoutState.mOffset + consumed; 1115 } 1116 } 1117 // We calculate everything with View's bounding box (which includes decor and margins) 1118 // To calculate correct layout position, we subtract margins. 1119 layoutDecorated(view, left + params.leftMargin, top + params.topMargin 1120 , right - params.rightMargin, bottom - params.bottomMargin); 1121 if (DEBUG) { 1122 Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" 1123 + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" 1124 + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)); 1125 } 1126 layoutState.mOffset += consumed * layoutState.mLayoutDirection; 1127 1128 if (!params.isItemRemoved() || mLayoutState.mScrapList == null) { 1129 layoutState.mAvailable -= consumed; 1130 // we keep a separate remaining space because mAvailable is important for recycling 1131 remainingSpace -= consumed; 1132 } 1133 1134 if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) { 1135 layoutState.mScrollingOffset += consumed; 1136 if (layoutState.mAvailable < 0) { 1137 layoutState.mScrollingOffset += layoutState.mAvailable; 1138 } 1139 recycleByLayoutState(recycler, layoutState); 1140 } 1141 if (stopOnFocusable && view.isFocusable()) { 1142 break; 1143 } 1144 1145 if (state != null && state.getTargetScrollPosition() == getPosition(view)) { 1146 break; 1147 } 1148 } 1149 if (DEBUG) { 1150 validateChildOrder(); 1151 } 1152 return start - layoutState.mAvailable; 1153 } 1154 1155 /** 1156 * Converts a focusDirection to orientation. 1157 * 1158 * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, 1159 * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, 1160 * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} 1161 * or 0 for not applicable 1162 * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction 1163 * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise. 1164 */ 1165 private int convertFocusDirectionToLayoutDirection(int focusDirection) { 1166 switch (focusDirection) { 1167 case View.FOCUS_BACKWARD: 1168 return LayoutState.LAYOUT_START; 1169 case View.FOCUS_FORWARD: 1170 return LayoutState.LAYOUT_END; 1171 case View.FOCUS_UP: 1172 return mOrientation == VERTICAL ? LayoutState.LAYOUT_START 1173 : LayoutState.INVALID_LAYOUT; 1174 case View.FOCUS_DOWN: 1175 return mOrientation == VERTICAL ? LayoutState.LAYOUT_END 1176 : LayoutState.INVALID_LAYOUT; 1177 case View.FOCUS_LEFT: 1178 return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START 1179 : LayoutState.INVALID_LAYOUT; 1180 case View.FOCUS_RIGHT: 1181 return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END 1182 : LayoutState.INVALID_LAYOUT; 1183 default: 1184 if (DEBUG) { 1185 Log.d(TAG, "Unknown focus request:" + focusDirection); 1186 } 1187 return LayoutState.INVALID_LAYOUT; 1188 } 1189 1190 } 1191 1192 /** 1193 * Convenience method to find the child closes to start. Caller should check it has enough 1194 * children. 1195 * @return The child closes to start of the layout from user's perspective. 1196 */ 1197 private View getChildClosestToStart() { 1198 return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0); 1199 } 1200 1201 /** 1202 * Convenience method to find the child closes to end. Caller should check it has enough 1203 * children. 1204 * @return The child closes to end of the layout from user's perspective. 1205 */ 1206 private View getChildClosestToEnd() { 1207 return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1); 1208 } 1209 1210 /** 1211 * Returns the adapter position of the first visible view. 1212 * <p> 1213 * Note that, this value is not affected by layout orientation or item order traversal. 1214 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 1215 * not in the layout. 1216 * <p> 1217 * If RecyclerView has item decorators, they will be considered in calculations as well. 1218 * <p> 1219 * LinearLayoutManager may pre-cache some views that are not necessarily visible. Those views 1220 * are ignored in this method. 1221 * 1222 * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if 1223 * there aren't any visible items. 1224 * @see #findFirstCompletelyVisibleItemPosition() 1225 * @see #findLastVisibleItemPosition() 1226 */ 1227 public int findFirstVisibleItemPosition() { 1228 return findOneVisibleChild(0, getChildCount(), false); 1229 } 1230 1231 /** 1232 * Returns the adapter position of the first fully visible view. 1233 * <p> 1234 * Note that bounds check is only performed in the current orientation. That means, if 1235 * LinearLayoutManager is horizontal, it will only check the view's left and right edges. 1236 * 1237 * @return The adapter position of the first fully visible item or 1238 * {@link RecyclerView#NO_POSITION} if there aren't any visible items. 1239 * @see #findFirstVisibleItemPosition() 1240 * @see #findLastCompletelyVisibleItemPosition() 1241 */ 1242 public int findFirstCompletelyVisibleItemPosition() { 1243 return findOneVisibleChild(0, getChildCount(), true); 1244 } 1245 1246 /** 1247 * Returns the adapter position of the last visible view. 1248 * <p> 1249 * Note that, this value is not affected by layout orientation or item order traversal. 1250 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, 1251 * not in the layout. 1252 * <p> 1253 * If RecyclerView has item decorators, they will be considered in calculations as well. 1254 * <p> 1255 * LinearLayoutManager may pre-cache some views that are not necessarily visible. Those views 1256 * are ignored in this method. 1257 * 1258 * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if 1259 * there aren't any visible items. 1260 * @see #findLastCompletelyVisibleItemPosition() 1261 * @see #findFirstVisibleItemPosition() 1262 */ 1263 public int findLastVisibleItemPosition() { 1264 return findOneVisibleChild(getChildCount() - 1, -1, false); 1265 } 1266 1267 /** 1268 * Returns the adapter position of the last fully visible view. 1269 * <p> 1270 * Note that bounds check is only performed in the current orientation. That means, if 1271 * LinearLayoutManager is horizontal, it will only check the view's left and right edges. 1272 * 1273 * @return The adapter position of the last fully visible view or 1274 * {@link RecyclerView#NO_POSITION} if there aren't any visible items. 1275 * @see #findLastVisibleItemPosition() 1276 * @see #findFirstCompletelyVisibleItemPosition() 1277 */ 1278 public int findLastCompletelyVisibleItemPosition() { 1279 return findOneVisibleChild(getChildCount() - 1, -1, true); 1280 } 1281 1282 int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { 1283 final int start = mOrientationHelper.getStartAfterPadding(); 1284 final int end = mOrientationHelper.getEndAfterPadding(); 1285 final int next = toIndex > fromIndex ? 1 : -1; 1286 for (int i = fromIndex; i != toIndex; i+=next) { 1287 final View child = getChildAt(i); 1288 final int childStart = mOrientationHelper.getDecoratedStart(child); 1289 final int childEnd = mOrientationHelper.getDecoratedEnd(child); 1290 if (childStart < end && childEnd > start) { 1291 if (completelyVisible) { 1292 if (childStart >= start && childEnd <= end) { 1293 return getPosition(child); 1294 } 1295 } else { 1296 return getPosition(child); 1297 } 1298 } 1299 } 1300 return RecyclerView.NO_POSITION; 1301 } 1302 1303 @Override 1304 public View onFocusSearchFailed(View focused, int focusDirection, 1305 RecyclerView.Recycler recycler, RecyclerView.State state) { 1306 resolveShouldLayoutReverse(); 1307 if (getChildCount() == 0) { 1308 return null; 1309 } 1310 1311 final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection); 1312 if (layoutDir == LayoutState.INVALID_LAYOUT) { 1313 return null; 1314 } 1315 final View referenceChild; 1316 if (layoutDir == LayoutState.LAYOUT_START) { 1317 referenceChild = getChildClosestToStart(); 1318 } else { 1319 referenceChild = getChildClosestToEnd(); 1320 } 1321 ensureLayoutState(); 1322 final int maxScroll = (int) (MAX_SCROLL_FACTOR * (mOrientationHelper.getEndAfterPadding() - 1323 mOrientationHelper.getStartAfterPadding())); 1324 updateLayoutState(layoutDir, maxScroll, false, state); 1325 mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN; 1326 fill(recycler, mLayoutState, state, true); 1327 final View nextFocus; 1328 if (layoutDir == LayoutState.LAYOUT_START) { 1329 nextFocus = getChildClosestToStart(); 1330 } else { 1331 nextFocus = getChildClosestToEnd(); 1332 } 1333 if (nextFocus == referenceChild || !nextFocus.isFocusable()) { 1334 return null; 1335 } 1336 return nextFocus; 1337 } 1338 1339 /** 1340 * Used for debugging. 1341 * Logs the internal representation of children to default logger. 1342 */ 1343 private void logChildren() { 1344 Log.d(TAG, "internal representation of views on the screen"); 1345 for (int i = 0; i < getChildCount(); i++) { 1346 View child = getChildAt(i); 1347 Log.d(TAG, "item " + getPosition(child) + ", coord:" 1348 + mOrientationHelper.getDecoratedStart(child)); 1349 } 1350 Log.d(TAG, "=============="); 1351 } 1352 1353 /** 1354 * Used for debugging. 1355 * Validates that child views are laid out in correct order. This is important because rest of 1356 * the algorithm relies on this constraint. 1357 * 1358 * In default layout, child 0 should be closest to screen position 0 and last child should be 1359 * closest to position WIDTH or HEIGHT. 1360 * In reverse layout, last child should be closes to screen position 0 and first child should 1361 * be closest to position WIDTH or HEIGHT 1362 */ 1363 private void validateChildOrder() { 1364 Log.d(TAG, "validating child count " + getChildCount()); 1365 if (getChildCount() < 1) { 1366 return; 1367 } 1368 int lastPos = getPosition(getChildAt(0)); 1369 int lastScreenLoc = mOrientationHelper.getDecoratedStart(getChildAt(0)); 1370 if (mShouldReverseLayout) { 1371 for (int i = 1; i < getChildCount(); i++) { 1372 View child = getChildAt(i); 1373 int pos = getPosition(child); 1374 int screenLoc = mOrientationHelper.getDecoratedStart(child); 1375 if (pos < lastPos) { 1376 logChildren(); 1377 throw new RuntimeException("detected invalid position. loc invalid? " + 1378 (screenLoc < lastScreenLoc)); 1379 } 1380 if (screenLoc > lastScreenLoc) { 1381 logChildren(); 1382 throw new RuntimeException("detected invalid location"); 1383 } 1384 } 1385 } else { 1386 for (int i = 1; i < getChildCount(); i++) { 1387 View child = getChildAt(i); 1388 int pos = getPosition(child); 1389 int screenLoc = mOrientationHelper.getDecoratedStart(child); 1390 if (pos < lastPos) { 1391 logChildren(); 1392 throw new RuntimeException("detected invalid position. loc invalid? " + 1393 (screenLoc < lastScreenLoc)); 1394 } 1395 if (screenLoc < lastScreenLoc) { 1396 logChildren(); 1397 throw new RuntimeException("detected invalid location"); 1398 } 1399 } 1400 } 1401 } 1402 1403 @Override 1404 public boolean supportsPredictiveItemAnimations() { 1405 return true; 1406 } 1407 1408 /** 1409 * Helper class that keeps temporary state while {LayoutManager} is filling out the empty 1410 * space. 1411 */ 1412 private static class LayoutState { 1413 1414 final static String TAG = "LinearLayoutManager#LayoutState"; 1415 1416 final static int LAYOUT_START = -1; 1417 1418 final static int LAYOUT_END = 1; 1419 1420 final static int INVALID_LAYOUT = Integer.MIN_VALUE; 1421 1422 final static int ITEM_DIRECTION_HEAD = -1; 1423 1424 final static int ITEM_DIRECTION_TAIL = 1; 1425 1426 final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE; 1427 1428 /** 1429 * Pixel offset where layout should start 1430 */ 1431 int mOffset; 1432 1433 /** 1434 * Number of pixels that we should fill, in the layout direction. 1435 */ 1436 int mAvailable; 1437 1438 /** 1439 * Current position on the adapter to get the next item. 1440 */ 1441 int mCurrentPosition; 1442 1443 /** 1444 * Defines the direction in which the data adapter is traversed. 1445 * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL} 1446 */ 1447 int mItemDirection; 1448 1449 /** 1450 * Defines the direction in which the layout is filled. 1451 * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END} 1452 */ 1453 int mLayoutDirection; 1454 1455 /** 1456 * Used when LayoutState is constructed in a scrolling state. 1457 * It should be set the amount of scrolling we can make without creating a new view. 1458 * Settings this is required for efficient view recycling. 1459 */ 1460 int mScrollingOffset; 1461 1462 /** 1463 * Used if you want to pre-layout items that are not yet visible. 1464 * The difference with {@link #mAvailable} is that, when recycling, distance laid out for 1465 * {@link #mExtra} is not considered to avoid recycling visible children. 1466 */ 1467 int mExtra = 0; 1468 1469 /** 1470 * When LLM needs to layout particular views, it sets this list in which case, LayoutState 1471 * will only return views from this list and return null if it cannot find an item. 1472 */ 1473 List<RecyclerView.ViewHolder> mScrapList = null; 1474 1475 /** 1476 * @return true if there are more items in the data adapter 1477 */ 1478 boolean hasMore(RecyclerView.State state) { 1479 return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount(); 1480 } 1481 1482 /** 1483 * Gets the view for the next element that we should layout. 1484 * Also updates current item index to the next item, based on {@link #mItemDirection} 1485 * 1486 * @return The next element that we should layout. 1487 */ 1488 View next(RecyclerView.Recycler recycler) { 1489 if (mScrapList != null) { 1490 return nextFromLimitedList(); 1491 } 1492 final View view = recycler.getViewForPosition(mCurrentPosition); 1493 mCurrentPosition += mItemDirection; 1494 return view; 1495 } 1496 1497 /** 1498 * Returns next item from limited list. 1499 * <p> 1500 * Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection 1501 * 1502 * @return View if an item in the current position or direction exists if not null. 1503 */ 1504 private View nextFromLimitedList() { 1505 int size = mScrapList.size(); 1506 RecyclerView.ViewHolder closest = null; 1507 int closestDistance = Integer.MAX_VALUE; 1508 for (int i = 0; i < size; i++) { 1509 RecyclerView.ViewHolder viewHolder = mScrapList.get(i); 1510 final int distance = (viewHolder.getPosition() - mCurrentPosition) * mItemDirection; 1511 if (distance < 0) { 1512 continue; // item is not in current direction 1513 } 1514 if (distance < closestDistance) { 1515 closest = viewHolder; 1516 closestDistance = distance; 1517 if (distance == 0) { 1518 break; 1519 } 1520 } 1521 } 1522 if (DEBUG) { 1523 Log.d(TAG, "layout from scrap. found view:?" + (closest != null)); 1524 } 1525 if (closest != null) { 1526 mCurrentPosition = closest.getPosition() + mItemDirection; 1527 return closest.itemView; 1528 } 1529 return null; 1530 } 1531 1532 void log() { 1533 Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" + 1534 mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection); 1535 } 1536 } 1537 1538 static class SavedState implements Parcelable { 1539 1540 int mOrientation; 1541 1542 int mAnchorPosition; 1543 1544 int mAnchorOffset; 1545 1546 boolean mReverseLayout; 1547 1548 boolean mStackFromEnd; 1549 1550 boolean mAnchorLayoutFromEnd; 1551 1552 boolean mRecycleChildrenOnDetach; 1553 1554 1555 public SavedState() { 1556 1557 } 1558 1559 SavedState(Parcel in) { 1560 mOrientation = in.readInt(); 1561 mAnchorPosition = in.readInt(); 1562 mAnchorOffset = in.readInt(); 1563 mReverseLayout = in.readInt() == 1; 1564 mStackFromEnd = in.readInt() == 1; 1565 mAnchorLayoutFromEnd = in.readInt() == 1; 1566 mRecycleChildrenOnDetach = in.readInt() == 1; 1567 } 1568 1569 public SavedState(SavedState other) { 1570 mOrientation = other.mOrientation; 1571 mAnchorPosition = other.mAnchorPosition; 1572 mAnchorOffset = other.mAnchorOffset; 1573 mReverseLayout = other.mReverseLayout; 1574 mStackFromEnd = other.mStackFromEnd; 1575 mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; 1576 mRecycleChildrenOnDetach = other.mRecycleChildrenOnDetach; 1577 } 1578 1579 boolean hasValidAnchor() { 1580 return mAnchorPosition >= 0; 1581 } 1582 1583 void invalidateAnchor() { 1584 mAnchorPosition = RecyclerView.NO_POSITION; 1585 } 1586 1587 @Override 1588 public int describeContents() { 1589 return 0; 1590 } 1591 1592 @Override 1593 public void writeToParcel(Parcel dest, int flags) { 1594 dest.writeInt(mOrientation); 1595 dest.writeInt(mAnchorPosition); 1596 dest.writeInt(mAnchorOffset); 1597 dest.writeInt(mReverseLayout ? 1 : 0); 1598 dest.writeInt(mStackFromEnd ? 1 : 0); 1599 dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); 1600 dest.writeInt(mRecycleChildrenOnDetach ? 1 : 0); 1601 } 1602 1603 public static final Parcelable.Creator<SavedState> CREATOR 1604 = new Parcelable.Creator<SavedState>() { 1605 @Override 1606 public SavedState createFromParcel(Parcel in) { 1607 return new SavedState(in); 1608 } 1609 1610 @Override 1611 public SavedState[] newArray(int size) { 1612 return new SavedState[size]; 1613 } 1614 }; 1615 } 1616} 1617