blob: 0387e9605e9469f25517907f72c7b1090e693d7a [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.view.ViewCompat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
/**
* A LayoutManager that lays out children in a staggered grid formation.
* It supports horizontal & vertical layout as well as an ability to layout children in reverse.
* <p>
* Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps,
* StaggeredGridLayoutManager can offset spans independently or move items between spans. You can
* control this behavior via {@link #setGapStrategy(int)}.
*/
public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements
RecyclerView.SmoothScroller.ScrollVectorProvider {
private static final String TAG = "StaggeredGridLManager";
static final boolean DEBUG = false;
public static final int HORIZONTAL = RecyclerView.HORIZONTAL;
public static final int VERTICAL = RecyclerView.VERTICAL;
/**
* Does not do anything to hide gaps.
*/
public static final int GAP_HANDLING_NONE = 0;
/**
* @deprecated No longer supported.
*/
@SuppressWarnings("unused")
@Deprecated
public static final int GAP_HANDLING_LAZY = 1;
/**
* When scroll state is changed to {@link RecyclerView#SCROLL_STATE_IDLE}, StaggeredGrid will
* check if there are gaps in the because of full span items. If it finds, it will re-layout
* and move items to correct positions with animations.
* <p>
* For example, if LayoutManager ends up with the following layout due to adapter changes:
* <pre>
* AAA
* _BC
* DDD
* </pre>
* <p>
* It will animate to the following state:
* <pre>
* AAA
* BC_
* DDD
* </pre>
*/
public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2;
static final int INVALID_OFFSET = Integer.MIN_VALUE;
/**
* While trying to find next view to focus, LayoutManager will not try to scroll more
* than this factor times the total space of the list. If layout is vertical, total space is the
* height minus padding, if layout is horizontal, total space is the width minus padding.
*/
private static final float MAX_SCROLL_FACTOR = 1 / 3f;
/**
* Number of spans
*/
private int mSpanCount = -1;
Span[] mSpans;
/**
* Primary orientation is the layout's orientation, secondary orientation is the orientation
* for spans. Having both makes code much cleaner for calculations.
*/
@NonNull
OrientationHelper mPrimaryOrientation;
@NonNull
OrientationHelper mSecondaryOrientation;
private int mOrientation;
/**
* The width or height per span, depending on the orientation.
*/
private int mSizePerSpan;
@NonNull
private final LayoutState mLayoutState;
boolean mReverseLayout = false;
/**
* Aggregated reverse layout value that takes RTL into account.
*/
boolean mShouldReverseLayout = false;
/**
* Temporary variable used during fill method to check which spans needs to be filled.
*/
private BitSet mRemainingSpans;
/**
* When LayoutManager needs to scroll to a position, it sets this variable and requests a
* layout which will check this variable and re-layout accordingly.
*/
int mPendingScrollPosition = RecyclerView.NO_POSITION;
/**
* Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is
* called.
*/
int mPendingScrollPositionOffset = INVALID_OFFSET;
/**
* Keeps the mapping between the adapter positions and spans. This is necessary to provide
* a consistent experience when user scrolls the list.
*/
LazySpanLookup mLazySpanLookup = new LazySpanLookup();
/**
* how we handle gaps in UI.
*/
private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
/**
* Saved state needs this information to properly layout on restore.
*/
private boolean mLastLayoutFromEnd;
/**
* Saved state and onLayout needs this information to re-layout properly
*/
private boolean mLastLayoutRTL;
/**
* SavedState is not handled until a layout happens. This is where we keep it until next
* layout.
*/
private SavedState mPendingSavedState;
/**
* Re-used measurement specs. updated by onLayout.
*/
private int mFullSizeSpec;
/**
* Re-used rectangle to get child decor offsets.
*/
private final Rect mTmpRect = new Rect();
/**
* Re-used anchor info.
*/
private final AnchorInfo mAnchorInfo = new AnchorInfo();
/**
* If a full span item is invalid / or created in reverse direction; it may create gaps in
* the UI. While laying out, if such case is detected, we set this flag.
* <p>
* After scrolling stops, we check this flag and if it is set, re-layout.
*/
private boolean mLaidOutInvalidFullSpan = false;
/**
* Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}.
* see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}
*/
private boolean mSmoothScrollbarEnabled = true;
/**
* Temporary array used (solely in {@link #collectAdjacentPrefetchPositions}) for stashing and
* sorting distances to views being prefetched.
*/
private int[] mPrefetchDistances;
private final Runnable mCheckForGapsRunnable = new Runnable() {
@Override
public void run() {
checkForGaps();
}
};
/**
* Constructor used when layout manager is set in XML by RecyclerView attribute
* "layoutManager". Defaults to single column and vertical.
*/
@SuppressWarnings("unused")
public StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
setOrientation(properties.orientation);
setSpanCount(properties.spanCount);
setReverseLayout(properties.reverseLayout);
mLayoutState = new LayoutState();
createOrientationHelpers();
}
/**
* Creates a StaggeredGridLayoutManager with given parameters.
*
* @param spanCount If orientation is vertical, spanCount is number of columns. If
* orientation is horizontal, spanCount is number of rows.
* @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
*/
public StaggeredGridLayoutManager(int spanCount, int orientation) {
mOrientation = orientation;
setSpanCount(spanCount);
mLayoutState = new LayoutState();
createOrientationHelpers();
}
@Override
public boolean isAutoMeasureEnabled() {
return mGapStrategy != GAP_HANDLING_NONE;
}
private void createOrientationHelpers() {
mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation);
mSecondaryOrientation = OrientationHelper
.createOrientationHelper(this, 1 - mOrientation);
}
/**
* Checks for gaps in the UI that may be caused by adapter changes.
* <p>
* When a full span item is laid out in reverse direction, it sets a flag which we check when
* scroll is stopped (or re-layout happens) and re-layout after first valid item.
*/
boolean checkForGaps() {
if (getChildCount() == 0 || mGapStrategy == GAP_HANDLING_NONE || !isAttachedToWindow()) {
return false;
}
final int minPos, maxPos;
if (mShouldReverseLayout) {
minPos = getLastChildPosition();
maxPos = getFirstChildPosition();
} else {
minPos = getFirstChildPosition();
maxPos = getLastChildPosition();
}
if (minPos == 0) {
View gapView = hasGapsToFix();
if (gapView != null) {
mLazySpanLookup.clear();
requestSimpleAnimationsInNextLayout();
requestLayout();
return true;
}
}
if (!mLaidOutInvalidFullSpan) {
return false;
}
int invalidGapDir = mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
final LazySpanLookup.FullSpanItem invalidFsi = mLazySpanLookup
.getFirstFullSpanItemInRange(minPos, maxPos + 1, invalidGapDir, true);
if (invalidFsi == null) {
mLaidOutInvalidFullSpan = false;
mLazySpanLookup.forceInvalidateAfter(maxPos + 1);
return false;
}
final LazySpanLookup.FullSpanItem validFsi = mLazySpanLookup
.getFirstFullSpanItemInRange(minPos, invalidFsi.mPosition,
invalidGapDir * -1, true);
if (validFsi == null) {
mLazySpanLookup.forceInvalidateAfter(invalidFsi.mPosition);
} else {
mLazySpanLookup.forceInvalidateAfter(validFsi.mPosition + 1);
}
requestSimpleAnimationsInNextLayout();
requestLayout();
return true;
}
@Override
public void onScrollStateChanged(int state) {
if (state == RecyclerView.SCROLL_STATE_IDLE) {
checkForGaps();
}
}
@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
removeCallbacks(mCheckForGapsRunnable);
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].clear();
}
// SGLM will require fresh layout call to recover state after detach
view.requestLayout();
}
/**
* Checks for gaps if we've reached to the top of the list.
* <p>
* Intermediate gaps created by full span items are tracked via mLaidOutInvalidFullSpan field.
*/
View hasGapsToFix() {
int startChildIndex = 0;
int endChildIndex = getChildCount() - 1;
BitSet mSpansToCheck = new BitSet(mSpanCount);
mSpansToCheck.set(0, mSpanCount, true);
final int firstChildIndex, childLimit;
final int preferredSpanDir = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1;
if (mShouldReverseLayout) {
firstChildIndex = endChildIndex;
childLimit = startChildIndex - 1;
} else {
firstChildIndex = startChildIndex;
childLimit = endChildIndex + 1;
}
final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1;
for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (mSpansToCheck.get(lp.mSpan.mIndex)) {
if (checkSpanForGap(lp.mSpan)) {
return child;
}
mSpansToCheck.clear(lp.mSpan.mIndex);
}
if (lp.mFullSpan) {
continue; // quick reject
}
if (i + nextChildDiff != childLimit) {
View nextChild = getChildAt(i + nextChildDiff);
boolean compareSpans = false;
if (mShouldReverseLayout) {
// ensure child's end is below nextChild's end
int myEnd = mPrimaryOrientation.getDecoratedEnd(child);
int nextEnd = mPrimaryOrientation.getDecoratedEnd(nextChild);
if (myEnd < nextEnd) {
return child; //i should have a better position
} else if (myEnd == nextEnd) {
compareSpans = true;
}
} else {
int myStart = mPrimaryOrientation.getDecoratedStart(child);
int nextStart = mPrimaryOrientation.getDecoratedStart(nextChild);
if (myStart > nextStart) {
return child; //i should have a better position
} else if (myStart == nextStart) {
compareSpans = true;
}
}
if (compareSpans) {
// equal, check span indices.
LayoutParams nextLp = (LayoutParams) nextChild.getLayoutParams();
if (lp.mSpan.mIndex - nextLp.mSpan.mIndex < 0 != preferredSpanDir < 0) {
return child;
}
}
}
}
// everything looks good
return null;
}
private boolean checkSpanForGap(Span span) {
if (mShouldReverseLayout) {
if (span.getEndLine() < mPrimaryOrientation.getEndAfterPadding()) {
// if it is full span, it is OK
final View endView = span.mViews.get(span.mViews.size() - 1);
final LayoutParams lp = span.getLayoutParams(endView);
return !lp.mFullSpan;
}
} else if (span.getStartLine() > mPrimaryOrientation.getStartAfterPadding()) {
// if it is full span, it is OK
final View startView = span.mViews.get(0);
final LayoutParams lp = span.getLayoutParams(startView);
return !lp.mFullSpan;
}
return false;
}
/**
* Sets the number of spans for the layout. This will invalidate all of the span assignments
* for Views.
* <p>
* Calling this method will automatically result in a new layout request unless the spanCount
* parameter is equal to current span count.
*
* @param spanCount Number of spans to layout
*/
public void setSpanCount(int spanCount) {
assertNotInLayoutOrScroll(null);
if (spanCount != mSpanCount) {
invalidateSpanAssignments();
mSpanCount = spanCount;
mRemainingSpans = new BitSet(mSpanCount);
mSpans = new Span[mSpanCount];
for (int i = 0; i < mSpanCount; i++) {
mSpans[i] = new Span(i);
}
requestLayout();
}
}
/**
* Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep
* scroll position if this method is called after views are laid out.
*
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException("invalid orientation.");
}
assertNotInLayoutOrScroll(null);
if (orientation == mOrientation) {
return;
}
mOrientation = orientation;
OrientationHelper tmp = mPrimaryOrientation;
mPrimaryOrientation = mSecondaryOrientation;
mSecondaryOrientation = tmp;
requestLayout();
}
/**
* Sets whether LayoutManager should start laying out items from the end of the UI. The order
* items are traversed is not affected by this call.
* <p>
* For vertical layout, if it is set to <code>true</code>, first item will be at the bottom of
* the list.
* <p>
* For horizontal layouts, it depends on the layout direction.
* When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if
* {@link RecyclerView}} is RTL, it will layout from LTR.
*
* @param reverseLayout Whether layout should be in reverse or not
*/
public void setReverseLayout(boolean reverseLayout) {
assertNotInLayoutOrScroll(null);
if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) {
mPendingSavedState.mReverseLayout = reverseLayout;
}
mReverseLayout = reverseLayout;
requestLayout();
}
/**
* Returns the current gap handling strategy for StaggeredGridLayoutManager.
* <p>
* Staggered grid may have gaps in the layout due to changes in the adapter. To avoid gaps,
* StaggeredGridLayoutManager provides 2 options. Check {@link #GAP_HANDLING_NONE} and
* {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} for details.
* <p>
* By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}.
*
* @return Current gap handling strategy.
* @see #setGapStrategy(int)
* @see #GAP_HANDLING_NONE
* @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
*/
public int getGapStrategy() {
return mGapStrategy;
}
/**
* Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter
* is different than the current strategy, calling this method will trigger a layout request.
*
* @param gapStrategy The new gap handling strategy. Should be
* {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or {@link
* #GAP_HANDLING_NONE}.
* @see #getGapStrategy()
*/
public void setGapStrategy(int gapStrategy) {
assertNotInLayoutOrScroll(null);
if (gapStrategy == mGapStrategy) {
return;
}
if (gapStrategy != GAP_HANDLING_NONE
&& gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE "
+ "or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS");
}
mGapStrategy = gapStrategy;
requestLayout();
}
@Override
public void assertNotInLayoutOrScroll(String message) {
if (mPendingSavedState == null) {
super.assertNotInLayoutOrScroll(message);
}
}
/**
* Returns the number of spans laid out by StaggeredGridLayoutManager.
*
* @return Number of spans in the layout
*/
public int getSpanCount() {
return mSpanCount;
}
/**
* For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items.
* <p>
* If you need to cancel current assignments, you can call this method which will clear all
* assignments and request a new layout.
*/
public void invalidateSpanAssignments() {
mLazySpanLookup.clear();
requestLayout();
}
/**
* Calculates the views' layout order. (e.g. from end to start or start to end)
* RTL layout support is applied automatically. So if layout is RTL and
* {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
*/
private void resolveShouldLayoutReverse() {
// A == B is the same result, but we rather keep it readable
if (mOrientation == VERTICAL || !isLayoutRTL()) {
mShouldReverseLayout = mReverseLayout;
} else {
mShouldReverseLayout = !mReverseLayout;
}
}
boolean isLayoutRTL() {
return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
}
/**
* Returns whether views are laid out in reverse order or not.
* <p>
* Not that this value is not affected by RecyclerView's layout direction.
*
* @return True if layout is reversed, false otherwise
* @see #setReverseLayout(boolean)
*/
public boolean getReverseLayout() {
return mReverseLayout;
}
@Override
public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
// we don't like it to wrap content in our non-scroll direction.
final int width, height;
final int horizontalPadding = getPaddingLeft() + getPaddingRight();
final int verticalPadding = getPaddingTop() + getPaddingBottom();
if (mOrientation == VERTICAL) {
final int usedHeight = childrenBounds.height() + verticalPadding;
height = chooseSize(hSpec, usedHeight, getMinimumHeight());
width = chooseSize(wSpec, mSizePerSpan * mSpanCount + horizontalPadding,
getMinimumWidth());
} else {
final int usedWidth = childrenBounds.width() + horizontalPadding;
width = chooseSize(wSpec, usedWidth, getMinimumWidth());
height = chooseSize(hSpec, mSizePerSpan * mSpanCount + verticalPadding,
getMinimumHeight());
}
setMeasuredDimension(width, height);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
onLayoutChildren(recycler, state, true);
}
@Override
public void onAdapterChanged(@Nullable RecyclerView.Adapter oldAdapter,
@Nullable RecyclerView.Adapter newAdapter) {
// RV will remove all views so we should clear all spans and assignments of views into spans
mLazySpanLookup.clear();
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].clear();
}
}
private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state,
boolean shouldCheckForGaps) {
final AnchorInfo anchorInfo = mAnchorInfo;
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
anchorInfo.reset();
return;
}
}
boolean recalculateAnchor = !anchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null;
if (recalculateAnchor) {
anchorInfo.reset();
if (mPendingSavedState != null) {
applyPendingSavedState(anchorInfo);
} else {
resolveShouldLayoutReverse();
anchorInfo.mLayoutFromEnd = mShouldReverseLayout;
}
updateAnchorInfoForLayout(state, anchorInfo);
anchorInfo.mValid = true;
}
if (mPendingSavedState == null && mPendingScrollPosition == RecyclerView.NO_POSITION) {
if (anchorInfo.mLayoutFromEnd != mLastLayoutFromEnd
|| isLayoutRTL() != mLastLayoutRTL) {
mLazySpanLookup.clear();
anchorInfo.mInvalidateOffsets = true;
}
}
if (getChildCount() > 0 && (mPendingSavedState == null
|| mPendingSavedState.mSpanOffsetsSize < 1)) {
if (anchorInfo.mInvalidateOffsets) {
for (int i = 0; i < mSpanCount; i++) {
// Scroll to position is set, clear.
mSpans[i].clear();
if (anchorInfo.mOffset != INVALID_OFFSET) {
mSpans[i].setLine(anchorInfo.mOffset);
}
}
} else {
if (recalculateAnchor || mAnchorInfo.mSpanReferenceLines == null) {
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout,
anchorInfo.mOffset);
}
mAnchorInfo.saveSpanReferenceLines(mSpans);
} else {
for (int i = 0; i < mSpanCount; i++) {
final Span span = mSpans[i];
span.clear();
span.setLine(mAnchorInfo.mSpanReferenceLines[i]);
}
}
}
}
detachAndScrapAttachedViews(recycler);
mLayoutState.mRecycle = false;
mLaidOutInvalidFullSpan = false;
updateMeasureSpecs(mSecondaryOrientation.getTotalSpace());
updateLayoutState(anchorInfo.mPosition, state);
if (anchorInfo.mLayoutFromEnd) {
// Layout start.
setLayoutStateDirection(LayoutState.LAYOUT_START);
fill(recycler, mLayoutState, state);
// Layout end.
setLayoutStateDirection(LayoutState.LAYOUT_END);
mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state);
} else {
// Layout end.
setLayoutStateDirection(LayoutState.LAYOUT_END);
fill(recycler, mLayoutState, state);
// Layout start.
setLayoutStateDirection(LayoutState.LAYOUT_START);
mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state);
}
repositionToWrapContentIfNecessary();
if (getChildCount() > 0) {
if (mShouldReverseLayout) {
fixEndGap(recycler, state, true);
fixStartGap(recycler, state, false);
} else {
fixStartGap(recycler, state, true);
fixEndGap(recycler, state, false);
}
}
boolean hasGaps = false;
if (shouldCheckForGaps && !state.isPreLayout()) {
final boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE
&& getChildCount() > 0
&& (mLaidOutInvalidFullSpan || hasGapsToFix() != null);
if (needToCheckForGaps) {
removeCallbacks(mCheckForGapsRunnable);
if (checkForGaps()) {
hasGaps = true;
}
}
}
if (state.isPreLayout()) {
mAnchorInfo.reset();
}
mLastLayoutFromEnd = anchorInfo.mLayoutFromEnd;
mLastLayoutRTL = isLayoutRTL();
if (hasGaps) {
mAnchorInfo.reset();
onLayoutChildren(recycler, state, false);
}
}
@Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
mPendingScrollPosition = RecyclerView.NO_POSITION;
mPendingScrollPositionOffset = INVALID_OFFSET;
mPendingSavedState = null; // we don't need this anymore
mAnchorInfo.reset();
}
private void repositionToWrapContentIfNecessary() {
if (mSecondaryOrientation.getMode() == View.MeasureSpec.EXACTLY) {
return; // nothing to do
}
float maxSize = 0;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
float size = mSecondaryOrientation.getDecoratedMeasurement(child);
if (size < maxSize) {
continue;
}
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
if (layoutParams.isFullSpan()) {
size = 1f * size / mSpanCount;
}
maxSize = Math.max(maxSize, size);
}
int before = mSizePerSpan;
int desired = Math.round(maxSize * mSpanCount);
if (mSecondaryOrientation.getMode() == View.MeasureSpec.AT_MOST) {
desired = Math.min(desired, mSecondaryOrientation.getTotalSpace());
}
updateMeasureSpecs(desired);
if (mSizePerSpan == before) {
return; // nothing has changed
}
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.mFullSpan) {
continue;
}
if (isLayoutRTL() && mOrientation == VERTICAL) {
int newOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * mSizePerSpan;
int prevOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * before;
child.offsetLeftAndRight(newOffset - prevOffset);
} else {
int newOffset = lp.mSpan.mIndex * mSizePerSpan;
int prevOffset = lp.mSpan.mIndex * before;
if (mOrientation == VERTICAL) {
child.offsetLeftAndRight(newOffset - prevOffset);
} else {
child.offsetTopAndBottom(newOffset - prevOffset);
}
}
}
}
private void applyPendingSavedState(AnchorInfo anchorInfo) {
if (DEBUG) {
Log.d(TAG, "found saved state: " + mPendingSavedState);
}
if (mPendingSavedState.mSpanOffsetsSize > 0) {
if (mPendingSavedState.mSpanOffsetsSize == mSpanCount) {
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].clear();
int line = mPendingSavedState.mSpanOffsets[i];
if (line != Span.INVALID_LINE) {
if (mPendingSavedState.mAnchorLayoutFromEnd) {
line += mPrimaryOrientation.getEndAfterPadding();
} else {
line += mPrimaryOrientation.getStartAfterPadding();
}
}
mSpans[i].setLine(line);
}
} else {
mPendingSavedState.invalidateSpanInfo();
mPendingSavedState.mAnchorPosition = mPendingSavedState.mVisibleAnchorPosition;
}
}
mLastLayoutRTL = mPendingSavedState.mLastLayoutRTL;
setReverseLayout(mPendingSavedState.mReverseLayout);
resolveShouldLayoutReverse();
if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
} else {
anchorInfo.mLayoutFromEnd = mShouldReverseLayout;
}
if (mPendingSavedState.mSpanLookupSize > 1) {
mLazySpanLookup.mData = mPendingSavedState.mSpanLookup;
mLazySpanLookup.mFullSpanItems = mPendingSavedState.mFullSpanItems;
}
}
void updateAnchorInfoForLayout(RecyclerView.State state, AnchorInfo anchorInfo) {
if (updateAnchorFromPendingData(state, anchorInfo)) {
return;
}
if (updateAnchorFromChildren(state, anchorInfo)) {
return;
}
if (DEBUG) {
Log.d(TAG, "Deciding anchor info from fresh state");
}
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = 0;
}
private boolean updateAnchorFromChildren(RecyclerView.State state, AnchorInfo anchorInfo) {
// We don't recycle views out of adapter order. This way, we can rely on the first or
// last child as the anchor position.
// Layout direction may change but we should select the child depending on the latest
// layout direction. Otherwise, we'll choose the wrong child.
anchorInfo.mPosition = mLastLayoutFromEnd
? findLastReferenceChildPosition(state.getItemCount())
: findFirstReferenceChildPosition(state.getItemCount());
anchorInfo.mOffset = INVALID_OFFSET;
return true;
}
boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
// Validate scroll position if exists.
if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) {
return false;
}
// Validate it.
if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
mPendingScrollPosition = RecyclerView.NO_POSITION;
mPendingScrollPositionOffset = INVALID_OFFSET;
return false;
}
if (mPendingSavedState == null || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION
|| mPendingSavedState.mSpanOffsetsSize < 1) {
// If item is visible, make it fully visible.
final View child = findViewByPosition(mPendingScrollPosition);
if (child != null) {
// Use regular anchor position, offset according to pending offset and target
// child
anchorInfo.mPosition = mShouldReverseLayout ? getLastChildPosition()
: getFirstChildPosition();
if (mPendingScrollPositionOffset != INVALID_OFFSET) {
if (anchorInfo.mLayoutFromEnd) {
final int target = mPrimaryOrientation.getEndAfterPadding()
- mPendingScrollPositionOffset;
anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedEnd(child);
} else {
final int target = mPrimaryOrientation.getStartAfterPadding()
+ mPendingScrollPositionOffset;
anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedStart(child);
}
return true;
}
// no offset provided. Decide according to the child location
final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child);
if (childSize > mPrimaryOrientation.getTotalSpace()) {
// Item does not fit. Fix depending on layout direction.
anchorInfo.mOffset = anchorInfo.mLayoutFromEnd
? mPrimaryOrientation.getEndAfterPadding()
: mPrimaryOrientation.getStartAfterPadding();
return true;
}
final int startGap = mPrimaryOrientation.getDecoratedStart(child)
- mPrimaryOrientation.getStartAfterPadding();
if (startGap < 0) {
anchorInfo.mOffset = -startGap;
return true;
}
final int endGap = mPrimaryOrientation.getEndAfterPadding()
- mPrimaryOrientation.getDecoratedEnd(child);
if (endGap < 0) {
anchorInfo.mOffset = endGap;
return true;
}
// child already visible. just layout as usual
anchorInfo.mOffset = INVALID_OFFSET;
} else {
// Child is not visible. Set anchor coordinate depending on in which direction
// child will be visible.
anchorInfo.mPosition = mPendingScrollPosition;
if (mPendingScrollPositionOffset == INVALID_OFFSET) {
final int position = calculateScrollDirectionForPosition(
anchorInfo.mPosition);
anchorInfo.mLayoutFromEnd = position == LayoutState.LAYOUT_END;
anchorInfo.assignCoordinateFromPadding();
} else {
anchorInfo.assignCoordinateFromPadding(mPendingScrollPositionOffset);
}
anchorInfo.mInvalidateOffsets = true;
}
} else {
anchorInfo.mOffset = INVALID_OFFSET;
anchorInfo.mPosition = mPendingScrollPosition;
}
return true;
}
void updateMeasureSpecs(int totalSpace) {
mSizePerSpan = totalSpace / mSpanCount;
//noinspection ResourceType
mFullSizeSpec = View.MeasureSpec.makeMeasureSpec(
totalSpace, mSecondaryOrientation.getMode());
}
@Override
public boolean supportsPredictiveItemAnimations() {
return mPendingSavedState == null;
}
/**
* Returns the adapter position of the first visible view for each span.
* <p>
* Note that, this value is not affected by layout orientation or item order traversal.
* ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
* not in the layout.
* <p>
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
* views are ignored in this method.
*
* @param into An array to put the results into. If you don't provide any, LayoutManager will
* create a new one.
* @return The adapter position of the first visible item in each span. If a span does not have
* any items, {@link RecyclerView#NO_POSITION} is returned for that span.
* @see #findFirstCompletelyVisibleItemPositions(int[])
* @see #findLastVisibleItemPositions(int[])
*/
public int[] findFirstVisibleItemPositions(int[] into) {
if (into == null) {
into = new int[mSpanCount];
} else if (into.length < mSpanCount) {
throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
+ " to span count. Expected:" + mSpanCount + ", array size:" + into.length);
}
for (int i = 0; i < mSpanCount; i++) {
into[i] = mSpans[i].findFirstVisibleItemPosition();
}
return into;
}
/**
* Returns the adapter position of the first completely visible view for each span.
* <p>
* Note that, this value is not affected by layout orientation or item order traversal.
* ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
* not in the layout.
* <p>
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
* views are ignored in this method.
*
* @param into An array to put the results into. If you don't provide any, LayoutManager will
* create a new one.
* @return The adapter position of the first fully visible item in each span. If a span does
* not have any items, {@link RecyclerView#NO_POSITION} is returned for that span.
* @see #findFirstVisibleItemPositions(int[])
* @see #findLastCompletelyVisibleItemPositions(int[])
*/
public int[] findFirstCompletelyVisibleItemPositions(int[] into) {
if (into == null) {
into = new int[mSpanCount];
} else if (into.length < mSpanCount) {
throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
+ " to span count. Expected:" + mSpanCount + ", array size:" + into.length);
}
for (int i = 0; i < mSpanCount; i++) {
into[i] = mSpans[i].findFirstCompletelyVisibleItemPosition();
}
return into;
}
/**
* Returns the adapter position of the last visible view for each span.
* <p>
* Note that, this value is not affected by layout orientation or item order traversal.
* ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
* not in the layout.
* <p>
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
* views are ignored in this method.
*
* @param into An array to put the results into. If you don't provide any, LayoutManager will
* create a new one.
* @return The adapter position of the last visible item in each span. If a span does not have
* any items, {@link RecyclerView#NO_POSITION} is returned for that span.
* @see #findLastCompletelyVisibleItemPositions(int[])
* @see #findFirstVisibleItemPositions(int[])
*/
public int[] findLastVisibleItemPositions(int[] into) {
if (into == null) {
into = new int[mSpanCount];
} else if (into.length < mSpanCount) {
throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
+ " to span count. Expected:" + mSpanCount + ", array size:" + into.length);
}
for (int i = 0; i < mSpanCount; i++) {
into[i] = mSpans[i].findLastVisibleItemPosition();
}
return into;
}
/**
* Returns the adapter position of the last completely visible view for each span.
* <p>
* Note that, this value is not affected by layout orientation or item order traversal.
* ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
* not in the layout.
* <p>
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
* views are ignored in this method.
*
* @param into An array to put the results into. If you don't provide any, LayoutManager will
* create a new one.
* @return The adapter position of the last fully visible item in each span. If a span does not
* have any items, {@link RecyclerView#NO_POSITION} is returned for that span.
* @see #findFirstCompletelyVisibleItemPositions(int[])
* @see #findLastVisibleItemPositions(int[])
*/
public int[] findLastCompletelyVisibleItemPositions(int[] into) {
if (into == null) {
into = new int[mSpanCount];
} else if (into.length < mSpanCount) {
throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
+ " to span count. Expected:" + mSpanCount + ", array size:" + into.length);
}
for (int i = 0; i < mSpanCount; i++) {
into[i] = mSpans[i].findLastCompletelyVisibleItemPosition();
}
return into;
}
@Override
public int computeHorizontalScrollOffset(RecyclerView.State state) {
return computeScrollOffset(state);
}
private int computeScrollOffset(RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
return ScrollbarHelper.computeScrollOffset(state, mPrimaryOrientation,
findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled),
findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled),
this, mSmoothScrollbarEnabled, mShouldReverseLayout);
}
@Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
return computeScrollOffset(state);
}
@Override
public int computeHorizontalScrollExtent(RecyclerView.State state) {
return computeScrollExtent(state);
}
private int computeScrollExtent(RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
return ScrollbarHelper.computeScrollExtent(state, mPrimaryOrientation,
findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled),
findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled),
this, mSmoothScrollbarEnabled);
}
@Override
public int computeVerticalScrollExtent(RecyclerView.State state) {
return computeScrollExtent(state);
}
@Override
public int computeHorizontalScrollRange(RecyclerView.State state) {
return computeScrollRange(state);
}
private int computeScrollRange(RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
return ScrollbarHelper.computeScrollRange(state, mPrimaryOrientation,
findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled),
findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled),
this, mSmoothScrollbarEnabled);
}
@Override
public int computeVerticalScrollRange(RecyclerView.State state) {
return computeScrollRange(state);
}
private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp,
boolean alreadyMeasured) {
if (lp.mFullSpan) {
if (mOrientation == VERTICAL) {
measureChildWithDecorationsAndMargin(child, mFullSizeSpec,
getChildMeasureSpec(
getHeight(),
getHeightMode(),
getPaddingTop() + getPaddingBottom(),
lp.height,
true),
alreadyMeasured);
} else {
measureChildWithDecorationsAndMargin(
child,
getChildMeasureSpec(
getWidth(),
getWidthMode(),
getPaddingLeft() + getPaddingRight(),
lp.width,
true),
mFullSizeSpec,
alreadyMeasured);
}
} else {
if (mOrientation == VERTICAL) {
// Padding for width measure spec is 0 because left and right padding were already
// factored into mSizePerSpan.
measureChildWithDecorationsAndMargin(
child,
getChildMeasureSpec(
mSizePerSpan,
getWidthMode(),
0,
lp.width,
false),
getChildMeasureSpec(
getHeight(),
getHeightMode(),
getPaddingTop() + getPaddingBottom(),
lp.height,
true),
alreadyMeasured);
} else {
// Padding for height measure spec is 0 because top and bottom padding were already
// factored into mSizePerSpan.
measureChildWithDecorationsAndMargin(
child,
getChildMeasureSpec(
getWidth(),
getWidthMode(),
getPaddingLeft() + getPaddingRight(),
lp.width,
true),
getChildMeasureSpec(
mSizePerSpan,
getHeightMode(),
0,
lp.height,
false),
alreadyMeasured);
}
}
}
private void measureChildWithDecorationsAndMargin(View child, int widthSpec,
int heightSpec, boolean alreadyMeasured) {
calculateItemDecorationsForChild(child, mTmpRect);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mTmpRect.left,
lp.rightMargin + mTmpRect.right);
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mTmpRect.top,
lp.bottomMargin + mTmpRect.bottom);
final boolean measure = alreadyMeasured
? shouldReMeasureChild(child, widthSpec, heightSpec, lp)
: shouldMeasureChild(child, widthSpec, heightSpec, lp);
if (measure) {
child.measure(widthSpec, heightSpec);
}
}
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
if (startInset == 0 && endInset == 0) {
return spec;
}
final int mode = View.MeasureSpec.getMode(spec);
if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
return View.MeasureSpec.makeMeasureSpec(
Math.max(0, View.MeasureSpec.getSize(spec) - startInset - endInset), mode);
}
return spec;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof SavedState) {
mPendingSavedState = (SavedState) state;
if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
mPendingSavedState.invalidateAnchorPositionInfo();
mPendingSavedState.invalidateSpanInfo();
}
requestLayout();
} else if (DEBUG) {
Log.d(TAG, "invalid saved state class");
}
}
@Override
public Parcelable onSaveInstanceState() {
if (mPendingSavedState != null) {
return new SavedState(mPendingSavedState);
}
SavedState state = new SavedState();
state.mReverseLayout = mReverseLayout;
state.mAnchorLayoutFromEnd = mLastLayoutFromEnd;
state.mLastLayoutRTL = mLastLayoutRTL;
if (mLazySpanLookup != null && mLazySpanLookup.mData != null) {
state.mSpanLookup = mLazySpanLookup.mData;
state.mSpanLookupSize = state.mSpanLookup.length;
state.mFullSpanItems = mLazySpanLookup.mFullSpanItems;
} else {
state.mSpanLookupSize = 0;
}
if (getChildCount() > 0) {
state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition()
: getFirstChildPosition();
state.mVisibleAnchorPosition = findFirstVisibleItemPositionInt();
state.mSpanOffsetsSize = mSpanCount;
state.mSpanOffsets = new int[mSpanCount];
for (int i = 0; i < mSpanCount; i++) {
int line;
if (mLastLayoutFromEnd) {
line = mSpans[i].getEndLine(Span.INVALID_LINE);
if (line != Span.INVALID_LINE) {
line -= mPrimaryOrientation.getEndAfterPadding();
}
} else {
line = mSpans[i].getStartLine(Span.INVALID_LINE);
if (line != Span.INVALID_LINE) {
line -= mPrimaryOrientation.getStartAfterPadding();
}
}
state.mSpanOffsets[i] = line;
}
} else {
state.mAnchorPosition = RecyclerView.NO_POSITION;
state.mVisibleAnchorPosition = RecyclerView.NO_POSITION;
state.mSpanOffsetsSize = 0;
}
if (DEBUG) {
Log.d(TAG, "saved state:\n" + state);
}
return state;
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
if (getChildCount() > 0) {
final View start = findFirstVisibleItemClosestToStart(false);
final View end = findFirstVisibleItemClosestToEnd(false);
if (start == null || end == null) {
return;
}
final int startPos = getPosition(start);
final int endPos = getPosition(end);
if (startPos < endPos) {
event.setFromIndex(startPos);
event.setToIndex(endPos);
} else {
event.setFromIndex(endPos);
event.setToIndex(startPos);
}
}
}
/**
* Finds the first fully visible child to be used as an anchor child if span count changes when
* state is restored. If no children is fully visible, returns a partially visible child instead
* of returning null.
*/
int findFirstVisibleItemPositionInt() {
final View first = mShouldReverseLayout ? findFirstVisibleItemClosestToEnd(true) :
findFirstVisibleItemClosestToStart(true);
return first == null ? RecyclerView.NO_POSITION : getPosition(first);
}
/**
* This is for internal use. Not necessarily the child closest to start but the first child
* we find that matches the criteria.
* This method does not do any sorting based on child's start coordinate, instead, it uses
* children order.
*/
View findFirstVisibleItemClosestToStart(boolean fullyVisible) {
final int boundsStart = mPrimaryOrientation.getStartAfterPadding();
final int boundsEnd = mPrimaryOrientation.getEndAfterPadding();
final int limit = getChildCount();
View partiallyVisible = null;
for (int i = 0; i < limit; i++) {
final View child = getChildAt(i);
final int childStart = mPrimaryOrientation.getDecoratedStart(child);
final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
if (childEnd <= boundsStart || childStart >= boundsEnd) {
continue; // not visible at all
}
if (childStart >= boundsStart || !fullyVisible) {
// when checking for start, it is enough even if part of the child's top is visible
// as long as fully visible is not requested.
return child;
}
if (partiallyVisible == null) {
partiallyVisible = child;
}
}
return partiallyVisible;
}
/**
* This is for internal use. Not necessarily the child closest to bottom but the first child
* we find that matches the criteria.
* This method does not do any sorting based on child's end coordinate, instead, it uses
* children order.
*/
View findFirstVisibleItemClosestToEnd(boolean fullyVisible) {
final int boundsStart = mPrimaryOrientation.getStartAfterPadding();
final int boundsEnd = mPrimaryOrientation.getEndAfterPadding();
View partiallyVisible = null;
for (int i = getChildCount() - 1; i >= 0; i--) {
final View child = getChildAt(i);
final int childStart = mPrimaryOrientation.getDecoratedStart(child);
final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
if (childEnd <= boundsStart || childStart >= boundsEnd) {
continue; // not visible at all
}
if (childEnd <= boundsEnd || !fullyVisible) {
// when checking for end, it is enough even if part of the child's bottom is visible
// as long as fully visible is not requested.
return child;
}
if (partiallyVisible == null) {
partiallyVisible = child;
}
}
return partiallyVisible;
}
private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state,
boolean canOffsetChildren) {
final int maxEndLine = getMaxEnd(Integer.MIN_VALUE);
if (maxEndLine == Integer.MIN_VALUE) {
return;
}
int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine;
int fixOffset;
if (gap > 0) {
fixOffset = -scrollBy(-gap, recycler, state);
} else {
return; // nothing to fix
}
gap -= fixOffset;
if (canOffsetChildren && gap > 0) {
mPrimaryOrientation.offsetChildren(gap);
}
}
private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state,
boolean canOffsetChildren) {
final int minStartLine = getMinStart(Integer.MAX_VALUE);
if (minStartLine == Integer.MAX_VALUE) {
return;
}
int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding();
int fixOffset;
if (gap > 0) {
fixOffset = scrollBy(gap, recycler, state);
} else {
return; // nothing to fix
}
gap -= fixOffset;
if (canOffsetChildren && gap > 0) {
mPrimaryOrientation.offsetChildren(-gap);
}
}
private void updateLayoutState(int anchorPosition, RecyclerView.State state) {
mLayoutState.mAvailable = 0;
mLayoutState.mCurrentPosition = anchorPosition;
int startExtra = 0;
int endExtra = 0;
if (isSmoothScrolling()) {
final int targetPos = state.getTargetScrollPosition();
if (targetPos != RecyclerView.NO_POSITION) {
if (mShouldReverseLayout == targetPos < anchorPosition) {
endExtra = mPrimaryOrientation.getTotalSpace();
} else {
startExtra = mPrimaryOrientation.getTotalSpace();
}
}
}
// Line of the furthest row.
final boolean clipToPadding = getClipToPadding();
if (clipToPadding) {
mLayoutState.mStartLine = mPrimaryOrientation.getStartAfterPadding() - startExtra;
mLayoutState.mEndLine = mPrimaryOrientation.getEndAfterPadding() + endExtra;
} else {
mLayoutState.mEndLine = mPrimaryOrientation.getEnd() + endExtra;
mLayoutState.mStartLine = -startExtra;
}
mLayoutState.mStopInFocusable = false;
mLayoutState.mRecycle = true;
mLayoutState.mInfinite = mPrimaryOrientation.getMode() == View.MeasureSpec.UNSPECIFIED
&& mPrimaryOrientation.getEnd() == 0;
}
private void setLayoutStateDirection(int direction) {
mLayoutState.mLayoutDirection = direction;
mLayoutState.mItemDirection = (mShouldReverseLayout == (direction == LayoutState.LAYOUT_START))
? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD;
}
@Override
public void offsetChildrenHorizontal(int dx) {
super.offsetChildrenHorizontal(dx);
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].onOffset(dx);
}
}
@Override
public void offsetChildrenVertical(int dy) {
super.offsetChildrenVertical(dy);
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].onOffset(dy);
}
}
@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.REMOVE);
}
@Override
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.ADD);
}
@Override
public void onItemsChanged(RecyclerView recyclerView) {
mLazySpanLookup.clear();
requestLayout();
}
@Override
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
handleUpdate(from, to, AdapterHelper.UpdateOp.MOVE);
}
@Override
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
Object payload) {
handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.UPDATE);
}
/**
* Checks whether it should invalidate span assignments in response to an adapter change.
*/
private void handleUpdate(int positionStart, int itemCountOrToPosition, int cmd) {
int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition();
final int affectedRangeEnd; // exclusive
final int affectedRangeStart; // inclusive
if (cmd == AdapterHelper.UpdateOp.MOVE) {
if (positionStart < itemCountOrToPosition) {
affectedRangeEnd = itemCountOrToPosition + 1;
affectedRangeStart = positionStart;
} else {
affectedRangeEnd = positionStart + 1;
affectedRangeStart = itemCountOrToPosition;
}
} else {
affectedRangeStart = positionStart;
affectedRangeEnd = positionStart + itemCountOrToPosition;
}
mLazySpanLookup.invalidateAfter(affectedRangeStart);
switch (cmd) {
case AdapterHelper.UpdateOp.ADD:
mLazySpanLookup.offsetForAddition(positionStart, itemCountOrToPosition);
break;
case AdapterHelper.UpdateOp.REMOVE:
mLazySpanLookup.offsetForRemoval(positionStart, itemCountOrToPosition);
break;
case AdapterHelper.UpdateOp.MOVE:
// TODO optimize
mLazySpanLookup.offsetForRemoval(positionStart, 1);
mLazySpanLookup.offsetForAddition(itemCountOrToPosition, 1);
break;
}
if (affectedRangeEnd <= minPosition) {
return;
}
int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition();
if (affectedRangeStart <= maxPosition) {
requestLayout();
}
}
private int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state) {
mRemainingSpans.set(0, mSpanCount, true);
// The target position we are trying to reach.
final int targetLine;
// Line of the furthest row.
if (mLayoutState.mInfinite) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
targetLine = Integer.MAX_VALUE;
} else { // LAYOUT_START
targetLine = Integer.MIN_VALUE;
}
} else {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
targetLine = layoutState.mEndLine + layoutState.mAvailable;
} else { // LAYOUT_START
targetLine = layoutState.mStartLine - layoutState.mAvailable;
}
}
updateAllRemainingSpans(layoutState.mLayoutDirection, targetLine);
if (DEBUG) {
Log.d(TAG, "FILLING targetLine: " + targetLine + ","
+ "remaining spans:" + mRemainingSpans + ", state: " + layoutState);
}
// the default coordinate to add new view.
final int defaultNewViewLine = mShouldReverseLayout
? mPrimaryOrientation.getEndAfterPadding()
: mPrimaryOrientation.getStartAfterPadding();
boolean added = false;
while (layoutState.hasMore(state)
&& (mLayoutState.mInfinite || !mRemainingSpans.isEmpty())) {
View view = layoutState.next(recycler);
LayoutParams lp = ((LayoutParams) view.getLayoutParams());
final int position = lp.getViewLayoutPosition();
final int spanIndex = mLazySpanLookup.getSpan(position);
Span currentSpan;
final boolean assignSpan = spanIndex == LayoutParams.INVALID_SPAN_ID;
if (assignSpan) {
currentSpan = lp.mFullSpan ? mSpans[0] : getNextSpan(layoutState);
mLazySpanLookup.setSpan(position, currentSpan);
if (DEBUG) {
Log.d(TAG, "assigned " + currentSpan.mIndex + " for " + position);
}
} else {
if (DEBUG) {
Log.d(TAG, "using " + spanIndex + " for pos " + position);
}
currentSpan = mSpans[spanIndex];
}
// assign span before measuring so that item decorators can get updated span index
lp.mSpan = currentSpan;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
addView(view);
} else {
addView(view, 0);
}
measureChildWithDecorationsAndMargin(view, lp, false);
final int start;
final int end;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
start = lp.mFullSpan ? getMaxEnd(defaultNewViewLine)
: currentSpan.getEndLine(defaultNewViewLine);
end = start + mPrimaryOrientation.getDecoratedMeasurement(view);
if (assignSpan && lp.mFullSpan) {
LazySpanLookup.FullSpanItem fullSpanItem;
fullSpanItem = createFullSpanItemFromEnd(start);
fullSpanItem.mGapDir = LayoutState.LAYOUT_START;
fullSpanItem.mPosition = position;
mLazySpanLookup.addFullSpanItem(fullSpanItem);
}
} else {
end = lp.mFullSpan ? getMinStart(defaultNewViewLine)
: currentSpan.getStartLine(defaultNewViewLine);
start = end - mPrimaryOrientation.getDecoratedMeasurement(view);
if (assignSpan && lp.mFullSpan) {
LazySpanLookup.FullSpanItem fullSpanItem;
fullSpanItem = createFullSpanItemFromStart(end);
fullSpanItem.mGapDir = LayoutState.LAYOUT_END;
fullSpanItem.mPosition = position;
mLazySpanLookup.addFullSpanItem(fullSpanItem);
}
}
// check if this item may create gaps in the future
if (lp.mFullSpan && layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_HEAD) {
if (assignSpan) {
mLaidOutInvalidFullSpan = true;
} else {
final boolean hasInvalidGap;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
hasInvalidGap = !areAllEndsEqual();
} else { // layoutState.mLayoutDirection == LAYOUT_START
hasInvalidGap = !areAllStartsEqual();
}
if (hasInvalidGap) {
final LazySpanLookup.FullSpanItem fullSpanItem = mLazySpanLookup
.getFullSpanItem(position);
if (fullSpanItem != null) {
fullSpanItem.mHasUnwantedGapAfter = true;
}
mLaidOutInvalidFullSpan = true;
}
}
}
attachViewToSpans(view, lp, layoutState);
final int otherStart;
final int otherEnd;
if (isLayoutRTL() && mOrientation == VERTICAL) {
otherEnd = lp.mFullSpan ? mSecondaryOrientation.getEndAfterPadding() :
mSecondaryOrientation.getEndAfterPadding()
- (mSpanCount - 1 - currentSpan.mIndex) * mSizePerSpan;
otherStart = otherEnd - mSecondaryOrientation.getDecoratedMeasurement(view);
} else {
otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding()
: currentSpan.mIndex * mSizePerSpan
+ mSecondaryOrientation.getStartAfterPadding();
otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view);
}
if (mOrientation == VERTICAL) {
layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end);
} else {
layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd);
}
if (lp.mFullSpan) {
updateAllRemainingSpans(mLayoutState.mLayoutDirection, targetLine);
} else {
updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine);
}
recycle(recycler, mLayoutState);
if (mLayoutState.mStopInFocusable && view.hasFocusable()) {
if (lp.mFullSpan) {
mRemainingSpans.clear();
} else {
mRemainingSpans.set(currentSpan.mIndex, false);
}
}
added = true;
}
if (!added) {
recycle(recycler, mLayoutState);
}
final int diff;
if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding());
diff = mPrimaryOrientation.getStartAfterPadding() - minStart;
} else {
final int maxEnd = getMaxEnd(mPrimaryOrientation.getEndAfterPadding());
diff = maxEnd - mPrimaryOrientation.getEndAfterPadding();
}
return diff > 0 ? Math.min(layoutState.mAvailable, diff) : 0;
}
private LazySpanLookup.FullSpanItem createFullSpanItemFromEnd(int newItemTop) {
LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem();
fsi.mGapPerSpan = new int[mSpanCount];
for (int i = 0; i < mSpanCount; i++) {
fsi.mGapPerSpan[i] = newItemTop - mSpans[i].getEndLine(newItemTop);
}
return fsi;
}
private LazySpanLookup.FullSpanItem createFullSpanItemFromStart(int newItemBottom) {
LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem();
fsi.mGapPerSpan = new int[mSpanCount];
for (int i = 0; i < mSpanCount; i++) {
fsi.mGapPerSpan[i] = mSpans[i].getStartLine(newItemBottom) - newItemBottom;
}
return fsi;
}
private void attachViewToSpans(View view, LayoutParams lp, LayoutState layoutState) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
if (lp.mFullSpan) {
appendViewToAllSpans(view);
} else {
lp.mSpan.appendToSpan(view);
}
} else {
if (lp.mFullSpan) {
prependViewToAllSpans(view);
} else {
lp.mSpan.prependToSpan(view);
}
}
}
private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
if (layoutState.mAvailable == 0) {
// easy, recycle line is still valid
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleFromEnd(recycler, layoutState.mEndLine);
} else {
recycleFromStart(recycler, layoutState.mStartLine);
}
} else {
// scrolling case, recycle line can be shifted by how much space we could cover
// by adding new views
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
// calculate recycle line
int scrolled = layoutState.mStartLine - getMaxStart(layoutState.mStartLine);
final int line;
if (scrolled < 0) {
line = layoutState.mEndLine;
} else {
line = layoutState.mEndLine - Math.min(scrolled, layoutState.mAvailable);
}
recycleFromEnd(recycler, line);
} else {
// calculate recycle line
int scrolled = getMinEnd(layoutState.mEndLine) - layoutState.mEndLine;
final int line;
if (scrolled < 0) {
line = layoutState.mStartLine;
} else {
line = layoutState.mStartLine + Math.min(scrolled, layoutState.mAvailable);
}
recycleFromStart(recycler, line);
}
}
}
private void appendViewToAllSpans(View view) {
// traverse in reverse so that we end up assigning full span items to 0
for (int i = mSpanCount - 1; i >= 0; i--) {
mSpans[i].appendToSpan(view);
}
}
private void prependViewToAllSpans(View view) {
// traverse in reverse so that we end up assigning full span items to 0
for (int i = mSpanCount - 1; i >= 0; i--) {
mSpans[i].prependToSpan(view);
}
}
private void updateAllRemainingSpans(int layoutDir, int targetLine) {
for (int i = 0; i < mSpanCount; i++) {
if (mSpans[i].mViews.isEmpty()) {
continue;
}
updateRemainingSpans(mSpans[i], layoutDir, targetLine);
}
}
private void updateRemainingSpans(Span span, int layoutDir, int targetLine) {
final int deletedSize = span.getDeletedSize();
if (layoutDir == LayoutState.LAYOUT_START) {
final int line = span.getStartLine();
if (line + deletedSize <= targetLine) {
mRemainingSpans.set(span.mIndex, false);
}
} else {
final int line = span.getEndLine();
if (line - deletedSize >= targetLine) {
mRemainingSpans.set(span.mIndex, false);
}
}
}
private int getMaxStart(int def) {
int maxStart = mSpans[0].getStartLine(def);
for (int i = 1; i < mSpanCount; i++) {
final int spanStart = mSpans[i].getStartLine(def);
if (spanStart > maxStart) {
maxStart = spanStart;
}
}
return maxStart;
}
private int getMinStart(int def) {
int minStart = mSpans[0].getStartLine(def);
for (int i = 1; i < mSpanCount; i++) {
final int spanStart = mSpans[i].getStartLine(def);
if (spanStart < minStart) {
minStart = spanStart;
}
}
return minStart;
}
boolean areAllEndsEqual() {
int end = mSpans[0].getEndLine(Span.INVALID_LINE);
for (int i = 1; i < mSpanCount; i++) {
if (mSpans[i].getEndLine(Span.INVALID_LINE) != end) {
return false;
}
}
return true;
}
boolean areAllStartsEqual() {
int start = mSpans[0].getStartLine(Span.INVALID_LINE);
for (int i = 1; i < mSpanCount; i++) {
if (mSpans[i].getStartLine(Span.INVALID_LINE) != start) {
return false;
}
}
return true;
}
private int getMaxEnd(int def) {
int maxEnd = mSpans[0].getEndLine(def);
for (int i = 1; i < mSpanCount; i++) {
final int spanEnd = mSpans[i].getEndLine(def);
if (spanEnd > maxEnd) {
maxEnd = spanEnd;
}
}
return maxEnd;
}
private int getMinEnd(int def) {
int minEnd = mSpans[0].getEndLine(def);
for (int i = 1; i < mSpanCount; i++) {
final int spanEnd = mSpans[i].getEndLine(def);
if (spanEnd < minEnd) {
minEnd = spanEnd;
}
}
return minEnd;
}
private void recycleFromStart(RecyclerView.Recycler recycler, int line) {
while (getChildCount() > 0) {
View child = getChildAt(0);
if (mPrimaryOrientation.getDecoratedEnd(child) <= line
&& mPrimaryOrientation.getTransformedEndWithDecoration(child) <= line) {
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Don't recycle the last View in a span not to lose span's start/end lines
if (lp.mFullSpan) {
for (int j = 0; j < mSpanCount; j++) {
if (mSpans[j].mViews.size() == 1) {
return;
}
}
for (int j = 0; j < mSpanCount; j++) {
mSpans[j].popStart();
}
} else {
if (lp.mSpan.mViews.size() == 1) {
return;
}
lp.mSpan.popStart();
}
removeAndRecycleView(child, recycler);
} else {
return; // done
}
}
}
private void recycleFromEnd(RecyclerView.Recycler recycler, int line) {
final int childCount = getChildCount();
int i;
for (i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (mPrimaryOrientation.getDecoratedStart(child) >= line
&& mPrimaryOrientation.getTransformedStartWithDecoration(child) >= line) {
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Don't recycle the last View in a span not to lose span's start/end lines
if (lp.mFullSpan) {
for (int j = 0; j < mSpanCount; j++) {
if (mSpans[j].mViews.size() == 1) {
return;
}
}
for (int j = 0; j < mSpanCount; j++) {
mSpans[j].popEnd();
}
} else {
if (lp.mSpan.mViews.size() == 1) {
return;
}
lp.mSpan.popEnd();
}
removeAndRecycleView(child, recycler);
} else {
return; // done
}
}
}
/**
* @return True if last span is the first one we want to fill
*/
private boolean preferLastSpan(int layoutDir) {
if (mOrientation == HORIZONTAL) {
return (layoutDir == LayoutState.LAYOUT_START) != mShouldReverseLayout;
}
return ((layoutDir == LayoutState.LAYOUT_START) == mShouldReverseLayout) == isLayoutRTL();
}
/**
* Finds the span for the next view.
*/
private Span getNextSpan(LayoutState layoutState) {
final boolean preferLastSpan = preferLastSpan(layoutState.mLayoutDirection);
final int startIndex, endIndex, diff;
if (preferLastSpan) {
startIndex = mSpanCount - 1;
endIndex = -1;
diff = -1;
} else {
startIndex = 0;
endIndex = mSpanCount;
diff = 1;
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
Span min = null;
int minLine = Integer.MAX_VALUE;
final int defaultLine = mPrimaryOrientation.getStartAfterPadding();
for (int i = startIndex; i != endIndex; i += diff) {
final Span other = mSpans[i];
int otherLine = other.getEndLine(defaultLine);
if (otherLine < minLine) {
min = other;
minLine = otherLine;
}
}
return min;
} else {
Span max = null;
int maxLine = Integer.MIN_VALUE;
final int defaultLine = mPrimaryOrientation.getEndAfterPadding();
for (int i = startIndex; i != endIndex; i += diff) {
final Span other = mSpans[i];
int otherLine = other.getStartLine(defaultLine);
if (otherLine > maxLine) {
max = other;
maxLine = otherLine;
}
}
return max;
}
}
@Override
public boolean canScrollVertically() {
return mOrientation == VERTICAL;
}
@Override
public boolean canScrollHorizontally() {
return mOrientation == HORIZONTAL;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
return scrollBy(dx, recycler, state);
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
return scrollBy(dy, recycler, state);
}
private int calculateScrollDirectionForPosition(int position) {
if (getChildCount() == 0) {
return mShouldReverseLayout ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
}
final int firstChildPos = getFirstChildPosition();
return position < firstChildPos != mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
final int direction = calculateScrollDirectionForPosition(targetPosition);
PointF outVector = new PointF();
if (direction == 0) {
return null;
}
if (mOrientation == HORIZONTAL) {
outVector.x = direction;
outVector.y = 0;
} else {
outVector.x = 0;
outVector.y = direction;
}
return outVector;
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext());
scroller.setTargetPosition(position);
startSmoothScroll(scroller);
}
@Override
public void scrollToPosition(int position) {
if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) {
mPendingSavedState.invalidateAnchorPositionInfo();
}
mPendingScrollPosition = position;
mPendingScrollPositionOffset = INVALID_OFFSET;
requestLayout();
}
/**
* Scroll to the specified adapter position with the given offset from layout start.
* <p>
* Note that scroll position change will not be reflected until the next layout call.
* <p>
* If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
*
* @param position Index (starting at 0) of the reference item.
* @param offset The distance (in pixels) between the start edge of the item view and
* start edge of the RecyclerView.
* @see #setReverseLayout(boolean)
* @see #scrollToPosition(int)
*/
public void scrollToPositionWithOffset(int position, int offset) {
if (mPendingSavedState != null) {
mPendingSavedState.invalidateAnchorPositionInfo();
}
mPendingScrollPosition = position;
mPendingScrollPositionOffset = offset;
requestLayout();
}
/** @hide */
@Override
@RestrictTo(LIBRARY)
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
/* This method uses the simplifying assumption that the next N items (where N = span count)
* will be assigned, one-to-one, to spans, where ordering is based on which span extends
* least beyond the viewport.
*
* While this simplified model will be incorrect in some cases, it's difficult to know
* item heights, or whether individual items will be full span prior to construction.
*
* While this greedy estimation approach may underestimate the distance to prefetch items,
* it's very unlikely to overestimate them, so distances can be conservatively used to know
* the soonest (in terms of scroll distance) a prefetched view may come on screen.
*/
int delta = (mOrientation == HORIZONTAL) ? dx : dy;
if (getChildCount() == 0 || delta == 0) {
// can't support this scroll, so don't bother prefetching
return;
}
prepareLayoutStateForDelta(delta, state);
// build sorted list of distances to end of each span (though we don't care which is which)
if (mPrefetchDistances == null || mPrefetchDistances.length < mSpanCount) {
mPrefetchDistances = new int[mSpanCount];
}
int itemPrefetchCount = 0;
for (int i = 0; i < mSpanCount; i++) {
// compute number of pixels past the edge of the viewport that the current span extends
int distance = mLayoutState.mItemDirection == LayoutState.LAYOUT_START
? mLayoutState.mStartLine - mSpans[i].getStartLine(mLayoutState.mStartLine)
: mSpans[i].getEndLine(mLayoutState.mEndLine) - mLayoutState.mEndLine;
if (distance >= 0) {
// span extends to the edge, so prefetch next item
mPrefetchDistances[itemPrefetchCount] = distance;
itemPrefetchCount++;
}
}
Arrays.sort(mPrefetchDistances, 0, itemPrefetchCount);
// then assign them in order to the next N views (where N = span count)
for (int i = 0; i < itemPrefetchCount && mLayoutState.hasMore(state); i++) {
layoutPrefetchRegistry.addPosition(mLayoutState.mCurrentPosition,
mPrefetchDistances[i]);
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
}
}
void prepareLayoutStateForDelta(int delta, RecyclerView.State state) {
final int referenceChildPosition;
final int layoutDir;
if (delta > 0) { // layout towards end
layoutDir = LayoutState.LAYOUT_END;
referenceChildPosition = getLastChildPosition();
} else {
layoutDir = LayoutState.LAYOUT_START;
referenceChildPosition = getFirstChildPosition();
}
mLayoutState.mRecycle = true;
updateLayoutState(referenceChildPosition, state);
setLayoutStateDirection(layoutDir);
mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
mLayoutState.mAvailable = Math.abs(delta);
}
int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dt == 0) {
return 0;
}
prepareLayoutStateForDelta(dt, state);
int consumed = fill(recycler, mLayoutState, state);
final int available = mLayoutState.mAvailable;
final int totalScroll;
if (available < consumed) {
totalScroll = dt;
} else if (dt < 0) {
totalScroll = -consumed;
} else { // dt > 0
totalScroll = consumed;
}
if (DEBUG) {
Log.d(TAG, "asked " + dt + " scrolled" + totalScroll);
}
mPrimaryOrientation.offsetChildren(-totalScroll);
// always reset this if we scroll for a proper save instance state
mLastLayoutFromEnd = mShouldReverseLayout;
mLayoutState.mAvailable = 0;
recycle(recycler, mLayoutState);
return totalScroll;
}
int getLastChildPosition() {
final int childCount = getChildCount();
return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1));
}
int getFirstChildPosition() {
final int childCount = getChildCount();
return childCount == 0 ? 0 : getPosition(getChildAt(0));
}
/**
* Finds the first View that can be used as an anchor View.
*
* @return Position of the View or 0 if it cannot find any such View.
*/
private int findFirstReferenceChildPosition(int itemCount) {
final int limit = getChildCount();
for (int i = 0; i < limit; i++) {
final View view = getChildAt(i);
final int position = getPosition(view);
if (position >= 0 && position < itemCount) {
return position;
}
}
return 0;
}
/**
* Finds the last View that can be used as an anchor View.
*
* @return Position of the View or 0 if it cannot find any such View.
*/
private int findLastReferenceChildPosition(int itemCount) {
for (int i = getChildCount() - 1; i >= 0; i--) {
final View view = getChildAt(i);
final int position = getPosition(view);
if (position >= 0 && position < itemCount) {
return position;
}
}
return 0;
}
@SuppressWarnings("deprecation")
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
if (mOrientation == HORIZONTAL) {
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT);
} else {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
return new LayoutParams(c, attrs);
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
if (lp instanceof ViewGroup.MarginLayoutParams) {
return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
} else {
return new LayoutParams(lp);
}
}
@Override
public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
return lp instanceof LayoutParams;
}
public int getOrientation() {
return mOrientation;
}
@Nullable
@Override
public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (getChildCount() == 0) {
return null;
}
final View directChild = findContainingItemView(focused);
if (directChild == null) {
return null;
}
resolveShouldLayoutReverse();
final int layoutDir = convertFocusDirectionToLayoutDirection(direction);
if (layoutDir == LayoutState.INVALID_LAYOUT) {
return null;
}
LayoutParams prevFocusLayoutParams = (LayoutParams) directChild.getLayoutParams();
boolean prevFocusFullSpan = prevFocusLayoutParams.mFullSpan;
final Span prevFocusSpan = prevFocusLayoutParams.mSpan;
final int referenceChildPosition;
if (layoutDir == LayoutState.LAYOUT_END) { // layout towards end
referenceChildPosition = getLastChildPosition();
} else {
referenceChildPosition = getFirstChildPosition();
}
updateLayoutState(referenceChildPosition, state);
setLayoutStateDirection(layoutDir);
mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
mLayoutState.mAvailable = (int) (MAX_SCROLL_FACTOR * mPrimaryOrientation.getTotalSpace());
mLayoutState.mStopInFocusable = true;
mLayoutState.mRecycle = false;
fill(recycler, mLayoutState, state);
mLastLayoutFromEnd = mShouldReverseLayout;
if (!prevFocusFullSpan) {
View view = prevFocusSpan.getFocusableViewAfter(referenceChildPosition, layoutDir);
if (view != null && view != directChild) {
return view;
}
}
// either could not find from the desired span or prev view is full span.
// traverse all spans
if (preferLastSpan(layoutDir)) {
for (int i = mSpanCount - 1; i >= 0; i--) {
View view = mSpans[i].getFocusableViewAfter(referenceChildPosition, layoutDir);
if (view != null && view != directChild) {
return view;
}
}
} else {
for (int i = 0; i < mSpanCount; i++) {
View view = mSpans[i].getFocusableViewAfter(referenceChildPosition, layoutDir);
if (view != null && view != directChild) {
return view;
}
}
}
// Could not find any focusable views from any of the existing spans. Now start the search
// to find the best unfocusable candidate to become visible on the screen next. The search
// is done in the same fashion: first, check the views in the desired span and if no
// candidate is found, traverse the views in all the remaining spans.
boolean shouldSearchFromStart = !mReverseLayout == (layoutDir == LayoutState.LAYOUT_START);
View unfocusableCandidate = null;
if (!prevFocusFullSpan) {
unfocusableCandidate = findViewByPosition(shouldSearchFromStart
? prevFocusSpan.findFirstPartiallyVisibleItemPosition() :
prevFocusSpan.findLastPartiallyVisibleItemPosition());
if (unfocusableCandidate != null && unfocusableCandidate != directChild) {
return unfocusableCandidate;
}
}
if (preferLastSpan(layoutDir)) {
for (int i = mSpanCount - 1; i >= 0; i--) {
if (i == prevFocusSpan.mIndex) {
continue;
}
unfocusableCandidate = findViewByPosition(shouldSearchFromStart
? mSpans[i].findFirstPartiallyVisibleItemPosition() :
mSpans[i].findLastPartiallyVisibleItemPosition());
if (unfocusableCandidate != null && unfocusableCandidate != directChild) {
return unfocusableCandidate;
}
}
} else {
for (int i = 0; i < mSpanCount; i++) {
unfocusableCandidate = findViewByPosition(shouldSearchFromStart
? mSpans[i].findFirstPartiallyVisibleItemPosition() :
mSpans[i].findLastPartiallyVisibleItemPosition());
if (unfocusableCandidate != null && unfocusableCandidate != directChild) {
return unfocusableCandidate;
}
}
}
return null;
}
/**
* Converts a focusDirection to orientation.
*
* @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
* {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
* {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
* or 0 for not applicable
* @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction
* is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise.
*/
private int convertFocusDirectionToLayoutDirection(int focusDirection) {
switch (focusDirection) {
case View.FOCUS_BACKWARD:
if (mOrientation == VERTICAL) {
return LayoutState.LAYOUT_START;
} else if (isLayoutRTL()) {
return LayoutState.LAYOUT_END;
} else {
return LayoutState.LAYOUT_START;
}
case View.FOCUS_FORWARD:
if (mOrientation == VERTICAL) {
return LayoutState.LAYOUT_END;
} else if (isLayoutRTL()) {
return LayoutState.LAYOUT_START;
} else {
return LayoutState.LAYOUT_END;
}
case View.FOCUS_UP:
return mOrientation == VERTICAL ? LayoutState.LAYOUT_START
: LayoutState.INVALID_LAYOUT;
case View.FOCUS_DOWN:
return mOrientation == VERTICAL ? LayoutState.LAYOUT_END
: LayoutState.INVALID_LAYOUT;
case View.FOCUS_LEFT:
return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START
: LayoutState.INVALID_LAYOUT;
case View.FOCUS_RIGHT:
return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END
: LayoutState.INVALID_LAYOUT;
default:
if (DEBUG) {
Log.d(TAG, "Unknown focus request:" + focusDirection);
}
return LayoutState.INVALID_LAYOUT;
}
}
/**
* LayoutParams used by StaggeredGridLayoutManager.
* <p>
* Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the
* orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is
* expected to fill all of the space given to it.
*/
public static class LayoutParams extends RecyclerView.LayoutParams {
/**
* Span Id for Views that are not laid out yet.
*/
public static final int INVALID_SPAN_ID = -1;
// Package scope to be able to access from tests.
Span mSpan;
boolean mFullSpan;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(RecyclerView.LayoutParams source) {
super(source);
}
/**
* When set to true, the item will layout using all span area. That means, if orientation
* is vertical, the view will have full width; if orientation is horizontal, the view will
* have full height.
*
* @param fullSpan True if this item should traverse all spans.
* @see #isFullSpan()
*/
public void setFullSpan(boolean fullSpan) {
mFullSpan = fullSpan;
}
/**
* Returns whether this View occupies all available spans or just one.
*
* @return True if the View occupies all spans or false otherwise.
* @see #setFullSpan(boolean)
*/
public boolean isFullSpan() {
return mFullSpan;
}
/**
* Returns the Span index to which this View is assigned.
*
* @return The Span index of the View. If View is not yet assigned to any span, returns
* {@link #INVALID_SPAN_ID}.
*/
public final int getSpanIndex() {
if (mSpan == null) {
return INVALID_SPAN_ID;
}
return mSpan.mIndex;
}
}
// Package scoped to access from tests.
class Span {
static final int INVALID_LINE = Integer.MIN_VALUE;
ArrayList<View> mViews = new ArrayList<>();
int mCachedStart = INVALID_LINE;
int mCachedEnd = INVALID_LINE;
int mDeletedSize = 0;
final int mIndex;
Span(int index) {
mIndex = index;
}
int getStartLine(int def) {
if (mCachedStart != INVALID_LINE) {
return mCachedStart;
}
if (mViews.size() == 0) {
return def;
}
calculateCachedStart();
return mCachedStart;
}
void calculateCachedStart() {
final View startView = mViews.get(0);
final LayoutParams lp = getLayoutParams(startView);
mCachedStart = mPrimaryOrientation.getDecoratedStart(startView);
if (lp.mFullSpan) {
LazySpanLookup.FullSpanItem fsi = mLazySpanLookup
.getFullSpanItem(lp.getViewLayoutPosition());
if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_START) {
mCachedStart -= fsi.getGapForSpan(mIndex);
}
}
}
// Use this one when default value does not make sense and not having a value means a bug.
int getStartLine() {
if (mCachedStart != INVALID_LINE) {
return mCachedStart;
}
calculateCachedStart();
return mCachedStart;
}
int getEndLine(int def) {
if (mCachedEnd != INVALID_LINE) {
return mCachedEnd;
}
final int size = mViews.size();
if (size == 0) {
return def;
}
calculateCachedEnd();
return mCachedEnd;
}
void calculateCachedEnd() {
final View endView = mViews.get(mViews.size() - 1);
final LayoutParams lp = getLayoutParams(endView);
mCachedEnd = mPrimaryOrientation.getDecoratedEnd(endView);
if (lp.mFullSpan) {
LazySpanLookup.FullSpanItem fsi = mLazySpanLookup
.getFullSpanItem(lp.getViewLayoutPosition());
if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_END) {
mCachedEnd += fsi.getGapForSpan(mIndex);
}
}
}
// Use this one when default value does not make sense and not having a value means a bug.
int getEndLine() {
if (mCachedEnd != INVALID_LINE) {
return mCachedEnd;
}
calculateCachedEnd();
return mCachedEnd;
}
void prependToSpan(View view) {
LayoutParams lp = getLayoutParams(view);
lp.mSpan = this;
mViews.add(0, view);
mCachedStart = INVALID_LINE;
if (mViews.size() == 1) {
mCachedEnd = INVALID_LINE;
}
if (lp.isItemRemoved() || lp.isItemChanged()) {
mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
}
}
void appendToSpan(View view) {
LayoutParams lp = getLayoutParams(view);
lp.mSpan = this;
mViews.add(view);
mCachedEnd = INVALID_LINE;
if (mViews.size() == 1) {
mCachedStart = INVALID_LINE;
}
if (lp.isItemRemoved() || lp.isItemChanged()) {
mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
}
}
// Useful method to preserve positions on a re-layout.
void cacheReferenceLineAndClear(boolean reverseLayout, int offset) {
int reference;
if (reverseLayout) {
reference = getEndLine(INVALID_LINE);
} else {
reference = getStartLine(INVALID_LINE);
}
clear();
if (reference == INVALID_LINE) {
return;
}
if ((reverseLayout && reference < mPrimaryOrientation.getEndAfterPadding())
|| (!reverseLayout && reference > mPrimaryOrientation.getStartAfterPadding())) {
return;
}
if (offset != INVALID_OFFSET) {
reference += offset;
}
mCachedStart = mCachedEnd = reference;
}
void clear() {
mViews.clear();
invalidateCache();
mDeletedSize = 0;
}
void invalidateCache() {
mCachedStart = INVALID_LINE;
mCachedEnd = INVALID_LINE;
}
void setLine(int line) {
mCachedEnd = mCachedStart = line;
}
void popEnd() {
final int size = mViews.size();
View end = mViews.remove(size - 1);
final LayoutParams lp = getLayoutParams(end);
lp.mSpan = null;
if (lp.isItemRemoved() || lp.isItemChanged()) {
mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end);
}
if (size == 1) {
mCachedStart = INVALID_LINE;
}
mCachedEnd = INVALID_LINE;
}
void popStart() {
View start = mViews.remove(0);
final LayoutParams lp = getLayoutParams(start);
lp.mSpan = null;
if (mViews.size() == 0) {
mCachedEnd = INVALID_LINE;
}
if (lp.isItemRemoved() || lp.isItemChanged()) {
mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start);
}
mCachedStart = INVALID_LINE;
}
public int getDeletedSize() {
return mDeletedSize;
}
LayoutParams getLayoutParams(View view) {
return (LayoutParams) view.getLayoutParams();
}
void onOffset(int dt) {
if (mCachedStart != INVALID_LINE) {
mCachedStart += dt;
}
if (mCachedEnd != INVALID_LINE) {
mCachedEnd += dt;
}
}
public int findFirstVisibleItemPosition() {
return mReverseLayout
? findOneVisibleChild(mViews.size() - 1, -1, false)
: findOneVisibleChild(0, mViews.size(), false);
}
public int findFirstPartiallyVisibleItemPosition() {
return mReverseLayout
? findOnePartiallyVisibleChild(mViews.size() - 1, -1, true)
: findOnePartiallyVisibleChild(0, mViews.size(), true);
}
public int findFirstCompletelyVisibleItemPosition() {
return mReverseLayout
? findOneVisibleChild(mViews.size() - 1, -1, true)
: findOneVisibleChild(0, mViews.size(), true);
}
public int findLastVisibleItemPosition() {
return mReverseLayout
? findOneVisibleChild(0, mViews.size(), false)
: findOneVisibleChild(mViews.size() - 1, -1, false);
}
public int findLastPartiallyVisibleItemPosition() {
return mReverseLayout
? findOnePartiallyVisibleChild(0, mViews.size(), true)
: findOnePartiallyVisibleChild(mViews.size() - 1, -1, true);
}
public int findLastCompletelyVisibleItemPosition() {
return mReverseLayout
? findOneVisibleChild(0, mViews.size(), true)
: findOneVisibleChild(mViews.size() - 1, -1, true);
}
/**
* Returns the first view within this span that is partially or fully visible. Partially
* visible refers to a view that overlaps but is not fully contained within RV's padded
* bounded area. This view returned can be defined to have an area of overlap strictly
* greater than zero if acceptEndPointInclusion is false. If true, the view's endpoint
* inclusion is enough to consider it partially visible. The latter case can then refer to
* an out-of-bounds view positioned right at the top (or bottom) boundaries of RV's padded
* area. This is used e.g. inside
* {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} for
* calculating the next unfocusable child to become visible on the screen.
* @param fromIndex The child position index to start the search from.
* @param toIndex The child position index to end the search at.
* @param completelyVisible True if we have to only consider completely visible views,
* false otherwise.
* @param acceptCompletelyVisible True if we can consider both partially or fully visible
* views, false, if only a partially visible child should be
* returned.
* @param acceptEndPointInclusion If the view's endpoint intersection with RV's padded
* bounded area is enough to consider it partially visible,
* false otherwise
* @return The adapter position of the first view that's either partially or fully visible.
* {@link RecyclerView#NO_POSITION} if no such view is found.
*/
int findOnePartiallyOrCompletelyVisibleChild(int fromIndex, int toIndex,
boolean completelyVisible,
boolean acceptCompletelyVisible,
boolean acceptEndPointInclusion) {
final int start = mPrimaryOrientation.getStartAfterPadding();
final int end = mPrimaryOrientation.getEndAfterPadding();
final int next = toIndex > fromIndex ? 1 : -1;
for (int i = fromIndex; i != toIndex; i += next) {
final View child = mViews.get(i);
final int childStart = mPrimaryOrientation.getDecoratedStart(child);
final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
boolean childStartInclusion = acceptEndPointInclusion ? (childStart <= end)
: (childStart < end);
boolean childEndInclusion = acceptEndPointInclusion ? (childEnd >= start)
: (childEnd > start);
if (childStartInclusion && childEndInclusion) {
if (completelyVisible && acceptCompletelyVisible) {
// the child has to be completely visible to be returned.
if (childStart >= start && childEnd <= end) {
return getPosition(child);
}
} else if (acceptCompletelyVisible) {
// can return either a partially or completely visible child.
return getPosition(child);
} else if (childStart < start || childEnd > end) {
// should return a partially visible child if exists and a completely
// visible child is not acceptable in this case.
return getPosition(child);
}
}
}
return RecyclerView.NO_POSITION;
}
int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) {
return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, completelyVisible,
true, false);
}
int findOnePartiallyVisibleChild(int fromIndex, int toIndex,
boolean acceptEndPointInclusion) {
return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, false, false,
acceptEndPointInclusion);
}
/**
* Depending on the layout direction, returns the View that is after the given position.
*/
public View getFocusableViewAfter(int referenceChildPosition, int layoutDir) {
View candidate = null;
if (layoutDir == LayoutState.LAYOUT_START) {
final int limit = mViews.size();
for (int i = 0; i < limit; i++) {
final View view = mViews.get(i);
if ((mReverseLayout && getPosition(view) <= referenceChildPosition)
|| (!mReverseLayout && getPosition(view) >= referenceChildPosition)) {
break;
}
if (view.hasFocusable()) {
candidate = view;
} else {
break;
}
}
} else {
for (int i = mViews.size() - 1; i >= 0; i--) {
final View view = mViews.get(i);
if ((mReverseLayout && getPosition(view) >= referenceChildPosition)
|| (!mReverseLayout && getPosition(view) <= referenceChildPosition)) {
break;
}
if (view.hasFocusable()) {
candidate = view;
} else {
break;
}
}
}
return candidate;
}
}
/**
* An array of mappings from adapter position to span.
* This only grows when a write happens and it grows up to the size of the adapter.
*/
static class LazySpanLookup {
private static final int MIN_SIZE = 10;
int[] mData;
List<FullSpanItem> mFullSpanItems;
/**
* Invalidates everything after this position, including full span information
*/
int forceInvalidateAfter(int position) {
if (mFullSpanItems != null) {
for (int i = mFullSpanItems.size() - 1; i >= 0; i--) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition >= position) {
mFullSpanItems.remove(i);
}
}
}
return invalidateAfter(position);
}
/**
* returns end position for invalidation.
*/
int invalidateAfter(int position) {
if (mData == null) {
return RecyclerView.NO_POSITION;
}
if (position >= mData.length) {
return RecyclerView.NO_POSITION;
}
int endPosition = invalidateFullSpansAfter(position);
if (endPosition == RecyclerView.NO_POSITION) {
Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID);
return mData.length;
} else {
// Just invalidate items in between `position` and the next full span item, or the
// end of the tracked spans in mData if it's not been lengthened yet.
final int invalidateToIndex = Math.min(endPosition + 1, mData.length);
Arrays.fill(mData, position, invalidateToIndex, LayoutParams.INVALID_SPAN_ID);
return invalidateToIndex;
}
}
int getSpan(int position) {
if (mData == null || position >= mData.length) {
return LayoutParams.INVALID_SPAN_ID;
} else {
return mData[position];
}
}
void setSpan(int position, Span span) {
ensureSize(position);
mData[position] = span.mIndex;
}
int sizeForPosition(int position) {
int len = mData.length;
while (len <= position) {
len *= 2;
}
return len;
}
void ensureSize(int position) {
if (mData == null) {
mData = new int[Math.max(position, MIN_SIZE) + 1];
Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
} else if (position >= mData.length) {
int[] old = mData;
mData = new int[sizeForPosition(position)];
System.arraycopy(old, 0, mData, 0, old.length);
Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID);
}
}
void clear() {
if (mData != null) {
Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
}
mFullSpanItems = null;
}
void offsetForRemoval(int positionStart, int itemCount) {
if (mData == null || positionStart >= mData.length) {
return;
}
ensureSize(positionStart + itemCount);
System.arraycopy(mData, positionStart + itemCount, mData, positionStart,
mData.length - positionStart - itemCount);
Arrays.fill(mData, mData.length - itemCount, mData.length,
LayoutParams.INVALID_SPAN_ID);
offsetFullSpansForRemoval(positionStart, itemCount);
}
private void offsetFullSpansForRemoval(int positionStart, int itemCount) {
if (mFullSpanItems == null) {
return;
}
final int end = positionStart + itemCount;
for (int i = mFullSpanItems.size() - 1; i >= 0; i--) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition < positionStart) {
continue;
}
if (fsi.mPosition < end) {
mFullSpanItems.remove(i);
} else {
fsi.mPosition -= itemCount;
}
}
}
void offsetForAddition(int positionStart, int itemCount) {
if (mData == null || positionStart >= mData.length) {
return;
}
ensureSize(positionStart + itemCount);
System.arraycopy(mData, positionStart, mData, positionStart + itemCount,
mData.length - positionStart - itemCount);
Arrays.fill(mData, positionStart, positionStart + itemCount,
LayoutParams.INVALID_SPAN_ID);
offsetFullSpansForAddition(positionStart, itemCount);
}
private void offsetFullSpansForAddition(int positionStart, int itemCount) {
if (mFullSpanItems == null) {
return;
}
for (int i = mFullSpanItems.size() - 1; i >= 0; i--) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition < positionStart) {
continue;
}
fsi.mPosition += itemCount;
}
}
/**
* Returns when invalidation should end. e.g. hitting a full span position.
* Returned position SHOULD BE invalidated.
*/
private int invalidateFullSpansAfter(int position) {
if (mFullSpanItems == null) {
return RecyclerView.NO_POSITION;
}
final FullSpanItem item = getFullSpanItem(position);
// if there is an fsi at this position, get rid of it.
if (item != null) {
mFullSpanItems.remove(item);
}
int nextFsiIndex = -1;
final int count = mFullSpanItems.size();
for (int i = 0; i < count; i++) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition >= position) {
nextFsiIndex = i;
break;
}
}
if (nextFsiIndex != -1) {
FullSpanItem fsi = mFullSpanItems.get(nextFsiIndex);
mFullSpanItems.remove(nextFsiIndex);
return fsi.mPosition;
}
return RecyclerView.NO_POSITION;
}
public void addFullSpanItem(FullSpanItem fullSpanItem) {
if (mFullSpanItems == null) {
mFullSpanItems = new ArrayList<>();
}
final int size = mFullSpanItems.size();
for (int i = 0; i < size; i++) {
FullSpanItem other = mFullSpanItems.get(i);
if (other.mPosition == fullSpanItem.mPosition) {
if (DEBUG) {
throw new IllegalStateException("two fsis for same position");
} else {
mFullSpanItems.remove(i);
}
}
if (other.mPosition >= fullSpanItem.mPosition) {
mFullSpanItems.add(i, fullSpanItem);
return;
}
}
// if it is not added to a position.
mFullSpanItems.add(fullSpanItem);
}
public FullSpanItem getFullSpanItem(int position) {
if (mFullSpanItems == null) {
return null;
}
for (int i = mFullSpanItems.size() - 1; i >= 0; i--) {
final FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition == position) {
return fsi;
}
}
return null;
}
/**
* @param minPos inclusive
* @param maxPos exclusive
* @param gapDir if not 0, returns FSIs on in that direction
* @param hasUnwantedGapAfter If true, when full span item has unwanted gaps, it will be
* returned even if its gap direction does not match.
*/
public FullSpanItem getFirstFullSpanItemInRange(int minPos, int maxPos, int gapDir,
boolean hasUnwantedGapAfter) {
if (mFullSpanItems == null) {
return null;
}
final int limit = mFullSpanItems.size();
for (int i = 0; i < limit; i++) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition >= maxPos) {
return null;
}
if (fsi.mPosition >= minPos
&& (gapDir == 0 || fsi.mGapDir == gapDir
|| (hasUnwantedGapAfter && fsi.mHasUnwantedGapAfter))) {
return fsi;
}
}
return null;
}
/**
* We keep information about full span items because they may create gaps in the UI.
*/
@SuppressLint("BanParcelableUsage")
static class FullSpanItem implements Parcelable {
int mPosition;
int mGapDir;
int[] mGapPerSpan;
// A full span may be laid out in primary direction but may have gaps due to
// invalidation of views after it. This is recorded during a reverse scroll and if
// view is still on the screen after scroll stops, we have to recalculate layout
boolean mHasUnwantedGapAfter;
FullSpanItem(Parcel in) {
mPosition = in.readInt();
mGapDir = in.readInt();
mHasUnwantedGapAfter = in.readInt() == 1;
int spanCount = in.readInt();
if (spanCount > 0) {
mGapPerSpan = new int[spanCount];
in.readIntArray(mGapPerSpan);
}
}
FullSpanItem() {
}
int getGapForSpan(int spanIndex) {
return mGapPerSpan == null ? 0 : mGapPerSpan[spanIndex];
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mPosition);
dest.writeInt(mGapDir);
dest.writeInt(mHasUnwantedGapAfter ? 1 : 0);
if (mGapPerSpan != null && mGapPerSpan.length > 0) {
dest.writeInt(mGapPerSpan.length);
dest.writeIntArray(mGapPerSpan);
} else {
dest.writeInt(0);
}
}
@Override
public String toString() {
return "FullSpanItem{"
+ "mPosition=" + mPosition
+ ", mGapDir=" + mGapDir
+ ", mHasUnwantedGapAfter=" + mHasUnwantedGapAfter
+ ", mGapPerSpan=" + Arrays.toString(mGapPerSpan)
+ '}';
}
public static final Parcelable.Creator<FullSpanItem> CREATOR =
new Parcelable.Creator<FullSpanItem>() {
@Override
public FullSpanItem createFromParcel(Parcel in) {
return new FullSpanItem(in);
}
@Override
public FullSpanItem[] newArray(int size) {
return new FullSpanItem[size];
}
};
}
}
/**
* @hide
*/
@RestrictTo(LIBRARY)
@SuppressLint("BanParcelableUsage")
public static class SavedState implements Parcelable {
int mAnchorPosition;
int mVisibleAnchorPosition; // Replacement for span info when spans are invalidated
int mSpanOffsetsSize;
int[] mSpanOffsets;
int mSpanLookupSize;
int[] mSpanLookup;
List<LazySpanLookup.FullSpanItem> mFullSpanItems;
boolean mReverseLayout;
boolean mAnchorLayoutFromEnd;
boolean mLastLayoutRTL;
public SavedState() {
}
SavedState(Parcel in) {
mAnchorPosition = in.readInt();
mVisibleAnchorPosition = in.readInt();
mSpanOffsetsSize = in.readInt();
if (mSpanOffsetsSize > 0) {
mSpanOffsets = new int[mSpanOffsetsSize];
in.readIntArray(mSpanOffsets);
}
mSpanLookupSize = in.readInt();
if (mSpanLookupSize > 0) {
mSpanLookup = new int[mSpanLookupSize];
in.readIntArray(mSpanLookup);
}
mReverseLayout = in.readInt() == 1;
mAnchorLayoutFromEnd = in.readInt() == 1;
mLastLayoutRTL = in.readInt() == 1;
@SuppressWarnings("unchecked")
List<LazySpanLookup.FullSpanItem> fullSpanItems =
in.readArrayList(LazySpanLookup.FullSpanItem.class.getClassLoader());
mFullSpanItems = fullSpanItems;
}
public SavedState(SavedState other) {
mSpanOffsetsSize = other.mSpanOffsetsSize;
mAnchorPosition = other.mAnchorPosition;
mVisibleAnchorPosition = other.mVisibleAnchorPosition;
mSpanOffsets = other.mSpanOffsets;
mSpanLookupSize = other.mSpanLookupSize;
mSpanLookup = other.mSpanLookup;
mReverseLayout = other.mReverseLayout;
mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
mLastLayoutRTL = other.mLastLayoutRTL;
mFullSpanItems = other.mFullSpanItems;
}
void invalidateSpanInfo() {
mSpanOffsets = null;
mSpanOffsetsSize = 0;
mSpanLookupSize = 0;
mSpanLookup = null;
mFullSpanItems = null;
}
void invalidateAnchorPositionInfo() {
mSpanOffsets = null;
mSpanOffsetsSize = 0;
mAnchorPosition = RecyclerView.NO_POSITION;
mVisibleAnchorPosition = RecyclerView.NO_POSITION;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mAnchorPosition);
dest.writeInt(mVisibleAnchorPosition);
dest.writeInt(mSpanOffsetsSize);
if (mSpanOffsetsSize > 0) {
dest.writeIntArray(mSpanOffsets);
}
dest.writeInt(mSpanLookupSize);
if (mSpanLookupSize > 0) {
dest.writeIntArray(mSpanLookup);
}
dest.writeInt(mReverseLayout ? 1 : 0);
dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
dest.writeInt(mLastLayoutRTL ? 1 : 0);
dest.writeList(mFullSpanItems);
}
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
/**
* Data class to hold the information about an anchor position which is used in onLayout call.
*/
class AnchorInfo {
int mPosition;
int mOffset;
boolean mLayoutFromEnd;
boolean mInvalidateOffsets;
boolean mValid;
// this is where we save span reference lines in case we need to re-use them for multi-pass
// measure steps
int[] mSpanReferenceLines;
AnchorInfo() {
reset();
}
void reset() {
mPosition = RecyclerView.NO_POSITION;
mOffset = INVALID_OFFSET;
mLayoutFromEnd = false;
mInvalidateOffsets = false;
mValid = false;
if (mSpanReferenceLines != null) {
Arrays.fill(mSpanReferenceLines, -1);
}
}
void saveSpanReferenceLines(Span[] spans) {
int spanCount = spans.length;
if (mSpanReferenceLines == null || mSpanReferenceLines.length < spanCount) {
mSpanReferenceLines = new int[mSpans.length];
}
for (int i = 0; i < spanCount; i++) {
// does not matter start or end since this is only recorded when span is reset
mSpanReferenceLines[i] = spans[i].getStartLine(Span.INVALID_LINE);
}
}
void assignCoordinateFromPadding() {
mOffset = mLayoutFromEnd ? mPrimaryOrientation.getEndAfterPadding()
: mPrimaryOrientation.getStartAfterPadding();
}
void assignCoordinateFromPadding(int addedDistance) {
if (mLayoutFromEnd) {
mOffset = mPrimaryOrientation.getEndAfterPadding() - addedDistance;
} else {
mOffset = mPrimaryOrientation.getStartAfterPadding() + addedDistance;
}
}
}
}