mirror of https://github.com/NekoX-Dev/NekoX.git
897 lines
34 KiB
Java
897 lines
34 KiB
Java
/*
|
|
* Copyright (C) 2014 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 languag`e governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package org.telegram.messenger.support.widget;
|
|
|
|
import android.content.Context;
|
|
import android.graphics.Rect;
|
|
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.util.SparseIntArray;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
|
|
import java.util.Arrays;
|
|
|
|
/**
|
|
* A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
|
|
* <p>
|
|
* By default, each item occupies 1 span. You can change it by providing a custom
|
|
* {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
|
|
*/
|
|
public class GridLayoutManager extends LinearLayoutManager {
|
|
|
|
private static final boolean DEBUG = false;
|
|
private static final String TAG = "GridLayoutManager";
|
|
public static final int DEFAULT_SPAN_COUNT = -1;
|
|
/**
|
|
* The measure spec for the scroll direction.
|
|
*/
|
|
static final int MAIN_DIR_SPEC =
|
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
|
/**
|
|
* Span size have been changed but we've not done a new layout calculation.
|
|
*/
|
|
boolean mPendingSpanCountChange = false;
|
|
int mSpanCount = DEFAULT_SPAN_COUNT;
|
|
/**
|
|
* Right borders for each span.
|
|
* <p>For <b>i-th</b> item start is {@link #mCachedBorders}[i-1] + 1
|
|
* and end is {@link #mCachedBorders}[i].
|
|
*/
|
|
int [] mCachedBorders;
|
|
/**
|
|
* Temporary array to keep views in layoutChunk method
|
|
*/
|
|
View[] mSet;
|
|
final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray();
|
|
final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray();
|
|
SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
|
|
// re-used variable to acquire decor insets from RecyclerView
|
|
final Rect mDecorInsets = new Rect();
|
|
|
|
/**
|
|
* Creates a vertical GridLayoutManager
|
|
*
|
|
* @param context Current context, will be used to access resources.
|
|
* @param spanCount The number of columns in the grid
|
|
*/
|
|
public GridLayoutManager(Context context, int spanCount) {
|
|
super(context);
|
|
setSpanCount(spanCount);
|
|
}
|
|
|
|
/**
|
|
* @param context Current context, will be used to access resources.
|
|
* @param spanCount The number of columns or rows in the grid
|
|
* @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
|
|
* #VERTICAL}.
|
|
* @param reverseLayout When set to true, layouts from end to start.
|
|
*/
|
|
public GridLayoutManager(Context context, int spanCount, int orientation,
|
|
boolean reverseLayout) {
|
|
super(context, orientation, reverseLayout);
|
|
setSpanCount(spanCount);
|
|
}
|
|
|
|
/**
|
|
* stackFromEnd is not supported by GridLayoutManager. Consider using
|
|
* {@link #setReverseLayout(boolean)}.
|
|
*/
|
|
@Override
|
|
public void setStackFromEnd(boolean stackFromEnd) {
|
|
if (stackFromEnd) {
|
|
throw new UnsupportedOperationException(
|
|
"GridLayoutManager does not support stack from end."
|
|
+ " Consider using reverse layout");
|
|
}
|
|
super.setStackFromEnd(false);
|
|
}
|
|
|
|
@Override
|
|
public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
|
|
RecyclerView.State state) {
|
|
if (mOrientation == HORIZONTAL) {
|
|
return mSpanCount;
|
|
}
|
|
if (state.getItemCount() < 1) {
|
|
return 0;
|
|
}
|
|
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
|
|
}
|
|
|
|
@Override
|
|
public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
|
|
RecyclerView.State state) {
|
|
if (mOrientation == VERTICAL) {
|
|
return mSpanCount;
|
|
}
|
|
if (state.getItemCount() < 1) {
|
|
return 0;
|
|
}
|
|
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
|
|
}
|
|
|
|
@Override
|
|
public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
|
|
RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
|
|
ViewGroup.LayoutParams lp = host.getLayoutParams();
|
|
if (!(lp instanceof LayoutParams)) {
|
|
super.onInitializeAccessibilityNodeInfoForItem(host, info);
|
|
return;
|
|
}
|
|
LayoutParams glp = (LayoutParams) lp;
|
|
int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition());
|
|
if (mOrientation == HORIZONTAL) {
|
|
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
|
|
glp.getSpanIndex(), glp.getSpanSize(),
|
|
spanGroupIndex, 1,
|
|
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
|
|
} else { // VERTICAL
|
|
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
|
|
spanGroupIndex , 1,
|
|
glp.getSpanIndex(), glp.getSpanSize(),
|
|
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
|
|
if (state.isPreLayout()) {
|
|
cachePreLayoutSpanMapping();
|
|
}
|
|
super.onLayoutChildren(recycler, state);
|
|
if (DEBUG) {
|
|
validateChildOrder();
|
|
}
|
|
clearPreLayoutSpanMappingCache();
|
|
if (!state.isPreLayout()) {
|
|
mPendingSpanCountChange = false;
|
|
}
|
|
}
|
|
|
|
private void clearPreLayoutSpanMappingCache() {
|
|
mPreLayoutSpanSizeCache.clear();
|
|
mPreLayoutSpanIndexCache.clear();
|
|
}
|
|
|
|
private void cachePreLayoutSpanMapping() {
|
|
final int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
|
|
final int viewPosition = lp.getViewLayoutPosition();
|
|
mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize());
|
|
mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
|
|
mSpanSizeLookup.invalidateSpanIndexCache();
|
|
}
|
|
|
|
@Override
|
|
public void onItemsChanged(RecyclerView recyclerView) {
|
|
mSpanSizeLookup.invalidateSpanIndexCache();
|
|
}
|
|
|
|
@Override
|
|
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
|
|
mSpanSizeLookup.invalidateSpanIndexCache();
|
|
}
|
|
|
|
@Override
|
|
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
|
|
Object payload) {
|
|
mSpanSizeLookup.invalidateSpanIndexCache();
|
|
}
|
|
|
|
@Override
|
|
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
|
|
mSpanSizeLookup.invalidateSpanIndexCache();
|
|
}
|
|
|
|
@Override
|
|
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
|
|
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Sets the source to get the number of spans occupied by each item in the adapter.
|
|
*
|
|
* @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
|
|
* occupied by each item
|
|
*/
|
|
public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
|
|
mSpanSizeLookup = spanSizeLookup;
|
|
}
|
|
|
|
/**
|
|
* Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
|
|
*
|
|
* @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
|
|
*/
|
|
public SpanSizeLookup getSpanSizeLookup() {
|
|
return mSpanSizeLookup;
|
|
}
|
|
|
|
private void updateMeasurements() {
|
|
int totalSpace;
|
|
if (getOrientation() == VERTICAL) {
|
|
totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
|
|
} else {
|
|
totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
|
|
}
|
|
calculateItemBorders(totalSpace);
|
|
}
|
|
|
|
private void calculateItemBorders(int totalSpace) {
|
|
if (mCachedBorders == null || mCachedBorders.length != mSpanCount + 1
|
|
|| mCachedBorders[mCachedBorders.length - 1] != totalSpace) {
|
|
mCachedBorders = new int[mSpanCount + 1];
|
|
}
|
|
mCachedBorders[0] = 0;
|
|
int sizePerSpan = totalSpace / mSpanCount;
|
|
int sizePerSpanRemainder = totalSpace % mSpanCount;
|
|
int consumedPixels = 0;
|
|
int additionalSize = 0;
|
|
for (int i = 1; i <= mSpanCount; i++) {
|
|
int itemSize = sizePerSpan;
|
|
additionalSize += sizePerSpanRemainder;
|
|
if (additionalSize > 0 && (mSpanCount - additionalSize) < sizePerSpanRemainder) {
|
|
itemSize += 1;
|
|
additionalSize -= mSpanCount;
|
|
}
|
|
consumedPixels += itemSize;
|
|
mCachedBorders[i] = consumedPixels;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state,
|
|
AnchorInfo anchorInfo) {
|
|
super.onAnchorReady(recycler, state, anchorInfo);
|
|
updateMeasurements();
|
|
if (state.getItemCount() > 0 && !state.isPreLayout()) {
|
|
ensureAnchorIsInFirstSpan(recycler, state, anchorInfo);
|
|
}
|
|
if (mSet == null || mSet.length != mSpanCount) {
|
|
mSet = new View[mSpanCount];
|
|
}
|
|
}
|
|
|
|
private void ensureAnchorIsInFirstSpan(RecyclerView.Recycler recycler, RecyclerView.State state,
|
|
AnchorInfo anchorInfo) {
|
|
int span = getSpanIndex(recycler, state, anchorInfo.mPosition);
|
|
while (span > 0 && anchorInfo.mPosition > 0) {
|
|
anchorInfo.mPosition--;
|
|
span = getSpanIndex(recycler, state, anchorInfo.mPosition);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state,
|
|
int start, int end, int itemCount) {
|
|
ensureLayoutState();
|
|
View invalidMatch = null;
|
|
View outOfBoundsMatch = null;
|
|
final int boundsStart = mOrientationHelper.getStartAfterPadding();
|
|
final int boundsEnd = mOrientationHelper.getEndAfterPadding();
|
|
final int diff = end > start ? 1 : -1;
|
|
for (int i = start; i != end; i += diff) {
|
|
final View view = getChildAt(i);
|
|
final int position = getPosition(view);
|
|
if (position >= 0 && position < itemCount) {
|
|
final int span = getSpanIndex(recycler, state, position);
|
|
if (span != 0) {
|
|
continue;
|
|
}
|
|
if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) {
|
|
if (invalidMatch == null) {
|
|
invalidMatch = view; // removed item, least preferred
|
|
}
|
|
} else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd ||
|
|
mOrientationHelper.getDecoratedEnd(view) < boundsStart) {
|
|
if (outOfBoundsMatch == null) {
|
|
outOfBoundsMatch = view; // item is not visible, less preferred
|
|
}
|
|
} else {
|
|
return view;
|
|
}
|
|
}
|
|
}
|
|
return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch;
|
|
}
|
|
|
|
private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state,
|
|
int viewPosition) {
|
|
if (!state.isPreLayout()) {
|
|
return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount);
|
|
}
|
|
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition);
|
|
if (adapterPosition == -1) {
|
|
if (DEBUG) {
|
|
throw new RuntimeException("Cannot find span group index for position "
|
|
+ viewPosition);
|
|
}
|
|
Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition);
|
|
return 0;
|
|
}
|
|
return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount);
|
|
}
|
|
|
|
private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
|
|
if (!state.isPreLayout()) {
|
|
return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount);
|
|
}
|
|
final int cached = mPreLayoutSpanIndexCache.get(pos, -1);
|
|
if (cached != -1) {
|
|
return cached;
|
|
}
|
|
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
|
|
if (adapterPosition == -1) {
|
|
if (DEBUG) {
|
|
throw new RuntimeException("Cannot find span index for pre layout position. It is"
|
|
+ " not cached, not in the adapter. Pos:" + pos);
|
|
}
|
|
Log.w(TAG, "Cannot find span size for pre layout position. It is"
|
|
+ " not cached, not in the adapter. Pos:" + pos);
|
|
return 0;
|
|
}
|
|
return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount);
|
|
}
|
|
|
|
private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
|
|
if (!state.isPreLayout()) {
|
|
return mSpanSizeLookup.getSpanSize(pos);
|
|
}
|
|
final int cached = mPreLayoutSpanSizeCache.get(pos, -1);
|
|
if (cached != -1) {
|
|
return cached;
|
|
}
|
|
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
|
|
if (adapterPosition == -1) {
|
|
if (DEBUG) {
|
|
throw new RuntimeException("Cannot find span size for pre layout position. It is"
|
|
+ " not cached, not in the adapter. Pos:" + pos);
|
|
}
|
|
Log.w(TAG, "Cannot find span size for pre layout position. It is"
|
|
+ " not cached, not in the adapter. Pos:" + pos);
|
|
return 1;
|
|
}
|
|
return mSpanSizeLookup.getSpanSize(adapterPosition);
|
|
}
|
|
|
|
@Override
|
|
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
|
|
LayoutState layoutState, LayoutChunkResult result) {
|
|
final boolean layingOutInPrimaryDirection =
|
|
layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
|
|
int count = 0;
|
|
int consumedSpanCount = 0;
|
|
int remainingSpan = mSpanCount;
|
|
if (!layingOutInPrimaryDirection) {
|
|
int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
|
|
int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
|
|
remainingSpan = itemSpanIndex + itemSpanSize;
|
|
}
|
|
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
|
|
int pos = layoutState.mCurrentPosition;
|
|
final int spanSize = getSpanSize(recycler, state, pos);
|
|
if (spanSize > mSpanCount) {
|
|
throw new IllegalArgumentException("Item at position " + pos + " requires " +
|
|
spanSize + " spans but GridLayoutManager has only " + mSpanCount
|
|
+ " spans.");
|
|
}
|
|
remainingSpan -= spanSize;
|
|
if (remainingSpan < 0) {
|
|
break; // item did not fit into this row or column
|
|
}
|
|
View view = layoutState.next(recycler);
|
|
if (view == null) {
|
|
break;
|
|
}
|
|
consumedSpanCount += spanSize;
|
|
mSet[count] = view;
|
|
count++;
|
|
}
|
|
|
|
if (count == 0) {
|
|
result.mFinished = true;
|
|
return;
|
|
}
|
|
|
|
int maxSize = 0;
|
|
|
|
// we should assign spans before item decor offsets are calculated
|
|
assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
|
|
for (int i = 0; i < count; i++) {
|
|
View view = mSet[i];
|
|
if (layoutState.mScrapList == null) {
|
|
if (layingOutInPrimaryDirection) {
|
|
addView(view);
|
|
} else {
|
|
addView(view, 0);
|
|
}
|
|
} else {
|
|
if (layingOutInPrimaryDirection) {
|
|
addDisappearingView(view);
|
|
} else {
|
|
addDisappearingView(view, 0);
|
|
}
|
|
}
|
|
|
|
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
|
|
final int spec = View.MeasureSpec.makeMeasureSpec(
|
|
mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
|
|
mCachedBorders[lp.mSpanIndex],
|
|
View.MeasureSpec.EXACTLY);
|
|
if (mOrientation == VERTICAL) {
|
|
measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height), false);
|
|
} else {
|
|
measureChildWithDecorationsAndMargin(view, getMainDirSpec(lp.width), spec, false);
|
|
}
|
|
final int size = mOrientationHelper.getDecoratedMeasurement(view);
|
|
if (size > maxSize) {
|
|
maxSize = size;
|
|
}
|
|
}
|
|
|
|
// views that did not measure the maxSize has to be re-measured
|
|
final int maxMeasureSpec = getMainDirSpec(maxSize);
|
|
for (int i = 0; i < count; i ++) {
|
|
final View view = mSet[i];
|
|
if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
|
|
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
|
|
final int spec = View.MeasureSpec.makeMeasureSpec(
|
|
mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
|
|
mCachedBorders[lp.mSpanIndex],
|
|
View.MeasureSpec.EXACTLY);
|
|
if (mOrientation == VERTICAL) {
|
|
measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec, true);
|
|
} else {
|
|
measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
result.mConsumed = maxSize;
|
|
|
|
int left = 0, right = 0, top = 0, bottom = 0;
|
|
if (mOrientation == VERTICAL) {
|
|
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
|
|
bottom = layoutState.mOffset;
|
|
top = bottom - maxSize;
|
|
} else {
|
|
top = layoutState.mOffset;
|
|
bottom = top + maxSize;
|
|
}
|
|
} else {
|
|
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
|
|
right = layoutState.mOffset;
|
|
left = right - maxSize;
|
|
} else {
|
|
left = layoutState.mOffset;
|
|
right = left + maxSize;
|
|
}
|
|
}
|
|
for (int i = 0; i < count; i++) {
|
|
View view = mSet[i];
|
|
LayoutParams params = (LayoutParams) view.getLayoutParams();
|
|
if (mOrientation == VERTICAL) {
|
|
left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
|
|
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
|
|
} else {
|
|
top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
|
|
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
|
|
}
|
|
// We calculate everything with View's bounding box (which includes decor and margins)
|
|
// To calculate correct layout position, we subtract margins.
|
|
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
|
|
right - params.rightMargin, bottom - params.bottomMargin);
|
|
if (DEBUG) {
|
|
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
|
|
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
|
|
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
|
|
+ ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
|
|
}
|
|
// Consume the available space if the view is not removed OR changed
|
|
if (params.isItemRemoved() || params.isItemChanged()) {
|
|
result.mIgnoreConsumed = true;
|
|
}
|
|
result.mFocusable |= view.isFocusable();
|
|
}
|
|
Arrays.fill(mSet, null);
|
|
}
|
|
|
|
private int getMainDirSpec(int dim) {
|
|
if (dim < 0) {
|
|
return MAIN_DIR_SPEC;
|
|
} else {
|
|
return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY);
|
|
}
|
|
}
|
|
|
|
private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec,
|
|
boolean capBothSpecs) {
|
|
calculateItemDecorationsForChild(child, mDecorInsets);
|
|
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
|
|
if (capBothSpecs || mOrientation == VERTICAL) {
|
|
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left,
|
|
lp.rightMargin + mDecorInsets.right);
|
|
}
|
|
if (capBothSpecs || mOrientation == HORIZONTAL) {
|
|
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
|
|
lp.bottomMargin + mDecorInsets.bottom);
|
|
}
|
|
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(
|
|
View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
|
|
}
|
|
return spec;
|
|
}
|
|
|
|
private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
|
|
int consumedSpanCount, boolean layingOutInPrimaryDirection) {
|
|
int span, spanDiff, start, end, diff;
|
|
// make sure we traverse from min position to max position
|
|
if (layingOutInPrimaryDirection) {
|
|
start = 0;
|
|
end = count;
|
|
diff = 1;
|
|
} else {
|
|
start = count - 1;
|
|
end = -1;
|
|
diff = -1;
|
|
}
|
|
if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span
|
|
span = mSpanCount - 1;
|
|
spanDiff = -1;
|
|
} else {
|
|
span = 0;
|
|
spanDiff = 1;
|
|
}
|
|
for (int i = start; i != end; i += diff) {
|
|
View view = mSet[i];
|
|
LayoutParams params = (LayoutParams) view.getLayoutParams();
|
|
params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
|
|
if (spanDiff == -1 && params.mSpanSize > 1) {
|
|
params.mSpanIndex = span - (params.mSpanSize - 1);
|
|
} else {
|
|
params.mSpanIndex = span;
|
|
}
|
|
span += spanDiff * params.mSpanSize;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the number of spans laid out by this grid.
|
|
*
|
|
* @return The number of spans
|
|
* @see #setSpanCount(int)
|
|
*/
|
|
public int getSpanCount() {
|
|
return mSpanCount;
|
|
}
|
|
|
|
/**
|
|
* Sets the number of spans to be laid out.
|
|
* <p>
|
|
* If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
|
|
* If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
|
|
*
|
|
* @param spanCount The total number of spans in the grid
|
|
* @see #getSpanCount()
|
|
*/
|
|
public void setSpanCount(int spanCount) {
|
|
if (spanCount == mSpanCount) {
|
|
return;
|
|
}
|
|
mPendingSpanCountChange = true;
|
|
if (spanCount < 1) {
|
|
throw new IllegalArgumentException("Span count should be at least 1. Provided "
|
|
+ spanCount);
|
|
}
|
|
mSpanCount = spanCount;
|
|
mSpanSizeLookup.invalidateSpanIndexCache();
|
|
}
|
|
|
|
/**
|
|
* A helper class to provide the number of spans each item occupies.
|
|
* <p>
|
|
* Default implementation sets each item to occupy exactly 1 span.
|
|
*
|
|
* @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
|
|
*/
|
|
public static abstract class SpanSizeLookup {
|
|
|
|
final SparseIntArray mSpanIndexCache = new SparseIntArray();
|
|
|
|
private boolean mCacheSpanIndices = false;
|
|
|
|
/**
|
|
* Returns the number of span occupied by the item at <code>position</code>.
|
|
*
|
|
* @param position The adapter position of the item
|
|
* @return The number of spans occupied by the item at the provided position
|
|
*/
|
|
abstract public int getSpanSize(int position);
|
|
|
|
/**
|
|
* Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or
|
|
* not. By default these values are not cached. If you are not overriding
|
|
* {@link #getSpanIndex(int, int)}, you should set this to true for better performance.
|
|
*
|
|
* @param cacheSpanIndices Whether results of getSpanIndex should be cached or not.
|
|
*/
|
|
public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) {
|
|
mCacheSpanIndices = cacheSpanIndices;
|
|
}
|
|
|
|
/**
|
|
* Clears the span index cache. GridLayoutManager automatically calls this method when
|
|
* adapter changes occur.
|
|
*/
|
|
public void invalidateSpanIndexCache() {
|
|
mSpanIndexCache.clear();
|
|
}
|
|
|
|
/**
|
|
* Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not.
|
|
*
|
|
* @return True if results of {@link #getSpanIndex(int, int)} are cached.
|
|
*/
|
|
public boolean isSpanIndexCacheEnabled() {
|
|
return mCacheSpanIndices;
|
|
}
|
|
|
|
int getCachedSpanIndex(int position, int spanCount) {
|
|
if (!mCacheSpanIndices) {
|
|
return getSpanIndex(position, spanCount);
|
|
}
|
|
final int existing = mSpanIndexCache.get(position, -1);
|
|
if (existing != -1) {
|
|
return existing;
|
|
}
|
|
final int value = getSpanIndex(position, spanCount);
|
|
mSpanIndexCache.put(position, value);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Returns the final span index of the provided position.
|
|
* <p>
|
|
* If you have a faster way to calculate span index for your items, you should override
|
|
* this method. Otherwise, you should enable span index cache
|
|
* ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is
|
|
* disabled, default implementation traverses all items from 0 to
|
|
* <code>position</code>. When caching is enabled, it calculates from the closest cached
|
|
* value before the <code>position</code>.
|
|
* <p>
|
|
* If you override this method, you need to make sure it is consistent with
|
|
* {@link #getSpanSize(int)}. GridLayoutManager does not call this method for
|
|
* each item. It is called only for the reference item and rest of the items
|
|
* are assigned to spans based on the reference item. For example, you cannot assign a
|
|
* position to span 2 while span 1 is empty.
|
|
* <p>
|
|
* Note that span offsets always start with 0 and are not affected by RTL.
|
|
*
|
|
* @param position The position of the item
|
|
* @param spanCount The total number of spans in the grid
|
|
* @return The final span position of the item. Should be between 0 (inclusive) and
|
|
* <code>spanCount</code>(exclusive)
|
|
*/
|
|
public int getSpanIndex(int position, int spanCount) {
|
|
int positionSpanSize = getSpanSize(position);
|
|
if (positionSpanSize == spanCount) {
|
|
return 0; // quick return for full-span items
|
|
}
|
|
int span = 0;
|
|
int startPos = 0;
|
|
// If caching is enabled, try to jump
|
|
if (mCacheSpanIndices && mSpanIndexCache.size() > 0) {
|
|
int prevKey = findReferenceIndexFromCache(position);
|
|
if (prevKey >= 0) {
|
|
span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey);
|
|
startPos = prevKey + 1;
|
|
}
|
|
}
|
|
for (int i = startPos; i < position; i++) {
|
|
int size = getSpanSize(i);
|
|
span += size;
|
|
if (span == spanCount) {
|
|
span = 0;
|
|
} else if (span > spanCount) {
|
|
// did not fit, moving to next row / column
|
|
span = size;
|
|
}
|
|
}
|
|
if (span + positionSpanSize <= spanCount) {
|
|
return span;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int findReferenceIndexFromCache(int position) {
|
|
int lo = 0;
|
|
int hi = mSpanIndexCache.size() - 1;
|
|
|
|
while (lo <= hi) {
|
|
final int mid = (lo + hi) >>> 1;
|
|
final int midVal = mSpanIndexCache.keyAt(mid);
|
|
if (midVal < position) {
|
|
lo = mid + 1;
|
|
} else {
|
|
hi = mid - 1;
|
|
}
|
|
}
|
|
int index = lo - 1;
|
|
if (index >= 0 && index < mSpanIndexCache.size()) {
|
|
return mSpanIndexCache.keyAt(index);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the group this position belongs.
|
|
* <p>
|
|
* For example, if grid has 3 columns and each item occupies 1 span, span group index
|
|
* for item 1 will be 0, item 5 will be 1.
|
|
*
|
|
* @param adapterPosition The position in adapter
|
|
* @param spanCount The total number of spans in the grid
|
|
* @return The index of the span group including the item at the given adapter position
|
|
*/
|
|
public int getSpanGroupIndex(int adapterPosition, int spanCount) {
|
|
int span = 0;
|
|
int group = 0;
|
|
int positionSpanSize = getSpanSize(adapterPosition);
|
|
for (int i = 0; i < adapterPosition; i++) {
|
|
int size = getSpanSize(i);
|
|
span += size;
|
|
if (span == spanCount) {
|
|
span = 0;
|
|
group++;
|
|
} else if (span > spanCount) {
|
|
// did not fit, moving to next row / column
|
|
span = size;
|
|
group++;
|
|
}
|
|
}
|
|
if (span + positionSpanSize > spanCount) {
|
|
group++;
|
|
}
|
|
return group;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean supportsPredictiveItemAnimations() {
|
|
return mPendingSavedState == null && !mPendingSpanCountChange;
|
|
}
|
|
|
|
/**
|
|
* Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
|
|
*/
|
|
public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
|
|
|
|
@Override
|
|
public int getSpanSize(int position) {
|
|
return 1;
|
|
}
|
|
|
|
@Override
|
|
public int getSpanIndex(int position, int spanCount) {
|
|
return position % spanCount;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* LayoutParams used by GridLayoutManager.
|
|
* <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;
|
|
|
|
private int mSpanIndex = INVALID_SPAN_ID;
|
|
|
|
private int mSpanSize = 0;
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Returns the current span index of this View. If the View is not laid out yet, the return
|
|
* value is <code>undefined</code>.
|
|
* <p>
|
|
* Note that span index may change by whether the RecyclerView is RTL or not. For
|
|
* example, if the number of spans is 3 and layout is RTL, the rightmost item will have
|
|
* span index of 2. If the layout changes back to LTR, span index for this view will be 0.
|
|
* If the item was occupying 2 spans, span indices would be 1 and 0 respectively.
|
|
* <p>
|
|
* If the View occupies multiple spans, span with the minimum index is returned.
|
|
*
|
|
* @return The span index of the View.
|
|
*/
|
|
public int getSpanIndex() {
|
|
return mSpanIndex;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of spans occupied by this View. If the View not laid out yet, the
|
|
* return value is <code>undefined</code>.
|
|
*
|
|
* @return The number of spans occupied by this View.
|
|
*/
|
|
public int getSpanSize() {
|
|
return mSpanSize;
|
|
}
|
|
}
|
|
|
|
}
|