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