2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android
.support
.v7
.widget
;
19 import android
.content
.Context
;
20 import android
.graphics
.PointF
;
21 import android
.graphics
.Rect
;
22 import android
.os
.Parcel
;
23 import android
.os
.Parcelable
;
24 import android
.support
.v4
.view
.ViewCompat
;
25 import android
.support
.v4
.view
.accessibility
.AccessibilityEventCompat
;
26 import android
.support
.v4
.view
.accessibility
.AccessibilityNodeInfoCompat
;
27 import android
.support
.v4
.view
.accessibility
.AccessibilityRecordCompat
;
28 import android
.util
.AttributeSet
;
29 import android
.util
.Log
;
30 import android
.view
.View
;
31 import android
.view
.ViewGroup
;
32 import android
.view
.accessibility
.AccessibilityEvent
;
34 import java
.util
.ArrayList
;
35 import java
.util
.Arrays
;
36 import java
.util
.BitSet
;
37 import java
.util
.List
;
39 import static android
.support
.v7
.widget
.LayoutState
.LAYOUT_START
;
40 import static android
.support
.v7
.widget
.LayoutState
.LAYOUT_END
;
41 import static android
.support
.v7
.widget
.LayoutState
.ITEM_DIRECTION_HEAD
;
42 import static android
.support
.v7
.widget
.LayoutState
.ITEM_DIRECTION_TAIL
;
43 import static android
.support
.v7
.widget
.RecyclerView
.NO_POSITION
;
46 * A LayoutManager that lays out children in a staggered grid formation.
47 * It supports horizontal & vertical layout as well as an ability to layout children in reverse.
49 * Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps,
50 * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can
51 * control this behavior via {@link #setGapStrategy(int)}.
53 public class StaggeredGridLayoutManager
extends RecyclerView
.LayoutManager
{
55 public static final String TAG
= "StaggeredGridLayoutManager";
57 private static final boolean DEBUG
= false;
59 public static final int HORIZONTAL
= OrientationHelper
.HORIZONTAL
;
61 public static final int VERTICAL
= OrientationHelper
.VERTICAL
;
64 * Does not do anything to hide gaps.
66 public static final int GAP_HANDLING_NONE
= 0;
69 public static final int GAP_HANDLING_LAZY
= 1;
72 * When scroll state is changed to {@link RecyclerView#SCROLL_STATE_IDLE}, StaggeredGrid will
73 * check if there are gaps in the because of full span items. If it finds, it will re-layout
74 * and move items to correct positions with animations.
76 * For example, if LayoutManager ends up with the following layout due to adapter changes:
83 * It will animate to the following state:
90 public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
= 2;
92 private static final int INVALID_OFFSET
= Integer
.MIN_VALUE
;
97 private int mSpanCount
= -1;
99 private Span
[] mSpans
;
102 * Primary orientation is the layout's orientation, secondary orientation is the orientation
103 * for spans. Having both makes code much cleaner for calculations.
105 OrientationHelper mPrimaryOrientation
;
106 OrientationHelper mSecondaryOrientation
;
108 private int mOrientation
;
111 * The width or height per span, depending on the orientation.
113 private int mSizePerSpan
;
115 private LayoutState mLayoutState
;
117 private boolean mReverseLayout
= false;
120 * Aggregated reverse layout value that takes RTL into account.
122 boolean mShouldReverseLayout
= false;
125 * Temporary variable used during fill method to check which spans needs to be filled.
127 private BitSet mRemainingSpans
;
130 * When LayoutManager needs to scroll to a position, it sets this variable and requests a
131 * layout which will check this variable and re-layout accordingly.
133 int mPendingScrollPosition
= NO_POSITION
;
136 * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is
139 int mPendingScrollPositionOffset
= INVALID_OFFSET
;
142 * Keeps the mapping between the adapter positions and spans. This is necessary to provide
143 * a consistent experience when user scrolls the list.
145 LazySpanLookup mLazySpanLookup
= new LazySpanLookup();
148 * how we handle gaps in UI.
150 private int mGapStrategy
= GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
;
153 * Saved state needs this information to properly layout on restore.
155 private boolean mLastLayoutFromEnd
;
158 * Saved state and onLayout needs this information to re-layout properly
160 private boolean mLastLayoutRTL
;
163 * SavedState is not handled until a layout happens. This is where we keep it until next
166 private SavedState mPendingSavedState
;
169 * Re-used measurement specs. updated by onLayout.
171 private int mFullSizeSpec
, mWidthSpec
, mHeightSpec
;
174 * Re-used rectangle to get child decor offsets.
176 private final Rect mTmpRect
= new Rect();
179 * Re-used anchor info.
181 private final AnchorInfo mAnchorInfo
= new AnchorInfo();
184 * If a full span item is invalid / or created in reverse direction; it may create gaps in
185 * the UI. While laying out, if such case is detected, we set this flag.
187 * After scrolling stops, we check this flag and if it is set, re-layout.
189 private boolean mLaidOutInvalidFullSpan
= false;
192 * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}.
193 * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}
195 private boolean mSmoothScrollbarEnabled
= true;
197 private final Runnable mCheckForGapsRunnable
= new Runnable() {
205 * Constructor used when layout manager is set in XML by RecyclerView attribute
206 * "layoutManager". Defaults to single column and vertical.
208 public StaggeredGridLayoutManager(Context context
, AttributeSet attrs
, int defStyleAttr
,
210 Properties properties
= getProperties(context
, attrs
, defStyleAttr
, defStyleRes
);
211 setOrientation(properties
.orientation
);
212 setSpanCount(properties
.spanCount
);
213 setReverseLayout(properties
.reverseLayout
);
217 * Creates a StaggeredGridLayoutManager with given parameters.
219 * @param spanCount If orientation is vertical, spanCount is number of columns. If
220 * orientation is horizontal, spanCount is number of rows.
221 * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
223 public StaggeredGridLayoutManager(int spanCount
, int orientation
) {
224 mOrientation
= orientation
;
225 setSpanCount(spanCount
);
229 * Checks for gaps in the UI that may be caused by adapter changes.
231 * When a full span item is laid out in reverse direction, it sets a flag which we check when
232 * scroll is stopped (or re-layout happens) and re-layout after first valid item.
234 private boolean checkForGaps() {
235 if (getChildCount() == 0 || mGapStrategy
== GAP_HANDLING_NONE
|| !isAttachedToWindow()) {
238 final int minPos
, maxPos
;
239 if (mShouldReverseLayout
) {
240 minPos
= getLastChildPosition();
241 maxPos
= getFirstChildPosition();
243 minPos
= getFirstChildPosition();
244 maxPos
= getLastChildPosition();
247 View gapView
= hasGapsToFix();
248 if (gapView
!= null) {
249 mLazySpanLookup
.clear();
250 requestSimpleAnimationsInNextLayout();
255 if (!mLaidOutInvalidFullSpan
) {
258 int invalidGapDir
= mShouldReverseLayout ? LAYOUT_START
: LAYOUT_END
;
259 final LazySpanLookup
.FullSpanItem invalidFsi
= mLazySpanLookup
260 .getFirstFullSpanItemInRange(minPos
, maxPos
+ 1, invalidGapDir
, true);
261 if (invalidFsi
== null) {
262 mLaidOutInvalidFullSpan
= false;
263 mLazySpanLookup
.forceInvalidateAfter(maxPos
+ 1);
266 final LazySpanLookup
.FullSpanItem validFsi
= mLazySpanLookup
267 .getFirstFullSpanItemInRange(minPos
, invalidFsi
.mPosition
,
268 invalidGapDir
* -1, true);
269 if (validFsi
== null) {
270 mLazySpanLookup
.forceInvalidateAfter(invalidFsi
.mPosition
);
272 mLazySpanLookup
.forceInvalidateAfter(validFsi
.mPosition
+ 1);
274 requestSimpleAnimationsInNextLayout();
280 public void onScrollStateChanged(int state
) {
281 if (state
== RecyclerView
.SCROLL_STATE_IDLE
) {
287 public void onDetachedFromWindow(RecyclerView view
, RecyclerView
.Recycler recycler
) {
288 removeCallbacks(mCheckForGapsRunnable
);
289 for (int i
= 0; i
< mSpanCount
; i
++) {
295 * Checks for gaps if we've reached to the top of the list.
297 * Intermediate gaps created by full span items are tracked via mLaidOutInvalidFullSpan field.
299 View
hasGapsToFix() {
300 int startChildIndex
= 0;
301 int endChildIndex
= getChildCount() - 1;
302 BitSet mSpansToCheck
= new BitSet(mSpanCount
);
303 mSpansToCheck
.set(0, mSpanCount
, true);
305 final int firstChildIndex
, childLimit
;
306 final int preferredSpanDir
= mOrientation
== VERTICAL
&& isLayoutRTL() ?
1 : -1;
308 if (mShouldReverseLayout
) {
309 firstChildIndex
= endChildIndex
;
310 childLimit
= startChildIndex
- 1;
312 firstChildIndex
= startChildIndex
;
313 childLimit
= endChildIndex
+ 1;
315 final int nextChildDiff
= firstChildIndex
< childLimit ?
1 : -1;
316 for (int i
= firstChildIndex
; i
!= childLimit
; i
+= nextChildDiff
) {
317 View child
= getChildAt(i
);
318 LayoutParams lp
= (LayoutParams
) child
.getLayoutParams();
319 if (mSpansToCheck
.get(lp
.mSpan
.mIndex
)) {
320 if (checkSpanForGap(lp
.mSpan
)) {
323 mSpansToCheck
.clear(lp
.mSpan
.mIndex
);
326 continue; // quick reject
329 if (i
+ nextChildDiff
!= childLimit
) {
330 View nextChild
= getChildAt(i
+ nextChildDiff
);
331 boolean compareSpans
= false;
332 if (mShouldReverseLayout
) {
333 // ensure child's end is below nextChild's end
334 int myEnd
= mPrimaryOrientation
.getDecoratedEnd(child
);
335 int nextEnd
= mPrimaryOrientation
.getDecoratedEnd(nextChild
);
336 if (myEnd
< nextEnd
) {
337 return child
;//i should have a better position
338 } else if (myEnd
== nextEnd
) {
342 int myStart
= mPrimaryOrientation
.getDecoratedStart(child
);
343 int nextStart
= mPrimaryOrientation
.getDecoratedStart(nextChild
);
344 if (myStart
> nextStart
) {
345 return child
;//i should have a better position
346 } else if (myStart
== nextStart
) {
351 // equal, check span indices.
352 LayoutParams nextLp
= (LayoutParams
) nextChild
.getLayoutParams();
353 if (lp
.mSpan
.mIndex
- nextLp
.mSpan
.mIndex
< 0 != preferredSpanDir
< 0) {
359 // everything looks good
363 private boolean checkSpanForGap(Span span
) {
364 if (mShouldReverseLayout
) {
365 if (span
.getEndLine() < mPrimaryOrientation
.getEndAfterPadding()) {
368 } else if (span
.getStartLine() > mPrimaryOrientation
.getStartAfterPadding()) {
375 * Sets the number of spans for the layout. This will invalidate all of the span assignments
378 * Calling this method will automatically result in a new layout request unless the spanCount
379 * parameter is equal to current span count.
381 * @param spanCount Number of spans to layout
383 public void setSpanCount(int spanCount
) {
384 assertNotInLayoutOrScroll(null);
385 if (spanCount
!= mSpanCount
) {
386 invalidateSpanAssignments();
387 mSpanCount
= spanCount
;
388 mRemainingSpans
= new BitSet(mSpanCount
);
389 mSpans
= new Span
[mSpanCount
];
390 for (int i
= 0; i
< mSpanCount
; i
++) {
391 mSpans
[i
] = new Span(i
);
398 * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep
399 * scroll position if this method is called after views are laid out.
401 * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
403 public void setOrientation(int orientation
) {
404 if (orientation
!= HORIZONTAL
&& orientation
!= VERTICAL
) {
405 throw new IllegalArgumentException("invalid orientation.");
407 assertNotInLayoutOrScroll(null);
408 if (orientation
== mOrientation
) {
411 mOrientation
= orientation
;
412 if (mPrimaryOrientation
!= null && mSecondaryOrientation
!= null) {
414 OrientationHelper tmp
= mPrimaryOrientation
;
415 mPrimaryOrientation
= mSecondaryOrientation
;
416 mSecondaryOrientation
= tmp
;
422 * Sets whether LayoutManager should start laying out items from the end of the UI. The order
423 * items are traversed is not affected by this call.
425 * For vertical layout, if it is set to <code>true</code>, first item will be at the bottom of
428 * For horizontal layouts, it depends on the layout direction.
429 * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if
430 * {@link RecyclerView}} is RTL, it will layout from LTR.
432 * @param reverseLayout Whether layout should be in reverse or not
434 public void setReverseLayout(boolean reverseLayout
) {
435 assertNotInLayoutOrScroll(null);
436 if (mPendingSavedState
!= null && mPendingSavedState
.mReverseLayout
!= reverseLayout
) {
437 mPendingSavedState
.mReverseLayout
= reverseLayout
;
439 mReverseLayout
= reverseLayout
;
444 * Returns the current gap handling strategy for StaggeredGridLayoutManager.
446 * Staggered grid may have gaps in the layout due to changes in the adapter. To avoid gaps,
447 * StaggeredGridLayoutManager provides 2 options. Check {@link #GAP_HANDLING_NONE} and
448 * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} for details.
450 * By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}.
452 * @return Current gap handling strategy.
453 * @see #setGapStrategy(int)
454 * @see #GAP_HANDLING_NONE
455 * @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
457 public int getGapStrategy() {
462 * Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter
463 * is different than the current strategy, calling this method will trigger a layout request.
465 * @param gapStrategy The new gap handling strategy. Should be
466 * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or {@link
467 * #GAP_HANDLING_NONE}.
468 * @see #getGapStrategy()
470 public void setGapStrategy(int gapStrategy
) {
471 assertNotInLayoutOrScroll(null);
472 if (gapStrategy
== mGapStrategy
) {
475 if (gapStrategy
!= GAP_HANDLING_NONE
&&
476 gapStrategy
!= GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
) {
477 throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE "
478 + "or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS");
480 mGapStrategy
= gapStrategy
;
485 public void assertNotInLayoutOrScroll(String message
) {
486 if (mPendingSavedState
== null) {
487 super.assertNotInLayoutOrScroll(message
);
492 * Returns the number of spans laid out by StaggeredGridLayoutManager.
494 * @return Number of spans in the layout
496 public int getSpanCount() {
501 * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items.
503 * If you need to cancel current assignments, you can call this method which will clear all
504 * assignments and request a new layout.
506 public void invalidateSpanAssignments() {
507 mLazySpanLookup
.clear();
511 private void ensureOrientationHelper() {
512 if (mPrimaryOrientation
== null) {
513 mPrimaryOrientation
= OrientationHelper
.createOrientationHelper(this, mOrientation
);
514 mSecondaryOrientation
= OrientationHelper
515 .createOrientationHelper(this, 1 - mOrientation
);
516 mLayoutState
= new LayoutState();
521 * Calculates the views' layout order. (e.g. from end to start or start to end)
522 * RTL layout support is applied automatically. So if layout is RTL and
523 * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
525 private void resolveShouldLayoutReverse() {
526 // A == B is the same result, but we rather keep it readable
527 if (mOrientation
== VERTICAL
|| !isLayoutRTL()) {
528 mShouldReverseLayout
= mReverseLayout
;
530 mShouldReverseLayout
= !mReverseLayout
;
534 boolean isLayoutRTL() {
535 return getLayoutDirection() == ViewCompat
.LAYOUT_DIRECTION_RTL
;
539 * Returns whether views are laid out in reverse order or not.
541 * Not that this value is not affected by RecyclerView's layout direction.
543 * @return True if layout is reversed, false otherwise
544 * @see #setReverseLayout(boolean)
546 public boolean getReverseLayout() {
547 return mReverseLayout
;
550 public void onLayoutChildren(RecyclerView
.Recycler recycler
, RecyclerView
.State state
) {
551 ensureOrientationHelper();
552 final AnchorInfo anchorInfo
= mAnchorInfo
;
555 if (mPendingSavedState
!= null || mPendingScrollPosition
!= NO_POSITION
) {
556 if (state
.getItemCount() == 0) {
557 removeAndRecycleAllViews(recycler
);
562 if (mPendingSavedState
!= null) {
563 applyPendingSavedState(anchorInfo
);
565 resolveShouldLayoutReverse();
566 anchorInfo
.mLayoutFromEnd
= mShouldReverseLayout
;
569 updateAnchorInfoForLayout(state
, anchorInfo
);
571 if (mPendingSavedState
== null) {
572 if (anchorInfo
.mLayoutFromEnd
!= mLastLayoutFromEnd
||
573 isLayoutRTL() != mLastLayoutRTL
) {
574 mLazySpanLookup
.clear();
575 anchorInfo
.mInvalidateOffsets
= true;
579 if (getChildCount() > 0 && (mPendingSavedState
== null ||
580 mPendingSavedState
.mSpanOffsetsSize
< 1)) {
581 if (anchorInfo
.mInvalidateOffsets
) {
582 for (int i
= 0; i
< mSpanCount
; i
++) {
583 // Scroll to position is set, clear.
585 if (anchorInfo
.mOffset
!= INVALID_OFFSET
) {
586 mSpans
[i
].setLine(anchorInfo
.mOffset
);
590 for (int i
= 0; i
< mSpanCount
; i
++) {
591 mSpans
[i
].cacheReferenceLineAndClear(mShouldReverseLayout
, anchorInfo
.mOffset
);
595 detachAndScrapAttachedViews(recycler
);
596 mLaidOutInvalidFullSpan
= false;
597 updateMeasureSpecs();
598 updateLayoutState(anchorInfo
.mPosition
, state
);
599 if (anchorInfo
.mLayoutFromEnd
) {
601 setLayoutStateDirection(LAYOUT_START
);
602 fill(recycler
, mLayoutState
, state
);
604 setLayoutStateDirection(LAYOUT_END
);
605 mLayoutState
.mCurrentPosition
= anchorInfo
.mPosition
+ mLayoutState
.mItemDirection
;
606 fill(recycler
, mLayoutState
, state
);
609 setLayoutStateDirection(LAYOUT_END
);
610 fill(recycler
, mLayoutState
, state
);
612 setLayoutStateDirection(LAYOUT_START
);
613 mLayoutState
.mCurrentPosition
= anchorInfo
.mPosition
+ mLayoutState
.mItemDirection
;
614 fill(recycler
, mLayoutState
, state
);
617 if (getChildCount() > 0) {
618 if (mShouldReverseLayout
) {
619 fixEndGap(recycler
, state
, true);
620 fixStartGap(recycler
, state
, false);
622 fixStartGap(recycler
, state
, true);
623 fixEndGap(recycler
, state
, false);
627 if (!state
.isPreLayout()) {
628 final boolean needToCheckForGaps
= mGapStrategy
!= GAP_HANDLING_NONE
629 && getChildCount() > 0
630 && (mLaidOutInvalidFullSpan
|| hasGapsToFix() != null);
631 if (needToCheckForGaps
) {
632 removeCallbacks(mCheckForGapsRunnable
);
633 postOnAnimation(mCheckForGapsRunnable
);
635 mPendingScrollPosition
= NO_POSITION
;
636 mPendingScrollPositionOffset
= INVALID_OFFSET
;
638 mLastLayoutFromEnd
= anchorInfo
.mLayoutFromEnd
;
639 mLastLayoutRTL
= isLayoutRTL();
640 mPendingSavedState
= null; // we don't need this anymore
643 private void applyPendingSavedState(AnchorInfo anchorInfo
) {
645 Log
.d(TAG
, "found saved state: " + mPendingSavedState
);
647 if (mPendingSavedState
.mSpanOffsetsSize
> 0) {
648 if (mPendingSavedState
.mSpanOffsetsSize
== mSpanCount
) {
649 for (int i
= 0; i
< mSpanCount
; i
++) {
651 int line
= mPendingSavedState
.mSpanOffsets
[i
];
652 if (line
!= Span
.INVALID_LINE
) {
653 if (mPendingSavedState
.mAnchorLayoutFromEnd
) {
654 line
+= mPrimaryOrientation
.getEndAfterPadding();
656 line
+= mPrimaryOrientation
.getStartAfterPadding();
659 mSpans
[i
].setLine(line
);
662 mPendingSavedState
.invalidateSpanInfo();
663 mPendingSavedState
.mAnchorPosition
= mPendingSavedState
.mVisibleAnchorPosition
;
666 mLastLayoutRTL
= mPendingSavedState
.mLastLayoutRTL
;
667 setReverseLayout(mPendingSavedState
.mReverseLayout
);
668 resolveShouldLayoutReverse();
670 if (mPendingSavedState
.mAnchorPosition
!= NO_POSITION
) {
671 mPendingScrollPosition
= mPendingSavedState
.mAnchorPosition
;
672 anchorInfo
.mLayoutFromEnd
= mPendingSavedState
.mAnchorLayoutFromEnd
;
674 anchorInfo
.mLayoutFromEnd
= mShouldReverseLayout
;
676 if (mPendingSavedState
.mSpanLookupSize
> 1) {
677 mLazySpanLookup
.mData
= mPendingSavedState
.mSpanLookup
;
678 mLazySpanLookup
.mFullSpanItems
= mPendingSavedState
.mFullSpanItems
;
682 void updateAnchorInfoForLayout(RecyclerView
.State state
, AnchorInfo anchorInfo
) {
683 if (updateAnchorFromPendingData(state
, anchorInfo
)) {
686 if (updateAnchorFromChildren(state
, anchorInfo
)) {
690 Log
.d(TAG
, "Deciding anchor info from fresh state");
692 anchorInfo
.assignCoordinateFromPadding();
693 anchorInfo
.mPosition
= 0;
696 private boolean updateAnchorFromChildren(RecyclerView
.State state
, AnchorInfo anchorInfo
) {
697 // We don't recycle views out of adapter order. This way, we can rely on the first or
698 // last child as the anchor position.
699 // Layout direction may change but we should select the child depending on the latest
700 // layout direction. Otherwise, we'll choose the wrong child.
701 anchorInfo
.mPosition
= mLastLayoutFromEnd
702 ?
findLastReferenceChildPosition(state
.getItemCount())
703 : findFirstReferenceChildPosition(state
.getItemCount());
704 anchorInfo
.mOffset
= INVALID_OFFSET
;
708 boolean updateAnchorFromPendingData(RecyclerView
.State state
, AnchorInfo anchorInfo
) {
709 // Validate scroll position if exists.
710 if (state
.isPreLayout() || mPendingScrollPosition
== NO_POSITION
) {
714 if (mPendingScrollPosition
< 0 || mPendingScrollPosition
>= state
.getItemCount()) {
715 mPendingScrollPosition
= NO_POSITION
;
716 mPendingScrollPositionOffset
= INVALID_OFFSET
;
720 if (mPendingSavedState
== null || mPendingSavedState
.mAnchorPosition
== NO_POSITION
721 || mPendingSavedState
.mSpanOffsetsSize
< 1) {
722 // If item is visible, make it fully visible.
723 final View child
= findViewByPosition(mPendingScrollPosition
);
725 // Use regular anchor position, offset according to pending offset and target
727 anchorInfo
.mPosition
= mShouldReverseLayout ?
getLastChildPosition()
728 : getFirstChildPosition();
730 if (mPendingScrollPositionOffset
!= INVALID_OFFSET
) {
731 if (anchorInfo
.mLayoutFromEnd
) {
732 final int target
= mPrimaryOrientation
.getEndAfterPadding() -
733 mPendingScrollPositionOffset
;
734 anchorInfo
.mOffset
= target
- mPrimaryOrientation
.getDecoratedEnd(child
);
736 final int target
= mPrimaryOrientation
.getStartAfterPadding() +
737 mPendingScrollPositionOffset
;
738 anchorInfo
.mOffset
= target
- mPrimaryOrientation
.getDecoratedStart(child
);
743 // no offset provided. Decide according to the child location
744 final int childSize
= mPrimaryOrientation
.getDecoratedMeasurement(child
);
745 if (childSize
> mPrimaryOrientation
.getTotalSpace()) {
746 // Item does not fit. Fix depending on layout direction.
747 anchorInfo
.mOffset
= anchorInfo
.mLayoutFromEnd
748 ? mPrimaryOrientation
.getEndAfterPadding()
749 : mPrimaryOrientation
.getStartAfterPadding();
753 final int startGap
= mPrimaryOrientation
.getDecoratedStart(child
)
754 - mPrimaryOrientation
.getStartAfterPadding();
756 anchorInfo
.mOffset
= -startGap
;
759 final int endGap
= mPrimaryOrientation
.getEndAfterPadding() -
760 mPrimaryOrientation
.getDecoratedEnd(child
);
762 anchorInfo
.mOffset
= endGap
;
765 // child already visible. just layout as usual
766 anchorInfo
.mOffset
= INVALID_OFFSET
;
768 // Child is not visible. Set anchor coordinate depending on in which direction
769 // child will be visible.
770 anchorInfo
.mPosition
= mPendingScrollPosition
;
771 if (mPendingScrollPositionOffset
== INVALID_OFFSET
) {
772 final int position
= calculateScrollDirectionForPosition(
773 anchorInfo
.mPosition
);
774 anchorInfo
.mLayoutFromEnd
= position
== LAYOUT_END
;
775 anchorInfo
.assignCoordinateFromPadding();
777 anchorInfo
.assignCoordinateFromPadding(mPendingScrollPositionOffset
);
779 anchorInfo
.mInvalidateOffsets
= true;
782 anchorInfo
.mOffset
= INVALID_OFFSET
;
783 anchorInfo
.mPosition
= mPendingScrollPosition
;
788 void updateMeasureSpecs() {
789 mSizePerSpan
= mSecondaryOrientation
.getTotalSpace() / mSpanCount
;
790 mFullSizeSpec
= View
.MeasureSpec
.makeMeasureSpec(
791 mSecondaryOrientation
.getTotalSpace(), View
.MeasureSpec
.EXACTLY
);
792 if (mOrientation
== VERTICAL
) {
793 mWidthSpec
= View
.MeasureSpec
.makeMeasureSpec(mSizePerSpan
, View
.MeasureSpec
.EXACTLY
);
794 mHeightSpec
= View
.MeasureSpec
.makeMeasureSpec(0, View
.MeasureSpec
.UNSPECIFIED
);
796 mHeightSpec
= View
.MeasureSpec
.makeMeasureSpec(mSizePerSpan
, View
.MeasureSpec
.EXACTLY
);
797 mWidthSpec
= View
.MeasureSpec
.makeMeasureSpec(0, View
.MeasureSpec
.UNSPECIFIED
);
802 public boolean supportsPredictiveItemAnimations() {
803 return mPendingSavedState
== null;
807 * Returns the adapter position of the first visible view for each span.
809 * Note that, this value is not affected by layout orientation or item order traversal.
810 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
813 * If RecyclerView has item decorators, they will be considered in calculations as well.
815 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
816 * views are ignored in this method.
818 * @param into An array to put the results into. If you don't provide any, LayoutManager will
820 * @return The adapter position of the first visible item in each span. If a span does not have
821 * any items, {@link RecyclerView#NO_POSITION} is returned for that span.
822 * @see #findFirstCompletelyVisibleItemPositions(int[])
823 * @see #findLastVisibleItemPositions(int[])
825 public int[] findFirstVisibleItemPositions(int[] into
) {
827 into
= new int[mSpanCount
];
828 } else if (into
.length
< mSpanCount
) {
829 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
830 + " to span count. Expected:" + mSpanCount
+ ", array size:" + into
.length
);
832 for (int i
= 0; i
< mSpanCount
; i
++) {
833 into
[i
] = mSpans
[i
].findFirstVisibleItemPosition();
839 * Returns the adapter position of the first completely visible view for each span.
841 * Note that, this value is not affected by layout orientation or item order traversal.
842 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
845 * If RecyclerView has item decorators, they will be considered in calculations as well.
847 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
848 * views are ignored in this method.
850 * @param into An array to put the results into. If you don't provide any, LayoutManager will
852 * @return The adapter position of the first fully visible item in each span. If a span does
853 * not have any items, {@link RecyclerView#NO_POSITION} is returned for that span.
854 * @see #findFirstVisibleItemPositions(int[])
855 * @see #findLastCompletelyVisibleItemPositions(int[])
857 public int[] findFirstCompletelyVisibleItemPositions(int[] into
) {
859 into
= new int[mSpanCount
];
860 } else if (into
.length
< mSpanCount
) {
861 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
862 + " to span count. Expected:" + mSpanCount
+ ", array size:" + into
.length
);
864 for (int i
= 0; i
< mSpanCount
; i
++) {
865 into
[i
] = mSpans
[i
].findFirstCompletelyVisibleItemPosition();
871 * Returns the adapter position of the last visible view for each span.
873 * Note that, this value is not affected by layout orientation or item order traversal.
874 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
877 * If RecyclerView has item decorators, they will be considered in calculations as well.
879 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
880 * views are ignored in this method.
882 * @param into An array to put the results into. If you don't provide any, LayoutManager will
884 * @return The adapter position of the last visible item in each span. If a span does not have
885 * any items, {@link RecyclerView#NO_POSITION} is returned for that span.
886 * @see #findLastCompletelyVisibleItemPositions(int[])
887 * @see #findFirstVisibleItemPositions(int[])
889 public int[] findLastVisibleItemPositions(int[] into
) {
891 into
= new int[mSpanCount
];
892 } else if (into
.length
< mSpanCount
) {
893 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
894 + " to span count. Expected:" + mSpanCount
+ ", array size:" + into
.length
);
896 for (int i
= 0; i
< mSpanCount
; i
++) {
897 into
[i
] = mSpans
[i
].findLastVisibleItemPosition();
903 * Returns the adapter position of the last completely visible view for each span.
905 * Note that, this value is not affected by layout orientation or item order traversal.
906 * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
909 * If RecyclerView has item decorators, they will be considered in calculations as well.
911 * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
912 * views are ignored in this method.
914 * @param into An array to put the results into. If you don't provide any, LayoutManager will
916 * @return The adapter position of the last fully visible item in each span. If a span does not
917 * have any items, {@link RecyclerView#NO_POSITION} is returned for that span.
918 * @see #findFirstCompletelyVisibleItemPositions(int[])
919 * @see #findLastVisibleItemPositions(int[])
921 public int[] findLastCompletelyVisibleItemPositions(int[] into
) {
923 into
= new int[mSpanCount
];
924 } else if (into
.length
< mSpanCount
) {
925 throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
926 + " to span count. Expected:" + mSpanCount
+ ", array size:" + into
.length
);
928 for (int i
= 0; i
< mSpanCount
; i
++) {
929 into
[i
] = mSpans
[i
].findLastCompletelyVisibleItemPosition();
935 public int computeHorizontalScrollOffset(RecyclerView
.State state
) {
936 return computeScrollOffset(state
);
939 private int computeScrollOffset(RecyclerView
.State state
) {
940 if (getChildCount() == 0) {
943 ensureOrientationHelper();
944 return ScrollbarHelper
.computeScrollOffset(state
, mPrimaryOrientation
,
945 findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled
, true)
946 , findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled
, true),
947 this, mSmoothScrollbarEnabled
, mShouldReverseLayout
);
951 public int computeVerticalScrollOffset(RecyclerView
.State state
) {
952 return computeScrollOffset(state
);
956 public int computeHorizontalScrollExtent(RecyclerView
.State state
) {
957 return computeScrollExtent(state
);
960 private int computeScrollExtent(RecyclerView
.State state
) {
961 if (getChildCount() == 0) {
964 ensureOrientationHelper();
965 return ScrollbarHelper
.computeScrollExtent(state
, mPrimaryOrientation
,
966 findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled
, true)
967 , findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled
, true),
968 this, mSmoothScrollbarEnabled
);
972 public int computeVerticalScrollExtent(RecyclerView
.State state
) {
973 return computeScrollExtent(state
);
977 public int computeHorizontalScrollRange(RecyclerView
.State state
) {
978 return computeScrollRange(state
);
981 private int computeScrollRange(RecyclerView
.State state
) {
982 if (getChildCount() == 0) {
985 ensureOrientationHelper();
986 return ScrollbarHelper
.computeScrollRange(state
, mPrimaryOrientation
,
987 findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled
, true)
988 , findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled
, true),
989 this, mSmoothScrollbarEnabled
);
993 public int computeVerticalScrollRange(RecyclerView
.State state
) {
994 return computeScrollRange(state
);
997 private void measureChildWithDecorationsAndMargin(View child
, LayoutParams lp
) {
999 if (mOrientation
== VERTICAL
) {
1000 measureChildWithDecorationsAndMargin(child
, mFullSizeSpec
,
1001 getSpecForDimension(lp
.height
, mHeightSpec
));
1003 measureChildWithDecorationsAndMargin(child
,
1004 getSpecForDimension(lp
.width
, mWidthSpec
), mFullSizeSpec
);
1007 if (mOrientation
== VERTICAL
) {
1008 measureChildWithDecorationsAndMargin(child
, mWidthSpec
,
1009 getSpecForDimension(lp
.height
, mHeightSpec
));
1011 measureChildWithDecorationsAndMargin(child
,
1012 getSpecForDimension(lp
.width
, mWidthSpec
), mHeightSpec
);
1017 private int getSpecForDimension(int dim
, int defaultSpec
) {
1021 return View
.MeasureSpec
.makeMeasureSpec(dim
, View
.MeasureSpec
.EXACTLY
);
1025 private void measureChildWithDecorationsAndMargin(View child
, int widthSpec
,
1027 calculateItemDecorationsForChild(child
, mTmpRect
);
1028 LayoutParams lp
= (LayoutParams
) child
.getLayoutParams();
1029 widthSpec
= updateSpecWithExtra(widthSpec
, lp
.leftMargin
+ mTmpRect
.left
,
1030 lp
.rightMargin
+ mTmpRect
.right
);
1031 heightSpec
= updateSpecWithExtra(heightSpec
, lp
.topMargin
+ mTmpRect
.top
,
1032 lp
.bottomMargin
+ mTmpRect
.bottom
);
1033 child
.measure(widthSpec
, heightSpec
);
1036 private int updateSpecWithExtra(int spec
, int startInset
, int endInset
) {
1037 if (startInset
== 0 && endInset
== 0) {
1040 final int mode
= View
.MeasureSpec
.getMode(spec
);
1041 if (mode
== View
.MeasureSpec
.AT_MOST
|| mode
== View
.MeasureSpec
.EXACTLY
) {
1042 return View
.MeasureSpec
.makeMeasureSpec(
1043 View
.MeasureSpec
.getSize(spec
) - startInset
- endInset
, mode
);
1049 public void onRestoreInstanceState(Parcelable state
) {
1050 if (state
instanceof SavedState
) {
1051 mPendingSavedState
= (SavedState
) state
;
1054 Log
.d(TAG
, "invalid saved state class");
1059 public Parcelable
onSaveInstanceState() {
1060 if (mPendingSavedState
!= null) {
1061 return new SavedState(mPendingSavedState
);
1063 SavedState state
= new SavedState();
1064 state
.mReverseLayout
= mReverseLayout
;
1065 state
.mAnchorLayoutFromEnd
= mLastLayoutFromEnd
;
1066 state
.mLastLayoutRTL
= mLastLayoutRTL
;
1068 if (mLazySpanLookup
!= null && mLazySpanLookup
.mData
!= null) {
1069 state
.mSpanLookup
= mLazySpanLookup
.mData
;
1070 state
.mSpanLookupSize
= state
.mSpanLookup
.length
;
1071 state
.mFullSpanItems
= mLazySpanLookup
.mFullSpanItems
;
1073 state
.mSpanLookupSize
= 0;
1076 if (getChildCount() > 0) {
1077 ensureOrientationHelper();
1078 state
.mAnchorPosition
= mLastLayoutFromEnd ?
getLastChildPosition()
1079 : getFirstChildPosition();
1080 state
.mVisibleAnchorPosition
= findFirstVisibleItemPositionInt();
1081 state
.mSpanOffsetsSize
= mSpanCount
;
1082 state
.mSpanOffsets
= new int[mSpanCount
];
1083 for (int i
= 0; i
< mSpanCount
; i
++) {
1085 if (mLastLayoutFromEnd
) {
1086 line
= mSpans
[i
].getEndLine(Span
.INVALID_LINE
);
1087 if (line
!= Span
.INVALID_LINE
) {
1088 line
-= mPrimaryOrientation
.getEndAfterPadding();
1091 line
= mSpans
[i
].getStartLine(Span
.INVALID_LINE
);
1092 if (line
!= Span
.INVALID_LINE
) {
1093 line
-= mPrimaryOrientation
.getStartAfterPadding();
1096 state
.mSpanOffsets
[i
] = line
;
1099 state
.mAnchorPosition
= NO_POSITION
;
1100 state
.mVisibleAnchorPosition
= NO_POSITION
;
1101 state
.mSpanOffsetsSize
= 0;
1104 Log
.d(TAG
, "saved state:\n" + state
);
1110 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView
.Recycler recycler
,
1111 RecyclerView
.State state
, View host
, AccessibilityNodeInfoCompat info
) {
1112 ViewGroup
.LayoutParams lp
= host
.getLayoutParams();
1113 if (!(lp
instanceof LayoutParams
)) {
1114 super.onInitializeAccessibilityNodeInfoForItem(host
, info
);
1117 LayoutParams sglp
= (LayoutParams
) lp
;
1118 if (mOrientation
== HORIZONTAL
) {
1119 info
.setCollectionItemInfo(AccessibilityNodeInfoCompat
.CollectionItemInfoCompat
.obtain(
1120 sglp
.getSpanIndex(), sglp
.mFullSpan ? mSpanCount
: 1,
1122 sglp
.mFullSpan
, false));
1123 } else { // VERTICAL
1124 info
.setCollectionItemInfo(AccessibilityNodeInfoCompat
.CollectionItemInfoCompat
.obtain(
1126 sglp
.getSpanIndex(), sglp
.mFullSpan ? mSpanCount
: 1,
1127 sglp
.mFullSpan
, false));
1132 public void onInitializeAccessibilityEvent(AccessibilityEvent event
) {
1133 super.onInitializeAccessibilityEvent(event
);
1134 if (getChildCount() > 0) {
1135 final AccessibilityRecordCompat record
= AccessibilityEventCompat
1137 final View start
= findFirstVisibleItemClosestToStart(false, true);
1138 final View end
= findFirstVisibleItemClosestToEnd(false, true);
1139 if (start
== null || end
== null) {
1142 final int startPos
= getPosition(start
);
1143 final int endPos
= getPosition(end
);
1144 if (startPos
< endPos
) {
1145 record
.setFromIndex(startPos
);
1146 record
.setToIndex(endPos
);
1148 record
.setFromIndex(endPos
);
1149 record
.setToIndex(startPos
);
1155 * Finds the first fully visible child to be used as an anchor child if span count changes when
1156 * state is restored. If no children is fully visible, returns a partially visible child instead
1157 * of returning null.
1159 int findFirstVisibleItemPositionInt() {
1160 final View first
= mShouldReverseLayout ?
findFirstVisibleItemClosestToEnd(true, true) :
1161 findFirstVisibleItemClosestToStart(true, true);
1162 return first
== null ? NO_POSITION
: getPosition(first
);
1166 public int getRowCountForAccessibility(RecyclerView
.Recycler recycler
,
1167 RecyclerView
.State state
) {
1168 if (mOrientation
== HORIZONTAL
) {
1171 return super.getRowCountForAccessibility(recycler
, state
);
1175 public int getColumnCountForAccessibility(RecyclerView
.Recycler recycler
,
1176 RecyclerView
.State state
) {
1177 if (mOrientation
== VERTICAL
) {
1180 return super.getColumnCountForAccessibility(recycler
, state
);
1184 * This is for internal use. Not necessarily the child closest to start but the first child
1185 * we find that matches the criteria.
1186 * This method does not do any sorting based on child's start coordinate, instead, it uses
1189 View
findFirstVisibleItemClosestToStart(boolean fullyVisible
, boolean acceptPartiallyVisible
) {
1190 ensureOrientationHelper();
1191 final int boundsStart
= mPrimaryOrientation
.getStartAfterPadding();
1192 final int boundsEnd
= mPrimaryOrientation
.getEndAfterPadding();
1193 final int limit
= getChildCount();
1194 View partiallyVisible
= null;
1195 for (int i
= 0; i
< limit
; i
++) {
1196 final View child
= getChildAt(i
);
1197 final int childStart
= mPrimaryOrientation
.getDecoratedStart(child
);
1198 final int childEnd
= mPrimaryOrientation
.getDecoratedEnd(child
);
1199 if(childEnd
<= boundsStart
|| childStart
>= boundsEnd
) {
1200 continue; // not visible at all
1202 if (childStart
>= boundsStart
|| !fullyVisible
) {
1203 // when checking for start, it is enough even if part of the child's top is visible
1204 // as long as fully visible is not requested.
1207 if (acceptPartiallyVisible
&& partiallyVisible
== null) {
1208 partiallyVisible
= child
;
1211 return partiallyVisible
;
1215 * This is for internal use. Not necessarily the child closest to bottom but the first child
1216 * we find that matches the criteria.
1217 * This method does not do any sorting based on child's end coordinate, instead, it uses
1220 View
findFirstVisibleItemClosestToEnd(boolean fullyVisible
, boolean acceptPartiallyVisible
) {
1221 ensureOrientationHelper();
1222 final int boundsStart
= mPrimaryOrientation
.getStartAfterPadding();
1223 final int boundsEnd
= mPrimaryOrientation
.getEndAfterPadding();
1224 View partiallyVisible
= null;
1225 for (int i
= getChildCount() - 1; i
>= 0; i
--) {
1226 final View child
= getChildAt(i
);
1227 final int childStart
= mPrimaryOrientation
.getDecoratedStart(child
);
1228 final int childEnd
= mPrimaryOrientation
.getDecoratedEnd(child
);
1229 if(childEnd
<= boundsStart
|| childStart
>= boundsEnd
) {
1230 continue; // not visible at all
1232 if (childEnd
<= boundsEnd
|| !fullyVisible
) {
1233 // when checking for end, it is enough even if part of the child's bottom is visible
1234 // as long as fully visible is not requested.
1237 if (acceptPartiallyVisible
&& partiallyVisible
== null) {
1238 partiallyVisible
= child
;
1241 return partiallyVisible
;
1244 private void fixEndGap(RecyclerView
.Recycler recycler
, RecyclerView
.State state
,
1245 boolean canOffsetChildren
) {
1246 final int maxEndLine
= getMaxEnd(mPrimaryOrientation
.getEndAfterPadding());
1247 int gap
= mPrimaryOrientation
.getEndAfterPadding() - maxEndLine
;
1250 fixOffset
= -scrollBy(-gap
, recycler
, state
);
1252 return; // nothing to fix
1255 if (canOffsetChildren
&& gap
> 0) {
1256 mPrimaryOrientation
.offsetChildren(gap
);
1260 private void fixStartGap(RecyclerView
.Recycler recycler
, RecyclerView
.State state
,
1261 boolean canOffsetChildren
) {
1262 final int minStartLine
= getMinStart(mPrimaryOrientation
.getStartAfterPadding());
1263 int gap
= minStartLine
- mPrimaryOrientation
.getStartAfterPadding();
1266 fixOffset
= scrollBy(gap
, recycler
, state
);
1268 return; // nothing to fix
1271 if (canOffsetChildren
&& gap
> 0) {
1272 mPrimaryOrientation
.offsetChildren(-gap
);
1276 private void updateLayoutState(int anchorPosition
, RecyclerView
.State state
) {
1277 mLayoutState
.mAvailable
= 0;
1278 mLayoutState
.mCurrentPosition
= anchorPosition
;
1281 if (isSmoothScrolling()) {
1282 final int targetPos
= state
.getTargetScrollPosition();
1283 if (targetPos
!= NO_POSITION
) {
1284 if (mShouldReverseLayout
== targetPos
< anchorPosition
) {
1285 endExtra
= mPrimaryOrientation
.getTotalSpace();
1287 startExtra
= mPrimaryOrientation
.getTotalSpace();
1292 // Line of the furthest row.
1293 final boolean clipToPadding
= getClipToPadding();
1294 if (clipToPadding
) {
1295 mLayoutState
.mStartLine
= mPrimaryOrientation
.getStartAfterPadding() - startExtra
;
1296 mLayoutState
.mEndLine
= mPrimaryOrientation
.getEndAfterPadding() + endExtra
;
1298 mLayoutState
.mEndLine
= mPrimaryOrientation
.getEnd() + endExtra
;
1299 mLayoutState
.mStartLine
= -startExtra
;
1303 private void setLayoutStateDirection(int direction
) {
1304 mLayoutState
.mLayoutDirection
= direction
;
1305 mLayoutState
.mItemDirection
= (mShouldReverseLayout
== (direction
== LAYOUT_START
)) ?
1306 ITEM_DIRECTION_TAIL
: ITEM_DIRECTION_HEAD
;
1310 public void offsetChildrenHorizontal(int dx
) {
1311 super.offsetChildrenHorizontal(dx
);
1312 for (int i
= 0; i
< mSpanCount
; i
++) {
1313 mSpans
[i
].onOffset(dx
);
1318 public void offsetChildrenVertical(int dy
) {
1319 super.offsetChildrenVertical(dy
);
1320 for (int i
= 0; i
< mSpanCount
; i
++) {
1321 mSpans
[i
].onOffset(dy
);
1326 public void onItemsRemoved(RecyclerView recyclerView
, int positionStart
, int itemCount
) {
1327 handleUpdate(positionStart
, itemCount
, AdapterHelper
.UpdateOp
.REMOVE
);
1331 public void onItemsAdded(RecyclerView recyclerView
, int positionStart
, int itemCount
) {
1332 handleUpdate(positionStart
, itemCount
, AdapterHelper
.UpdateOp
.ADD
);
1336 public void onItemsChanged(RecyclerView recyclerView
) {
1337 mLazySpanLookup
.clear();
1342 public void onItemsMoved(RecyclerView recyclerView
, int from
, int to
, int itemCount
) {
1343 handleUpdate(from
, to
, AdapterHelper
.UpdateOp
.MOVE
);
1347 public void onItemsUpdated(RecyclerView recyclerView
, int positionStart
, int itemCount
,
1349 handleUpdate(positionStart
, itemCount
, AdapterHelper
.UpdateOp
.UPDATE
);
1353 * Checks whether it should invalidate span assignments in response to an adapter change.
1355 private void handleUpdate(int positionStart
, int itemCountOrToPosition
, int cmd
) {
1356 int minPosition
= mShouldReverseLayout ?
getLastChildPosition() : getFirstChildPosition();
1357 final int affectedRangeEnd
;// exclusive
1358 final int affectedRangeStart
;// inclusive
1360 if (cmd
== AdapterHelper
.UpdateOp
.MOVE
) {
1361 if (positionStart
< itemCountOrToPosition
) {
1362 affectedRangeEnd
= itemCountOrToPosition
+ 1;
1363 affectedRangeStart
= positionStart
;
1365 affectedRangeEnd
= positionStart
+ 1;
1366 affectedRangeStart
= itemCountOrToPosition
;
1369 affectedRangeStart
= positionStart
;
1370 affectedRangeEnd
= positionStart
+ itemCountOrToPosition
;
1373 mLazySpanLookup
.invalidateAfter(affectedRangeStart
);
1375 case AdapterHelper
.UpdateOp
.ADD
:
1376 mLazySpanLookup
.offsetForAddition(positionStart
, itemCountOrToPosition
);
1378 case AdapterHelper
.UpdateOp
.REMOVE
:
1379 mLazySpanLookup
.offsetForRemoval(positionStart
, itemCountOrToPosition
);
1381 case AdapterHelper
.UpdateOp
.MOVE
:
1383 mLazySpanLookup
.offsetForRemoval(positionStart
, 1);
1384 mLazySpanLookup
.offsetForAddition(itemCountOrToPosition
, 1);
1388 if (affectedRangeEnd
<= minPosition
) {
1392 int maxPosition
= mShouldReverseLayout ?
getFirstChildPosition() : getLastChildPosition();
1393 if (affectedRangeStart
<= maxPosition
) {
1398 private int fill(RecyclerView
.Recycler recycler
, LayoutState layoutState
,
1399 RecyclerView
.State state
) {
1400 mRemainingSpans
.set(0, mSpanCount
, true);
1401 // The target position we are trying to reach.
1402 final int targetLine
;
1404 // Line of the furthest row.
1405 if (layoutState
.mLayoutDirection
== LAYOUT_END
) {
1406 targetLine
= layoutState
.mEndLine
+ layoutState
.mAvailable
;
1407 } else { // LAYOUT_START
1408 targetLine
= layoutState
.mStartLine
- layoutState
.mAvailable
;
1411 updateAllRemainingSpans(layoutState
.mLayoutDirection
, targetLine
);
1413 Log
.d(TAG
, "FILLING targetLine: " + targetLine
+ "," +
1414 "remaining spans:" + mRemainingSpans
+ ", state: " + layoutState
);
1417 // the default coordinate to add new view.
1418 final int defaultNewViewLine
= mShouldReverseLayout
1419 ? mPrimaryOrientation
.getEndAfterPadding()
1420 : mPrimaryOrientation
.getStartAfterPadding();
1421 boolean added
= false;
1422 while (layoutState
.hasMore(state
) && !mRemainingSpans
.isEmpty()) {
1423 View view
= layoutState
.next(recycler
);
1424 LayoutParams lp
= ((LayoutParams
) view
.getLayoutParams());
1425 final int position
= lp
.getViewLayoutPosition();
1426 final int spanIndex
= mLazySpanLookup
.getSpan(position
);
1428 final boolean assignSpan
= spanIndex
== LayoutParams
.INVALID_SPAN_ID
;
1430 currentSpan
= lp
.mFullSpan ? mSpans
[0] : getNextSpan(layoutState
);
1431 mLazySpanLookup
.setSpan(position
, currentSpan
);
1433 Log
.d(TAG
, "assigned " + currentSpan
.mIndex
+ " for " + position
);
1437 Log
.d(TAG
, "using " + spanIndex
+ " for pos " + position
);
1439 currentSpan
= mSpans
[spanIndex
];
1441 // assign span before measuring so that item decorators can get updated span index
1442 lp
.mSpan
= currentSpan
;
1443 if (layoutState
.mLayoutDirection
== LAYOUT_END
) {
1448 measureChildWithDecorationsAndMargin(view
, lp
);
1452 if (layoutState
.mLayoutDirection
== LAYOUT_END
) {
1453 start
= lp
.mFullSpan ?
getMaxEnd(defaultNewViewLine
)
1454 : currentSpan
.getEndLine(defaultNewViewLine
);
1455 end
= start
+ mPrimaryOrientation
.getDecoratedMeasurement(view
);
1456 if (assignSpan
&& lp
.mFullSpan
) {
1457 LazySpanLookup
.FullSpanItem fullSpanItem
;
1458 fullSpanItem
= createFullSpanItemFromEnd(start
);
1459 fullSpanItem
.mGapDir
= LAYOUT_START
;
1460 fullSpanItem
.mPosition
= position
;
1461 mLazySpanLookup
.addFullSpanItem(fullSpanItem
);
1464 end
= lp
.mFullSpan ?
getMinStart(defaultNewViewLine
)
1465 : currentSpan
.getStartLine(defaultNewViewLine
);
1466 start
= end
- mPrimaryOrientation
.getDecoratedMeasurement(view
);
1467 if (assignSpan
&& lp
.mFullSpan
) {
1468 LazySpanLookup
.FullSpanItem fullSpanItem
;
1469 fullSpanItem
= createFullSpanItemFromStart(end
);
1470 fullSpanItem
.mGapDir
= LAYOUT_END
;
1471 fullSpanItem
.mPosition
= position
;
1472 mLazySpanLookup
.addFullSpanItem(fullSpanItem
);
1476 // check if this item may create gaps in the future
1477 if (lp
.mFullSpan
&& layoutState
.mItemDirection
== ITEM_DIRECTION_HEAD
) {
1479 mLaidOutInvalidFullSpan
= true;
1481 final boolean hasInvalidGap
;
1482 if (layoutState
.mLayoutDirection
== LAYOUT_END
) {
1483 hasInvalidGap
= !areAllEndsEqual();
1484 } else { // layoutState.mLayoutDirection == LAYOUT_START
1485 hasInvalidGap
= !areAllStartsEqual();
1487 if (hasInvalidGap
) {
1488 final LazySpanLookup
.FullSpanItem fullSpanItem
= mLazySpanLookup
1489 .getFullSpanItem(position
);
1490 if (fullSpanItem
!= null) {
1491 fullSpanItem
.mHasUnwantedGapAfter
= true;
1493 mLaidOutInvalidFullSpan
= true;
1498 attachViewToSpans(view
, lp
, layoutState
);
1499 final int otherStart
= lp
.mFullSpan ? mSecondaryOrientation
.getStartAfterPadding()
1500 : currentSpan
.mIndex
* mSizePerSpan
+
1501 mSecondaryOrientation
.getStartAfterPadding();
1502 final int otherEnd
= otherStart
+ mSecondaryOrientation
.getDecoratedMeasurement(view
);
1503 if (mOrientation
== VERTICAL
) {
1504 layoutDecoratedWithMargins(view
, otherStart
, start
, otherEnd
, end
);
1506 layoutDecoratedWithMargins(view
, start
, otherStart
, end
, otherEnd
);
1510 updateAllRemainingSpans(mLayoutState
.mLayoutDirection
, targetLine
);
1512 updateRemainingSpans(currentSpan
, mLayoutState
.mLayoutDirection
, targetLine
);
1514 recycle(recycler
, mLayoutState
);
1518 recycle(recycler
, mLayoutState
);
1521 if (mLayoutState
.mLayoutDirection
== LAYOUT_START
) {
1522 final int minStart
= getMinStart(mPrimaryOrientation
.getStartAfterPadding());
1523 diff
= mPrimaryOrientation
.getStartAfterPadding() - minStart
;
1525 final int maxEnd
= getMaxEnd(mPrimaryOrientation
.getEndAfterPadding());
1526 diff
= maxEnd
- mPrimaryOrientation
.getEndAfterPadding();
1528 return diff
> 0 ? Math
.min(layoutState
.mAvailable
, diff
) : 0;
1531 private LazySpanLookup
.FullSpanItem
createFullSpanItemFromEnd(int newItemTop
) {
1532 LazySpanLookup
.FullSpanItem fsi
= new LazySpanLookup
.FullSpanItem();
1533 fsi
.mGapPerSpan
= new int[mSpanCount
];
1534 for (int i
= 0; i
< mSpanCount
; i
++) {
1535 fsi
.mGapPerSpan
[i
] = newItemTop
- mSpans
[i
].getEndLine(newItemTop
);
1540 private LazySpanLookup
.FullSpanItem
createFullSpanItemFromStart(int newItemBottom
) {
1541 LazySpanLookup
.FullSpanItem fsi
= new LazySpanLookup
.FullSpanItem();
1542 fsi
.mGapPerSpan
= new int[mSpanCount
];
1543 for (int i
= 0; i
< mSpanCount
; i
++) {
1544 fsi
.mGapPerSpan
[i
] = mSpans
[i
].getStartLine(newItemBottom
) - newItemBottom
;
1549 private void attachViewToSpans(View view
, LayoutParams lp
, LayoutState layoutState
) {
1550 if (layoutState
.mLayoutDirection
== LayoutState
.LAYOUT_END
) {
1552 appendViewToAllSpans(view
);
1554 lp
.mSpan
.appendToSpan(view
);
1558 prependViewToAllSpans(view
);
1560 lp
.mSpan
.prependToSpan(view
);
1565 private void recycle(RecyclerView
.Recycler recycler
, LayoutState layoutState
) {
1566 if (layoutState
.mAvailable
== 0) {
1567 // easy, recycle line is still valid
1568 if (layoutState
.mLayoutDirection
== LAYOUT_START
) {
1569 recycleFromEnd(recycler
, layoutState
.mEndLine
);
1571 recycleFromStart(recycler
, layoutState
.mStartLine
);
1574 // scrolling case, recycle line can be shifted by how much space we could cover
1575 // by adding new views
1576 if (layoutState
.mLayoutDirection
== LAYOUT_START
) {
1577 // calculate recycle line
1578 int scrolled
= layoutState
.mStartLine
- getMaxStart(layoutState
.mStartLine
);
1581 line
= layoutState
.mEndLine
;
1583 line
= layoutState
.mEndLine
- Math
.min(scrolled
, layoutState
.mAvailable
);
1585 recycleFromEnd(recycler
, line
);
1587 // calculate recycle line
1588 int scrolled
= getMinEnd(layoutState
.mEndLine
) - layoutState
.mEndLine
;
1591 line
= layoutState
.mStartLine
;
1593 line
= layoutState
.mStartLine
+ Math
.min(scrolled
, layoutState
.mAvailable
);
1595 recycleFromStart(recycler
, line
);
1601 private void appendViewToAllSpans(View view
) {
1602 // traverse in reverse so that we end up assigning full span items to 0
1603 for (int i
= mSpanCount
- 1; i
>= 0; i
--) {
1604 mSpans
[i
].appendToSpan(view
);
1608 private void prependViewToAllSpans(View view
) {
1609 // traverse in reverse so that we end up assigning full span items to 0
1610 for (int i
= mSpanCount
- 1; i
>= 0; i
--) {
1611 mSpans
[i
].prependToSpan(view
);
1615 private void layoutDecoratedWithMargins(View child
, int left
, int top
, int right
, int bottom
) {
1616 LayoutParams lp
= (LayoutParams
) child
.getLayoutParams();
1618 Log
.d(TAG
, "layout decorated pos: " + lp
.getViewLayoutPosition() + ", span:"
1619 + lp
.getSpanIndex() + ", fullspan:" + lp
.mFullSpan
1620 + ". l:" + left
+ ",t:" + top
1621 + ", r:" + right
+ ", b:" + bottom
);
1623 layoutDecorated(child
, left
+ lp
.leftMargin
, top
+ lp
.topMargin
, right
- lp
.rightMargin
1624 , bottom
- lp
.bottomMargin
);
1627 private void updateAllRemainingSpans(int layoutDir
, int targetLine
) {
1628 for (int i
= 0; i
< mSpanCount
; i
++) {
1629 if (mSpans
[i
].mViews
.isEmpty()) {
1632 updateRemainingSpans(mSpans
[i
], layoutDir
, targetLine
);
1636 private void updateRemainingSpans(Span span
, int layoutDir
, int targetLine
) {
1637 final int deletedSize
= span
.getDeletedSize();
1638 if (layoutDir
== LAYOUT_START
) {
1639 final int line
= span
.getStartLine();
1640 if (line
+ deletedSize
<= targetLine
) {
1641 mRemainingSpans
.set(span
.mIndex
, false);
1644 final int line
= span
.getEndLine();
1645 if (line
- deletedSize
>= targetLine
) {
1646 mRemainingSpans
.set(span
.mIndex
, false);
1651 private int getMaxStart(int def
) {
1652 int maxStart
= mSpans
[0].getStartLine(def
);
1653 for (int i
= 1; i
< mSpanCount
; i
++) {
1654 final int spanStart
= mSpans
[i
].getStartLine(def
);
1655 if (spanStart
> maxStart
) {
1656 maxStart
= spanStart
;
1662 private int getMinStart(int def
) {
1663 int minStart
= mSpans
[0].getStartLine(def
);
1664 for (int i
= 1; i
< mSpanCount
; i
++) {
1665 final int spanStart
= mSpans
[i
].getStartLine(def
);
1666 if (spanStart
< minStart
) {
1667 minStart
= spanStart
;
1673 boolean areAllEndsEqual() {
1674 int end
= mSpans
[0].getEndLine(Span
.INVALID_LINE
);
1675 for (int i
= 1; i
< mSpanCount
; i
++) {
1676 if (mSpans
[i
].getEndLine(Span
.INVALID_LINE
) != end
) {
1683 boolean areAllStartsEqual() {
1684 int start
= mSpans
[0].getStartLine(Span
.INVALID_LINE
);
1685 for (int i
= 1; i
< mSpanCount
; i
++) {
1686 if (mSpans
[i
].getStartLine(Span
.INVALID_LINE
) != start
) {
1693 private int getMaxEnd(int def
) {
1694 int maxEnd
= mSpans
[0].getEndLine(def
);
1695 for (int i
= 1; i
< mSpanCount
; i
++) {
1696 final int spanEnd
= mSpans
[i
].getEndLine(def
);
1697 if (spanEnd
> maxEnd
) {
1704 private int getMinEnd(int def
) {
1705 int minEnd
= mSpans
[0].getEndLine(def
);
1706 for (int i
= 1; i
< mSpanCount
; i
++) {
1707 final int spanEnd
= mSpans
[i
].getEndLine(def
);
1708 if (spanEnd
< minEnd
) {
1715 private void recycleFromStart(RecyclerView
.Recycler recycler
, int line
) {
1716 while (getChildCount() > 0) {
1717 View child
= getChildAt(0);
1718 if (mPrimaryOrientation
.getDecoratedEnd(child
) <= line
) {
1719 LayoutParams lp
= (LayoutParams
) child
.getLayoutParams();
1720 // Don't recycle the last View in a span not to lose span's start/end lines
1722 for (int j
= 0; j
< mSpanCount
; j
++) {
1723 if (mSpans
[j
].mViews
.size() == 1) {
1727 for (int j
= 0; j
< mSpanCount
; j
++) {
1728 mSpans
[j
].popStart();
1731 if (lp
.mSpan
.mViews
.size() == 1) {
1734 lp
.mSpan
.popStart();
1736 removeAndRecycleView(child
, recycler
);
1743 private void recycleFromEnd(RecyclerView
.Recycler recycler
, int line
) {
1744 final int childCount
= getChildCount();
1746 for (i
= childCount
- 1; i
>= 0; i
--) {
1747 View child
= getChildAt(i
);
1748 if (mPrimaryOrientation
.getDecoratedStart(child
) >= line
) {
1749 LayoutParams lp
= (LayoutParams
) child
.getLayoutParams();
1750 // Don't recycle the last View in a span not to lose span's start/end lines
1752 for (int j
= 0; j
< mSpanCount
; j
++) {
1753 if (mSpans
[j
].mViews
.size() == 1) {
1757 for (int j
= 0; j
< mSpanCount
; j
++) {
1761 if (lp
.mSpan
.mViews
.size() == 1) {
1766 removeAndRecycleView(child
, recycler
);
1774 * @return True if last span is the first one we want to fill
1776 private boolean preferLastSpan(int layoutDir
) {
1777 if (mOrientation
== HORIZONTAL
) {
1778 return (layoutDir
== LAYOUT_START
) != mShouldReverseLayout
;
1780 return ((layoutDir
== LAYOUT_START
) == mShouldReverseLayout
) == isLayoutRTL();
1784 * Finds the span for the next view.
1786 private Span
getNextSpan(LayoutState layoutState
) {
1787 final boolean preferLastSpan
= preferLastSpan(layoutState
.mLayoutDirection
);
1788 final int startIndex
, endIndex
, diff
;
1789 if (preferLastSpan
) {
1790 startIndex
= mSpanCount
- 1;
1795 endIndex
= mSpanCount
;
1798 if (layoutState
.mLayoutDirection
== LAYOUT_END
) {
1800 int minLine
= Integer
.MAX_VALUE
;
1801 final int defaultLine
= mPrimaryOrientation
.getStartAfterPadding();
1802 for (int i
= startIndex
; i
!= endIndex
; i
+= diff
) {
1803 final Span other
= mSpans
[i
];
1804 int otherLine
= other
.getEndLine(defaultLine
);
1805 if (otherLine
< minLine
) {
1807 minLine
= otherLine
;
1813 int maxLine
= Integer
.MIN_VALUE
;
1814 final int defaultLine
= mPrimaryOrientation
.getEndAfterPadding();
1815 for (int i
= startIndex
; i
!= endIndex
; i
+= diff
) {
1816 final Span other
= mSpans
[i
];
1817 int otherLine
= other
.getStartLine(defaultLine
);
1818 if (otherLine
> maxLine
) {
1820 maxLine
= otherLine
;
1828 public boolean canScrollVertically() {
1829 return mOrientation
== VERTICAL
;
1833 public boolean canScrollHorizontally() {
1834 return mOrientation
== HORIZONTAL
;
1838 public int scrollHorizontallyBy(int dx
, RecyclerView
.Recycler recycler
,
1839 RecyclerView
.State state
) {
1840 return scrollBy(dx
, recycler
, state
);
1844 public int scrollVerticallyBy(int dy
, RecyclerView
.Recycler recycler
,
1845 RecyclerView
.State state
) {
1846 return scrollBy(dy
, recycler
, state
);
1849 private int calculateScrollDirectionForPosition(int position
) {
1850 if (getChildCount() == 0) {
1851 return mShouldReverseLayout ? LAYOUT_END
: LAYOUT_START
;
1853 final int firstChildPos
= getFirstChildPosition();
1854 return position
< firstChildPos
!= mShouldReverseLayout ? LAYOUT_START
: LAYOUT_END
;
1858 public void smoothScrollToPosition(RecyclerView recyclerView
, RecyclerView
.State state
,
1860 LinearSmoothScroller scroller
= new LinearSmoothScroller(recyclerView
.getContext()) {
1862 public PointF
computeScrollVectorForPosition(int targetPosition
) {
1863 final int direction
= calculateScrollDirectionForPosition(targetPosition
);
1864 if (direction
== 0) {
1867 if (mOrientation
== HORIZONTAL
) {
1868 return new PointF(direction
, 0);
1870 return new PointF(0, direction
);
1874 scroller
.setTargetPosition(position
);
1875 startSmoothScroll(scroller
);
1879 public void scrollToPosition(int position
) {
1880 if (mPendingSavedState
!= null && mPendingSavedState
.mAnchorPosition
!= position
) {
1881 mPendingSavedState
.invalidateAnchorPositionInfo();
1883 mPendingScrollPosition
= position
;
1884 mPendingScrollPositionOffset
= INVALID_OFFSET
;
1889 * Scroll to the specified adapter position with the given offset from layout start.
1891 * Note that scroll position change will not be reflected until the next layout call.
1893 * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
1895 * @param position Index (starting at 0) of the reference item.
1896 * @param offset The distance (in pixels) between the start edge of the item view and
1897 * start edge of the RecyclerView.
1898 * @see #setReverseLayout(boolean)
1899 * @see #scrollToPosition(int)
1901 public void scrollToPositionWithOffset(int position
, int offset
) {
1902 if (mPendingSavedState
!= null) {
1903 mPendingSavedState
.invalidateAnchorPositionInfo();
1905 mPendingScrollPosition
= position
;
1906 mPendingScrollPositionOffset
= offset
;
1910 int scrollBy(int dt
, RecyclerView
.Recycler recycler
, RecyclerView
.State state
) {
1911 ensureOrientationHelper();
1912 final int referenceChildPosition
;
1913 final int layoutDir
;
1914 if (dt
> 0) { // layout towards end
1915 layoutDir
= LAYOUT_END
;
1916 referenceChildPosition
= getLastChildPosition();
1918 layoutDir
= LAYOUT_START
;
1919 referenceChildPosition
= getFirstChildPosition();
1921 updateLayoutState(referenceChildPosition
, state
);
1922 setLayoutStateDirection(layoutDir
);
1923 mLayoutState
.mCurrentPosition
= referenceChildPosition
+ mLayoutState
.mItemDirection
;
1924 final int absDt
= Math
.abs(dt
);
1925 mLayoutState
.mAvailable
= absDt
;
1926 int consumed
= fill(recycler
, mLayoutState
, state
);
1927 final int totalScroll
;
1928 if (absDt
< consumed
) {
1930 } else if (dt
< 0) {
1931 totalScroll
= -consumed
;
1933 totalScroll
= consumed
;
1936 Log
.d(TAG
, "asked " + dt
+ " scrolled" + totalScroll
);
1939 mPrimaryOrientation
.offsetChildren(-totalScroll
);
1940 // always reset this if we scroll for a proper save instance state
1941 mLastLayoutFromEnd
= mShouldReverseLayout
;
1945 private int getLastChildPosition() {
1946 final int childCount
= getChildCount();
1947 return childCount
== 0 ?
0 : getPosition(getChildAt(childCount
- 1));
1950 private int getFirstChildPosition() {
1951 final int childCount
= getChildCount();
1952 return childCount
== 0 ?
0 : getPosition(getChildAt(0));
1956 * Finds the first View that can be used as an anchor View.
1958 * @return Position of the View or 0 if it cannot find any such View.
1960 private int findFirstReferenceChildPosition(int itemCount
) {
1961 final int limit
= getChildCount();
1962 for (int i
= 0; i
< limit
; i
++) {
1963 final View view
= getChildAt(i
);
1964 final int position
= getPosition(view
);
1965 if (position
>= 0 && position
< itemCount
) {
1973 * Finds the last View that can be used as an anchor View.
1975 * @return Position of the View or 0 if it cannot find any such View.
1977 private int findLastReferenceChildPosition(int itemCount
) {
1978 for (int i
= getChildCount() - 1; i
>= 0; i
--) {
1979 final View view
= getChildAt(i
);
1980 final int position
= getPosition(view
);
1981 if (position
>= 0 && position
< itemCount
) {
1989 public RecyclerView
.LayoutParams
generateDefaultLayoutParams() {
1990 return new LayoutParams(ViewGroup
.LayoutParams
.WRAP_CONTENT
,
1991 ViewGroup
.LayoutParams
.WRAP_CONTENT
);
1995 public RecyclerView
.LayoutParams
generateLayoutParams(Context c
, AttributeSet attrs
) {
1996 return new LayoutParams(c
, attrs
);
2000 public RecyclerView
.LayoutParams
generateLayoutParams(ViewGroup
.LayoutParams lp
) {
2001 if (lp
instanceof ViewGroup
.MarginLayoutParams
) {
2002 return new LayoutParams((ViewGroup
.MarginLayoutParams
) lp
);
2004 return new LayoutParams(lp
);
2009 public boolean checkLayoutParams(RecyclerView
.LayoutParams lp
) {
2010 return lp
instanceof LayoutParams
;
2013 public int getOrientation() {
2014 return mOrientation
;
2019 * LayoutParams used by StaggeredGridLayoutManager.
2021 * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the
2022 * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is
2023 * expected to fill all of the space given to it.
2025 public static class LayoutParams
extends RecyclerView
.LayoutParams
{
2028 * Span Id for Views that are not laid out yet.
2030 public static final int INVALID_SPAN_ID
= -1;
2032 // Package scope to be able to access from tests.
2037 public LayoutParams(Context c
, AttributeSet attrs
) {
2041 public LayoutParams(int width
, int height
) {
2042 super(width
, height
);
2045 public LayoutParams(ViewGroup
.MarginLayoutParams source
) {
2049 public LayoutParams(ViewGroup
.LayoutParams source
) {
2053 public LayoutParams(RecyclerView
.LayoutParams source
) {
2058 * When set to true, the item will layout using all span area. That means, if orientation
2059 * is vertical, the view will have full width; if orientation is horizontal, the view will
2062 * @param fullSpan True if this item should traverse all spans.
2063 * @see #isFullSpan()
2065 public void setFullSpan(boolean fullSpan
) {
2066 mFullSpan
= fullSpan
;
2070 * Returns whether this View occupies all available spans or just one.
2072 * @return True if the View occupies all spans or false otherwise.
2073 * @see #setFullSpan(boolean)
2075 public boolean isFullSpan() {
2080 * Returns the Span index to which this View is assigned.
2082 * @return The Span index of the View. If View is not yet assigned to any span, returns
2083 * {@link #INVALID_SPAN_ID}.
2085 public final int getSpanIndex() {
2086 if (mSpan
== null) {
2087 return INVALID_SPAN_ID
;
2089 return mSpan
.mIndex
;
2093 // Package scoped to access from tests.
2096 static final int INVALID_LINE
= Integer
.MIN_VALUE
;
2097 private ArrayList
<View
> mViews
= new ArrayList
<View
>();
2098 int mCachedStart
= INVALID_LINE
;
2099 int mCachedEnd
= INVALID_LINE
;
2100 int mDeletedSize
= 0;
2103 private Span(int index
) {
2107 int getStartLine(int def
) {
2108 if (mCachedStart
!= INVALID_LINE
) {
2109 return mCachedStart
;
2111 if (mViews
.size() == 0) {
2114 calculateCachedStart();
2115 return mCachedStart
;
2118 void calculateCachedStart() {
2119 final View startView
= mViews
.get(0);
2120 final LayoutParams lp
= getLayoutParams(startView
);
2121 mCachedStart
= mPrimaryOrientation
.getDecoratedStart(startView
);
2123 LazySpanLookup
.FullSpanItem fsi
= mLazySpanLookup
2124 .getFullSpanItem(lp
.getViewLayoutPosition());
2125 if (fsi
!= null && fsi
.mGapDir
== LAYOUT_START
) {
2126 mCachedStart
-= fsi
.getGapForSpan(mIndex
);
2131 // Use this one when default value does not make sense and not having a value means a bug.
2132 int getStartLine() {
2133 if (mCachedStart
!= INVALID_LINE
) {
2134 return mCachedStart
;
2136 calculateCachedStart();
2137 return mCachedStart
;
2140 int getEndLine(int def
) {
2141 if (mCachedEnd
!= INVALID_LINE
) {
2144 final int size
= mViews
.size();
2148 calculateCachedEnd();
2152 void calculateCachedEnd() {
2153 final View endView
= mViews
.get(mViews
.size() - 1);
2154 final LayoutParams lp
= getLayoutParams(endView
);
2155 mCachedEnd
= mPrimaryOrientation
.getDecoratedEnd(endView
);
2157 LazySpanLookup
.FullSpanItem fsi
= mLazySpanLookup
2158 .getFullSpanItem(lp
.getViewLayoutPosition());
2159 if (fsi
!= null && fsi
.mGapDir
== LAYOUT_END
) {
2160 mCachedEnd
+= fsi
.getGapForSpan(mIndex
);
2165 // Use this one when default value does not make sense and not having a value means a bug.
2167 if (mCachedEnd
!= INVALID_LINE
) {
2170 calculateCachedEnd();
2174 void prependToSpan(View view
) {
2175 LayoutParams lp
= getLayoutParams(view
);
2177 mViews
.add(0, view
);
2178 mCachedStart
= INVALID_LINE
;
2179 if (mViews
.size() == 1) {
2180 mCachedEnd
= INVALID_LINE
;
2182 if (lp
.isItemRemoved() || lp
.isItemChanged()) {
2183 mDeletedSize
+= mPrimaryOrientation
.getDecoratedMeasurement(view
);
2187 void appendToSpan(View view
) {
2188 LayoutParams lp
= getLayoutParams(view
);
2191 mCachedEnd
= INVALID_LINE
;
2192 if (mViews
.size() == 1) {
2193 mCachedStart
= INVALID_LINE
;
2195 if (lp
.isItemRemoved() || lp
.isItemChanged()) {
2196 mDeletedSize
+= mPrimaryOrientation
.getDecoratedMeasurement(view
);
2200 // Useful method to preserve positions on a re-layout.
2201 void cacheReferenceLineAndClear(boolean reverseLayout
, int offset
) {
2203 if (reverseLayout
) {
2204 reference
= getEndLine(INVALID_LINE
);
2206 reference
= getStartLine(INVALID_LINE
);
2209 if (reference
== INVALID_LINE
) {
2212 if ((reverseLayout
&& reference
< mPrimaryOrientation
.getEndAfterPadding()) ||
2213 (!reverseLayout
&& reference
> mPrimaryOrientation
.getStartAfterPadding())) {
2216 if (offset
!= INVALID_OFFSET
) {
2217 reference
+= offset
;
2219 mCachedStart
= mCachedEnd
= reference
;
2228 void invalidateCache() {
2229 mCachedStart
= INVALID_LINE
;
2230 mCachedEnd
= INVALID_LINE
;
2233 void setLine(int line
) {
2234 mCachedEnd
= mCachedStart
= line
;
2238 final int size
= mViews
.size();
2239 View end
= mViews
.remove(size
- 1);
2240 final LayoutParams lp
= getLayoutParams(end
);
2242 if (lp
.isItemRemoved() || lp
.isItemChanged()) {
2243 mDeletedSize
-= mPrimaryOrientation
.getDecoratedMeasurement(end
);
2246 mCachedStart
= INVALID_LINE
;
2248 mCachedEnd
= INVALID_LINE
;
2252 View start
= mViews
.remove(0);
2253 final LayoutParams lp
= getLayoutParams(start
);
2255 if (mViews
.size() == 0) {
2256 mCachedEnd
= INVALID_LINE
;
2258 if (lp
.isItemRemoved() || lp
.isItemChanged()) {
2259 mDeletedSize
-= mPrimaryOrientation
.getDecoratedMeasurement(start
);
2261 mCachedStart
= INVALID_LINE
;
2264 public int getDeletedSize() {
2265 return mDeletedSize
;
2268 LayoutParams
getLayoutParams(View view
) {
2269 return (LayoutParams
) view
.getLayoutParams();
2272 void onOffset(int dt
) {
2273 if (mCachedStart
!= INVALID_LINE
) {
2276 if (mCachedEnd
!= INVALID_LINE
) {
2281 // normalized offset is how much this span can scroll
2282 int getNormalizedOffset(int dt
, int targetStart
, int targetEnd
) {
2283 if (mViews
.size() == 0) {
2287 final int endSpace
= getEndLine() - targetEnd
;
2288 if (endSpace
<= 0) {
2291 return -dt
> endSpace ?
-endSpace
: dt
;
2293 final int startSpace
= targetStart
- getStartLine();
2294 if (startSpace
<= 0) {
2297 return startSpace
< dt ? startSpace
: dt
;
2302 * Returns if there is no child between start-end lines
2304 * @param start The start line
2305 * @param end The end line
2306 * @return true if a new child can be added between start and end
2308 boolean isEmpty(int start
, int end
) {
2309 final int count
= mViews
.size();
2310 for (int i
= 0; i
< count
; i
++) {
2311 final View view
= mViews
.get(i
);
2312 if (mPrimaryOrientation
.getDecoratedStart(view
) < end
&&
2313 mPrimaryOrientation
.getDecoratedEnd(view
) > start
) {
2320 public int findFirstVisibleItemPosition() {
2321 return mReverseLayout
2322 ?
findOneVisibleChild(mViews
.size() - 1, -1, false)
2323 : findOneVisibleChild(0, mViews
.size(), false);
2326 public int findFirstCompletelyVisibleItemPosition() {
2327 return mReverseLayout
2328 ?
findOneVisibleChild(mViews
.size() - 1, -1, true)
2329 : findOneVisibleChild(0, mViews
.size(), true);
2332 public int findLastVisibleItemPosition() {
2333 return mReverseLayout
2334 ?
findOneVisibleChild(0, mViews
.size(), false)
2335 : findOneVisibleChild(mViews
.size() - 1, -1, false);
2338 public int findLastCompletelyVisibleItemPosition() {
2339 return mReverseLayout
2340 ?
findOneVisibleChild(0, mViews
.size(), true)
2341 : findOneVisibleChild(mViews
.size() - 1, -1, true);
2344 int findOneVisibleChild(int fromIndex
, int toIndex
, boolean completelyVisible
) {
2345 final int start
= mPrimaryOrientation
.getStartAfterPadding();
2346 final int end
= mPrimaryOrientation
.getEndAfterPadding();
2347 final int next
= toIndex
> fromIndex ?
1 : -1;
2348 for (int i
= fromIndex
; i
!= toIndex
; i
+= next
) {
2349 final View child
= mViews
.get(i
);
2350 final int childStart
= mPrimaryOrientation
.getDecoratedStart(child
);
2351 final int childEnd
= mPrimaryOrientation
.getDecoratedEnd(child
);
2352 if (childStart
< end
&& childEnd
> start
) {
2353 if (completelyVisible
) {
2354 if (childStart
>= start
&& childEnd
<= end
) {
2355 return getPosition(child
);
2358 return getPosition(child
);
2367 * An array of mappings from adapter position to span.
2368 * This only grows when a write happens and it grows up to the size of the adapter.
2370 static class LazySpanLookup
{
2372 private static final int MIN_SIZE
= 10;
2374 List
<FullSpanItem
> mFullSpanItems
;
2378 * Invalidates everything after this position, including full span information
2380 int forceInvalidateAfter(int position
) {
2381 if (mFullSpanItems
!= null) {
2382 for (int i
= mFullSpanItems
.size() - 1; i
>= 0; i
--) {
2383 FullSpanItem fsi
= mFullSpanItems
.get(i
);
2384 if (fsi
.mPosition
>= position
) {
2385 mFullSpanItems
.remove(i
);
2389 return invalidateAfter(position
);
2393 * returns end position for invalidation.
2395 int invalidateAfter(int position
) {
2396 if (mData
== null) {
2397 return RecyclerView
.NO_POSITION
;
2399 if (position
>= mData
.length
) {
2400 return RecyclerView
.NO_POSITION
;
2402 int endPosition
= invalidateFullSpansAfter(position
);
2403 if (endPosition
== RecyclerView
.NO_POSITION
) {
2404 Arrays
.fill(mData
, position
, mData
.length
, LayoutParams
.INVALID_SPAN_ID
);
2405 return mData
.length
;
2407 // just invalidate items in between
2408 Arrays
.fill(mData
, position
, endPosition
+ 1, LayoutParams
.INVALID_SPAN_ID
);
2409 return endPosition
+ 1;
2413 int getSpan(int position
) {
2414 if (mData
== null || position
>= mData
.length
) {
2415 return LayoutParams
.INVALID_SPAN_ID
;
2417 return mData
[position
];
2421 void setSpan(int position
, Span span
) {
2422 ensureSize(position
);
2423 mData
[position
] = span
.mIndex
;
2426 int sizeForPosition(int position
) {
2427 int len
= mData
.length
;
2428 while (len
<= position
) {
2434 void ensureSize(int position
) {
2435 if (mData
== null) {
2436 mData
= new int[Math
.max(position
, MIN_SIZE
) + 1];
2437 Arrays
.fill(mData
, LayoutParams
.INVALID_SPAN_ID
);
2438 } else if (position
>= mData
.length
) {
2440 mData
= new int[sizeForPosition(position
)];
2441 System
.arraycopy(old
, 0, mData
, 0, old
.length
);
2442 Arrays
.fill(mData
, old
.length
, mData
.length
, LayoutParams
.INVALID_SPAN_ID
);
2447 if (mData
!= null) {
2448 Arrays
.fill(mData
, LayoutParams
.INVALID_SPAN_ID
);
2450 mFullSpanItems
= null;
2453 void offsetForRemoval(int positionStart
, int itemCount
) {
2454 if (mData
== null || positionStart
>= mData
.length
) {
2457 ensureSize(positionStart
+ itemCount
);
2458 System
.arraycopy(mData
, positionStart
+ itemCount
, mData
, positionStart
,
2459 mData
.length
- positionStart
- itemCount
);
2460 Arrays
.fill(mData
, mData
.length
- itemCount
, mData
.length
,
2461 LayoutParams
.INVALID_SPAN_ID
);
2462 offsetFullSpansForRemoval(positionStart
, itemCount
);
2465 private void offsetFullSpansForRemoval(int positionStart
, int itemCount
) {
2466 if (mFullSpanItems
== null) {
2469 final int end
= positionStart
+ itemCount
;
2470 for (int i
= mFullSpanItems
.size() - 1; i
>= 0; i
--) {
2471 FullSpanItem fsi
= mFullSpanItems
.get(i
);
2472 if (fsi
.mPosition
< positionStart
) {
2475 if (fsi
.mPosition
< end
) {
2476 mFullSpanItems
.remove(i
);
2478 fsi
.mPosition
-= itemCount
;
2483 void offsetForAddition(int positionStart
, int itemCount
) {
2484 if (mData
== null || positionStart
>= mData
.length
) {
2487 ensureSize(positionStart
+ itemCount
);
2488 System
.arraycopy(mData
, positionStart
, mData
, positionStart
+ itemCount
,
2489 mData
.length
- positionStart
- itemCount
);
2490 Arrays
.fill(mData
, positionStart
, positionStart
+ itemCount
,
2491 LayoutParams
.INVALID_SPAN_ID
);
2492 offsetFullSpansForAddition(positionStart
, itemCount
);
2495 private void offsetFullSpansForAddition(int positionStart
, int itemCount
) {
2496 if (mFullSpanItems
== null) {
2499 for (int i
= mFullSpanItems
.size() - 1; i
>= 0; i
--) {
2500 FullSpanItem fsi
= mFullSpanItems
.get(i
);
2501 if (fsi
.mPosition
< positionStart
) {
2504 fsi
.mPosition
+= itemCount
;
2509 * Returns when invalidation should end. e.g. hitting a full span position.
2510 * Returned position SHOULD BE invalidated.
2512 private int invalidateFullSpansAfter(int position
) {
2513 if (mFullSpanItems
== null) {
2514 return RecyclerView
.NO_POSITION
;
2516 final FullSpanItem item
= getFullSpanItem(position
);
2517 // if there is an fsi at this position, get rid of it.
2519 mFullSpanItems
.remove(item
);
2521 int nextFsiIndex
= -1;
2522 final int count
= mFullSpanItems
.size();
2523 for (int i
= 0; i
< count
; i
++) {
2524 FullSpanItem fsi
= mFullSpanItems
.get(i
);
2525 if (fsi
.mPosition
>= position
) {
2530 if (nextFsiIndex
!= -1) {
2531 FullSpanItem fsi
= mFullSpanItems
.get(nextFsiIndex
);
2532 mFullSpanItems
.remove(nextFsiIndex
);
2533 return fsi
.mPosition
;
2535 return RecyclerView
.NO_POSITION
;
2538 public void addFullSpanItem(FullSpanItem fullSpanItem
) {
2539 if (mFullSpanItems
== null) {
2540 mFullSpanItems
= new ArrayList
<FullSpanItem
>();
2542 final int size
= mFullSpanItems
.size();
2543 for (int i
= 0; i
< size
; i
++) {
2544 FullSpanItem other
= mFullSpanItems
.get(i
);
2545 if (other
.mPosition
== fullSpanItem
.mPosition
) {
2547 throw new IllegalStateException("two fsis for same position");
2549 mFullSpanItems
.remove(i
);
2552 if (other
.mPosition
>= fullSpanItem
.mPosition
) {
2553 mFullSpanItems
.add(i
, fullSpanItem
);
2557 // if it is not added to a position.
2558 mFullSpanItems
.add(fullSpanItem
);
2561 public FullSpanItem
getFullSpanItem(int position
) {
2562 if (mFullSpanItems
== null) {
2565 for (int i
= mFullSpanItems
.size() - 1; i
>= 0; i
--) {
2566 final FullSpanItem fsi
= mFullSpanItems
.get(i
);
2567 if (fsi
.mPosition
== position
) {
2575 * @param minPos inclusive
2576 * @param maxPos exclusive
2577 * @param gapDir if not 0, returns FSIs on in that direction
2578 * @param hasUnwantedGapAfter If true, when full span item has unwanted gaps, it will be
2579 * returned even if its gap direction does not match.
2581 public FullSpanItem
getFirstFullSpanItemInRange(int minPos
, int maxPos
, int gapDir
,
2582 boolean hasUnwantedGapAfter
) {
2583 if (mFullSpanItems
== null) {
2586 final int limit
= mFullSpanItems
.size();
2587 for (int i
= 0; i
< limit
; i
++) {
2588 FullSpanItem fsi
= mFullSpanItems
.get(i
);
2589 if (fsi
.mPosition
>= maxPos
) {
2592 if (fsi
.mPosition
>= minPos
2593 && (gapDir
== 0 || fsi
.mGapDir
== gapDir
||
2594 (hasUnwantedGapAfter
&& fsi
.mHasUnwantedGapAfter
))) {
2602 * We keep information about full span items because they may create gaps in the UI.
2604 static class FullSpanItem
implements Parcelable
{
2609 // A full span may be laid out in primary direction but may have gaps due to
2610 // invalidation of views after it. This is recorded during a reverse scroll and if
2611 // view is still on the screen after scroll stops, we have to recalculate layout
2612 boolean mHasUnwantedGapAfter
;
2614 public FullSpanItem(Parcel in
) {
2615 mPosition
= in
.readInt();
2616 mGapDir
= in
.readInt();
2617 mHasUnwantedGapAfter
= in
.readInt() == 1;
2618 int spanCount
= in
.readInt();
2619 if (spanCount
> 0) {
2620 mGapPerSpan
= new int[spanCount
];
2621 in
.readIntArray(mGapPerSpan
);
2625 public FullSpanItem() {
2628 int getGapForSpan(int spanIndex
) {
2629 return mGapPerSpan
== null ?
0 : mGapPerSpan
[spanIndex
];
2632 public void invalidateSpanGaps() {
2637 public int describeContents() {
2642 public void writeToParcel(Parcel dest
, int flags
) {
2643 dest
.writeInt(mPosition
);
2644 dest
.writeInt(mGapDir
);
2645 dest
.writeInt(mHasUnwantedGapAfter ?
1 : 0);
2646 if (mGapPerSpan
!= null && mGapPerSpan
.length
> 0) {
2647 dest
.writeInt(mGapPerSpan
.length
);
2648 dest
.writeIntArray(mGapPerSpan
);
2655 public String
toString() {
2656 return "FullSpanItem{" +
2657 "mPosition=" + mPosition
+
2658 ", mGapDir=" + mGapDir
+
2659 ", mHasUnwantedGapAfter=" + mHasUnwantedGapAfter
+
2660 ", mGapPerSpan=" + Arrays
.toString(mGapPerSpan
) +
2664 public static final Parcelable
.Creator
<FullSpanItem
> CREATOR
2665 = new Parcelable
.Creator
<FullSpanItem
>() {
2667 public FullSpanItem
createFromParcel(Parcel in
) {
2668 return new FullSpanItem(in
);
2672 public FullSpanItem
[] newArray(int size
) {
2673 return new FullSpanItem
[size
];
2679 static class SavedState
implements Parcelable
{
2681 int mAnchorPosition
;
2682 int mVisibleAnchorPosition
; // Replacement for span info when spans are invalidated
2683 int mSpanOffsetsSize
;
2685 int mSpanLookupSize
;
2687 List
<LazySpanLookup
.FullSpanItem
> mFullSpanItems
;
2688 boolean mReverseLayout
;
2689 boolean mAnchorLayoutFromEnd
;
2690 boolean mLastLayoutRTL
;
2692 public SavedState() {
2695 SavedState(Parcel in
) {
2696 mAnchorPosition
= in
.readInt();
2697 mVisibleAnchorPosition
= in
.readInt();
2698 mSpanOffsetsSize
= in
.readInt();
2699 if (mSpanOffsetsSize
> 0) {
2700 mSpanOffsets
= new int[mSpanOffsetsSize
];
2701 in
.readIntArray(mSpanOffsets
);
2704 mSpanLookupSize
= in
.readInt();
2705 if (mSpanLookupSize
> 0) {
2706 mSpanLookup
= new int[mSpanLookupSize
];
2707 in
.readIntArray(mSpanLookup
);
2709 mReverseLayout
= in
.readInt() == 1;
2710 mAnchorLayoutFromEnd
= in
.readInt() == 1;
2711 mLastLayoutRTL
= in
.readInt() == 1;
2712 mFullSpanItems
= in
.readArrayList(
2713 LazySpanLookup
.FullSpanItem
.class.getClassLoader());
2716 public SavedState(SavedState other
) {
2717 mSpanOffsetsSize
= other
.mSpanOffsetsSize
;
2718 mAnchorPosition
= other
.mAnchorPosition
;
2719 mVisibleAnchorPosition
= other
.mVisibleAnchorPosition
;
2720 mSpanOffsets
= other
.mSpanOffsets
;
2721 mSpanLookupSize
= other
.mSpanLookupSize
;
2722 mSpanLookup
= other
.mSpanLookup
;
2723 mReverseLayout
= other
.mReverseLayout
;
2724 mAnchorLayoutFromEnd
= other
.mAnchorLayoutFromEnd
;
2725 mLastLayoutRTL
= other
.mLastLayoutRTL
;
2726 mFullSpanItems
= other
.mFullSpanItems
;
2729 void invalidateSpanInfo() {
2730 mSpanOffsets
= null;
2731 mSpanOffsetsSize
= 0;
2732 mSpanLookupSize
= 0;
2734 mFullSpanItems
= null;
2737 void invalidateAnchorPositionInfo() {
2738 mSpanOffsets
= null;
2739 mSpanOffsetsSize
= 0;
2740 mAnchorPosition
= NO_POSITION
;
2741 mVisibleAnchorPosition
= NO_POSITION
;
2745 public int describeContents() {
2750 public void writeToParcel(Parcel dest
, int flags
) {
2751 dest
.writeInt(mAnchorPosition
);
2752 dest
.writeInt(mVisibleAnchorPosition
);
2753 dest
.writeInt(mSpanOffsetsSize
);
2754 if (mSpanOffsetsSize
> 0) {
2755 dest
.writeIntArray(mSpanOffsets
);
2757 dest
.writeInt(mSpanLookupSize
);
2758 if (mSpanLookupSize
> 0) {
2759 dest
.writeIntArray(mSpanLookup
);
2761 dest
.writeInt(mReverseLayout ?
1 : 0);
2762 dest
.writeInt(mAnchorLayoutFromEnd ?
1 : 0);
2763 dest
.writeInt(mLastLayoutRTL ?
1 : 0);
2764 dest
.writeList(mFullSpanItems
);
2767 public static final Parcelable
.Creator
<SavedState
> CREATOR
2768 = new Parcelable
.Creator
<SavedState
>() {
2770 public SavedState
createFromParcel(Parcel in
) {
2771 return new SavedState(in
);
2775 public SavedState
[] newArray(int size
) {
2776 return new SavedState
[size
];
2782 * Data class to hold the information about an anchor position which is used in onLayout call.
2784 private class AnchorInfo
{
2788 boolean mLayoutFromEnd
;
2789 boolean mInvalidateOffsets
;
2792 mPosition
= NO_POSITION
;
2793 mOffset
= INVALID_OFFSET
;
2794 mLayoutFromEnd
= false;
2795 mInvalidateOffsets
= false;
2798 void assignCoordinateFromPadding() {
2799 mOffset
= mLayoutFromEnd ? mPrimaryOrientation
.getEndAfterPadding()
2800 : mPrimaryOrientation
.getStartAfterPadding();
2803 void assignCoordinateFromPadding(int addedDistance
) {
2804 if (mLayoutFromEnd
) {
2805 mOffset
= mPrimaryOrientation
.getEndAfterPadding() - addedDistance
;
2807 mOffset
= mPrimaryOrientation
.getStartAfterPadding() + addedDistance
;