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