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