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