| /* |
| * 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 android.support.v7.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(); |
| |
| |
| /** |
| * Constructor used when layout manager is set in XML by RecyclerView attribute |
| * "layoutManager". If spanCount is not specified in the XML, it defaults to a |
| * single column. |
| * |
| * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount |
| */ |
| public GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, |
| int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); |
| setSpanCount(properties.spanCount); |
| } |
| |
| /** |
| * 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); |
| } |
| ensureViewSet(); |
| } |
| |
| private void ensureViewSet() { |
| if (mSet == null || mSet.length != mSpanCount) { |
| mSet = new View[mSpanCount]; |
| } |
| } |
| |
| @Override |
| public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| updateMeasurements(); |
| ensureViewSet(); |
| return super.scrollHorizontallyBy(dx, recycler, state); |
| } |
| |
| @Override |
| public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, |
| RecyclerView.State state) { |
| updateMeasurements(); |
| ensureViewSet(); |
| return super.scrollVerticallyBy(dy, recycler, state); |
| } |
| |
| 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; |
| } |
| } |
| |
| } |