mirror of https://github.com/M66B/FairEmail.git
3283 lines
127 KiB
Java
3283 lines
127 KiB
Java
/*
|
|
* 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;
|
|
}
|
|
}
|
|
}
|
|
}
|