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