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