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