Signal-Android/sticky-header-grid/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManag...

1369 wiersze
46 KiB
Java

package com.codewaves.stickyheadergrid;
import android.content.Context;
import android.graphics.PointF;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Arrays;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static com.codewaves.stickyheadergrid.StickyHeaderGridAdapter.TYPE_HEADER;
import static com.codewaves.stickyheadergrid.StickyHeaderGridAdapter.TYPE_ITEM;
/**
* Created by Sergej Kravcenko on 4/24/2017.
* Copyright (c) 2017 Sergej Kravcenko
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public class StickyHeaderGridLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider {
public static final String TAG = "StickyLayoutManager";
private static final int DEFAULT_ROW_COUNT = 16;
private int mSpanCount;
private SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
private StickyHeaderGridAdapter mAdapter;
private int mHeadersStartPosition;
private View mFloatingHeaderView;
private int mFloatingHeaderPosition;
private int mStickOffset;
private int mAverageHeaderHeight;
private int mHeaderOverlapMargin;
private HeaderStateChangeListener mHeaderStateListener;
private int mStickyHeaderSection = NO_POSITION;
private View mStickyHeaderView;
private HeaderState mStickyHeadeState;
private View mFillViewSet[];
private SavedState mPendingSavedState;
private int mPendingScrollPosition = NO_POSITION;
private int mPendingScrollPositionOffset;
private AnchorPosition mAnchor = new AnchorPosition();
private final FillResult mFillResult = new FillResult();
private ArrayList<LayoutRow> mLayoutRows = new ArrayList<>(DEFAULT_ROW_COUNT);
public enum HeaderState {
NORMAL,
STICKY,
PUSHED
}
/**
* The interface to be implemented by listeners to header events from this
* LayoutManager.
*/
public interface HeaderStateChangeListener {
/**
* Called when a section header state changes. The position can be HeaderState.NORMAL,
* HeaderState.STICKY, HeaderState.PUSHED.
*
* <p>
* <ul>
* <li>NORMAL - the section header is invisible or has normal position</li>
* <li>STICKY - the section header is sticky at the top of RecyclerView</li>
* <li>PUSHED - the section header is sticky and pushed up by next header</li>
* </ul
*
* @param section the section index
* @param headerView the header view, can be null if header is out of screen
* @param state the new state of the header (NORMAL, STICKY or PUSHED)
* @param pushOffset the distance over which section header is pushed up
*/
void onHeaderStateChanged(int section, View headerView, HeaderState state, int pushOffset);
}
/**
* Creates a vertical StickyHeaderGridLayoutManager
*
* @param spanCount The number of columns in the grid
*/
public StickyHeaderGridLayoutManager(int spanCount) {
mSpanCount = spanCount;
mFillViewSet = new View[spanCount];
mHeaderOverlapMargin = 0;
if (spanCount < 1) {
throw new IllegalArgumentException("Span count should be at least 1. Provided " + spanCount);
}
}
/**
* Sets the source to get the number of spans occupied by each item in the adapter.
*
* @param spanSizeLookup {@link StickyHeaderGridLayoutManager.SpanSizeLookup} instance to be used to query number of spans
* occupied by each item
*/
public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
mSpanSizeLookup = spanSizeLookup;
if (mSpanSizeLookup == null) {
mSpanSizeLookup = new DefaultSpanSizeLookup();
}
}
/**
* Returns the current {@link StickyHeaderGridLayoutManager.SpanSizeLookup} used by the StickyHeaderGridLayoutManager.
*
* @return The current {@link StickyHeaderGridLayoutManager.SpanSizeLookup} used by the StickyHeaderGridLayoutManager.
*/
public SpanSizeLookup getSpanSizeLookup() {
return mSpanSizeLookup;
}
/**
* Returns the current {@link StickyHeaderGridLayoutManager.HeaderStateChangeListener} used by the StickyHeaderGridLayoutManager.
*
* @return The current {@link StickyHeaderGridLayoutManager.HeaderStateChangeListener} used by the StickyHeaderGridLayoutManager.
*/
public HeaderStateChangeListener getHeaderStateChangeListener() {
return mHeaderStateListener;
}
/**
* Sets the listener to receive header state changes.
*
* @param listener {@link StickyHeaderGridLayoutManager.HeaderStateChangeListener} instance to be used to receive header
* state changes
*/
public void setHeaderStateChangeListener(HeaderStateChangeListener listener) {
mHeaderStateListener = listener;
}
/**
* Sets the size of header bottom margin that overlaps first section item. Used to create header bottom edge shadows.
*
* @param bottomMargin Size of header bottom margin in pixels
*
*/
public void setHeaderBottomOverlapMargin(int bottomMargin) {
mHeaderOverlapMargin = bottomMargin;
}
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
super.onAdapterChanged(oldAdapter, newAdapter);
try {
mAdapter = (StickyHeaderGridAdapter)newAdapter;
}
catch (ClassCastException e) {
throw new ClassCastException("Adapter used with StickyHeaderGridLayoutManager must be kind of StickyHeaderGridAdapter");
}
removeAllViews();
clearState();
}
@Override
public void onAttachedToWindow(RecyclerView view) {
super.onAttachedToWindow(view);
try {
mAdapter = (StickyHeaderGridAdapter)view.getAdapter();
}
catch (ClassCastException e) {
throw new ClassCastException("Adapter used with StickyHeaderGridLayoutManager must be kind of StickyHeaderGridAdapter");
}
}
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
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 Parcelable onSaveInstanceState() {
if (mPendingSavedState != null) {
return new SavedState(mPendingSavedState);
}
SavedState state = new SavedState();
if (getChildCount() > 0) {
state.mAnchorSection = mAnchor.section;
state.mAnchorItem = mAnchor.item;
state.mAnchorOffset = mAnchor.offset;
}
else {
state.invalidateAnchor();
}
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof SavedState) {
mPendingSavedState = (SavedState) state;
requestLayout();
}
}
@Override
public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
return lp instanceof LayoutParams;
}
@Override
public boolean canScrollVertically() {
return true;
}
/**
* <p>Scroll the RecyclerView to make the position visible.</p>
*
* <p>RecyclerView will scroll the minimum amount that is necessary to make the
* target position visible.
*
* <p>Note that scroll position change will not be reflected until the next layout call.</p>
*
* @param position Scroll to this adapter position
*/
@Override
public void scrollToPosition(int position) {
if (position < 0 || position > getItemCount()) {
throw new IndexOutOfBoundsException("adapter position out of range");
}
mPendingScrollPosition = position;
mPendingScrollPositionOffset = 0;
if (mPendingSavedState != null) {
mPendingSavedState.invalidateAnchor();
}
requestLayout();
}
private int getExtraLayoutSpace(RecyclerView.State state) {
if (state.hasTargetScrollPosition()) {
return getHeight();
}
else {
return 0;
}
}
@Override
public void smoothScrollToPosition(final RecyclerView recyclerView, RecyclerView.State state, int position) {
final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
@Override
public int calculateDyToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollVertically()) {
return 0;
}
final int adapterPosition = getPosition(view);
final int topOffset = getPositionSectionHeaderHeight(adapterPosition);
final int top = layoutManager.getDecoratedTop(view);
final int bottom = layoutManager.getDecoratedBottom(view);
final int start = layoutManager.getPaddingTop() + topOffset;
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
return calculateDtToFit(top, bottom, start, end, snapPreference);
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
if (getChildCount() == 0) {
return null;
}
final LayoutRow firstRow = getFirstVisibleRow();
if (firstRow == null) {
return null;
}
return new PointF(0, targetPosition - firstRow.adapterPosition);
}
private int getAdapterPositionFromAnchor(AnchorPosition anchor) {
if (anchor.section < 0 || anchor.section >= mAdapter.getSectionCount()) {
anchor.reset();
return NO_POSITION;
}
else if (anchor.item < 0 || anchor.item >= mAdapter.getSectionItemCount(anchor.section)) {
anchor.offset = 0;
return mAdapter.getSectionHeaderPosition(anchor.section);
}
return mAdapter.getSectionItemPosition(anchor.section, anchor.item);
}
private int getAdapterPositionChecked(int section, int offset) {
if (section < 0 || section >= mAdapter.getSectionCount()) {
return NO_POSITION;
}
else if (offset < 0 || offset >= mAdapter.getSectionItemCount(section)) {
return mAdapter.getSectionHeaderPosition(section);
}
return mAdapter.getSectionItemPosition(section, offset);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mAdapter == null || state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
clearState();
return;
}
int pendingAdapterPosition;
int pendingAdapterOffset;
if (mPendingScrollPosition >= 0) {
pendingAdapterPosition = mPendingScrollPosition;
pendingAdapterOffset = mPendingScrollPositionOffset;
}
else if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
pendingAdapterPosition = getAdapterPositionChecked(mPendingSavedState.mAnchorSection, mPendingSavedState.mAnchorItem);
pendingAdapterOffset = mPendingSavedState.mAnchorOffset;
mPendingSavedState = null;
}
else {
pendingAdapterPosition = getAdapterPositionFromAnchor(mAnchor);
pendingAdapterOffset = mAnchor.offset;
}
if (pendingAdapterPosition < 0 || pendingAdapterPosition >= state.getItemCount()) {
pendingAdapterPosition = 0;
pendingAdapterOffset = 0;
mPendingScrollPosition = NO_POSITION;
}
if (pendingAdapterOffset > 0) {
pendingAdapterOffset = 0;
}
detachAndScrapAttachedViews(recycler);
clearState();
// Make sure mFirstViewPosition is the start of the row
pendingAdapterPosition = findFirstRowItem(pendingAdapterPosition);
int left = getPaddingLeft();
int right = getWidth() - getPaddingRight();
final int recyclerBottom = getHeight() - getPaddingBottom();
int totalHeight = 0;
int adapterPosition = pendingAdapterPosition;
int top = getPaddingTop() + pendingAdapterOffset;
while (true) {
if (adapterPosition >= state.getItemCount()) {
break;
}
int bottom;
final int viewType = mAdapter.getItemViewInternalType(adapterPosition);
if (viewType == TYPE_HEADER) {
final View v = recycler.getViewForPosition(adapterPosition);
addView(v);
measureChildWithMargins(v, 0, 0);
int height = getDecoratedMeasuredHeight(v);
final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height;
bottom = top + height;
layoutDecorated(v, left, top, right, bottom);
bottom -= margin;
height -= margin;
mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, bottom));
adapterPosition++;
mAverageHeaderHeight = height;
}
else {
final FillResult result = fillBottomRow(recycler, state, adapterPosition, top);
bottom = top + result.height;
mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, bottom));
adapterPosition += result.length;
}
top = bottom;
if (bottom >= recyclerBottom + getExtraLayoutSpace(state)) {
break;
}
}
if (getBottomRow().bottom < recyclerBottom) {
scrollVerticallyBy(getBottomRow().bottom - recyclerBottom, recycler, state);
}
else {
clearViewsAndStickHeaders(recycler, state, false);
}
// If layout was caused by the pending scroll, adjust top item position and move it under sticky header
if (mPendingScrollPosition >= 0) {
mPendingScrollPosition = NO_POSITION;
final int topOffset = getPositionSectionHeaderHeight(pendingAdapterPosition);
if (topOffset != 0) {
scrollVerticallyBy(-topOffset, recycler, state);
}
}
}
@Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
mPendingSavedState = null;
}
private int getPositionSectionHeaderHeight(int adapterPosition) {
final int section = mAdapter.getAdapterPositionSection(adapterPosition);
if (section >= 0 && mAdapter.isSectionHeaderSticky(section)) {
final int offset = mAdapter.getItemSectionOffset(section, adapterPosition);
if (offset >= 0) {
final int headerAdapterPosition = mAdapter.getSectionHeaderPosition(section);
if (mFloatingHeaderView != null && headerAdapterPosition == mFloatingHeaderPosition) {
return Math.max(0, getDecoratedMeasuredHeight(mFloatingHeaderView) - mHeaderOverlapMargin);
}
else {
final LayoutRow header = getHeaderRow(headerAdapterPosition);
if (header != null) {
return header.getHeight();
}
else {
// Fall back to cached header size, can be incorrect
return mAverageHeaderHeight;
}
}
}
}
return 0;
}
private int findFirstRowItem(int adapterPosition) {
final int section = mAdapter.getAdapterPositionSection(adapterPosition);
int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition);
while (sectionPosition > 0 && mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount) != 0) {
sectionPosition--;
adapterPosition--;
}
return adapterPosition;
}
private int getSpanWidth(int recyclerWidth, int spanIndex, int spanSize) {
final int spanWidth = recyclerWidth / mSpanCount;
final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount;
final int widthCorrection = Math.min(Math.max(0, spanWidthReminder - spanIndex), spanSize);
return spanWidth * spanSize + widthCorrection;
}
private int getSpanLeft(int recyclerWidth, int spanIndex) {
final int spanWidth = recyclerWidth / mSpanCount;
final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount;
final int widthCorrection = Math.min(spanWidthReminder, spanIndex);
return spanWidth * spanIndex + widthCorrection;
}
private FillResult fillBottomRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) {
final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight();
final int section = mAdapter.getAdapterPositionSection(position);
int adapterPosition = position;
int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition);
int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition);
int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount);
int count = 0;
int maxHeight = 0;
// Create phase
Arrays.fill(mFillViewSet, null);
while (spanIndex + spanSize <= mSpanCount) {
// Create view and fill layout params
final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize);
final View v = recycler.getViewForPosition(adapterPosition);
final LayoutParams params = (LayoutParams)v.getLayoutParams();
params.mSpanIndex = spanIndex;
params.mSpanSize = spanSize;
addView(v, mHeadersStartPosition);
mHeadersStartPosition++;
measureChildWithMargins(v, recyclerWidth - spanWidth, 0);
mFillViewSet[count] = v;
count++;
final int height = getDecoratedMeasuredHeight(v);
if (maxHeight < height) {
maxHeight = height;
}
// Check next
adapterPosition++;
sectionPosition++;
if (sectionPosition >= mAdapter.getSectionItemCount(section)) {
break;
}
spanIndex += spanSize;
spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition);
}
// Layout phase
int left = getPaddingLeft();
for (int i = 0; i < count; ++i) {
final View v = mFillViewSet[i];
final int height = getDecoratedMeasuredHeight(v);
final int width = getDecoratedMeasuredWidth(v);
layoutDecorated(v, left, top, left + width, top + height);
left += width;
}
mFillResult.edgeView = mFillViewSet[count - 1];
mFillResult.adapterPosition = position;
mFillResult.length = count;
mFillResult.height = maxHeight;
return mFillResult;
}
private FillResult fillTopRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) {
final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight();
final int section = mAdapter.getAdapterPositionSection(position);
int adapterPosition = position;
int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition);
int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition);
int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount);
int count = 0;
int maxHeight = 0;
Arrays.fill(mFillViewSet, null);
while (spanIndex >= 0) {
// Create view and fill layout params
final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize);
final View v = recycler.getViewForPosition(adapterPosition);
final LayoutParams params = (LayoutParams)v.getLayoutParams();
params.mSpanIndex = spanIndex;
params.mSpanSize = spanSize;
addView(v, 0);
mHeadersStartPosition++;
measureChildWithMargins(v, recyclerWidth - spanWidth, 0);
mFillViewSet[count] = v;
count++;
final int height = getDecoratedMeasuredHeight(v);
if (maxHeight < height) {
maxHeight = height;
}
// Check next
adapterPosition--;
sectionPosition--;
if (sectionPosition < 0) {
break;
}
spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition);
spanIndex -= spanSize;
}
// Layout phase
int left = getPaddingLeft();
for (int i = count - 1; i >= 0; --i) {
final View v = mFillViewSet[i];
final int height = getDecoratedMeasuredHeight(v);
final int width = getDecoratedMeasuredWidth(v);
layoutDecorated(v, left, top - maxHeight, left + width, top - (maxHeight - height));
left += width;
}
mFillResult.edgeView = mFillViewSet[count - 1];
mFillResult.adapterPosition = adapterPosition + 1;
mFillResult.length = count;
mFillResult.height = maxHeight;
return mFillResult;
}
private void clearHiddenRows(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) {
if (mLayoutRows.size() <= 0) {
return;
}
final int recyclerTop = getPaddingTop();
final int recyclerBottom = getHeight() - getPaddingBottom();
if (top) {
LayoutRow row = getTopRow();
while (row.bottom < recyclerTop - getExtraLayoutSpace(state) || row.top > recyclerBottom) {
if (row.header) {
removeAndRecycleViewAt(mHeadersStartPosition + (mFloatingHeaderView != null ? 1 : 0), recycler);
}
else {
for (int i = 0; i < row.length; ++i) {
removeAndRecycleViewAt(0, recycler);
mHeadersStartPosition--;
}
}
mLayoutRows.remove(0);
row = getTopRow();
}
}
else {
LayoutRow row = getBottomRow();
while (row.bottom < recyclerTop || row.top > recyclerBottom + getExtraLayoutSpace(state)) {
if (row.header) {
removeAndRecycleViewAt(getChildCount() - 1, recycler);
}
else {
for (int i = 0; i < row.length; ++i) {
removeAndRecycleViewAt(mHeadersStartPosition - 1, recycler);
mHeadersStartPosition--;
}
}
mLayoutRows.remove(mLayoutRows.size() - 1);
row = getBottomRow();
}
}
}
private void clearViewsAndStickHeaders(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) {
clearHiddenRows(recycler, state, top);
if (getChildCount() > 0) {
stickTopHeader(recycler);
}
updateTopPosition();
}
private LayoutRow getBottomRow() {
return mLayoutRows.get(mLayoutRows.size() - 1);
}
private LayoutRow getTopRow() {
return mLayoutRows.get(0);
}
private void offsetRowsVertical(int offset) {
for (LayoutRow row : mLayoutRows) {
row.top += offset;
row.bottom += offset;
}
offsetChildrenVertical(offset);
}
private void addRow(RecyclerView.Recycler recycler, RecyclerView.State state, boolean isTop, int adapterPosition, int top) {
final int left = getPaddingLeft();
final int right = getWidth() - getPaddingRight();
// Reattach floating header if needed
if (isTop && mFloatingHeaderView != null && adapterPosition == mFloatingHeaderPosition) {
removeFloatingHeader(recycler);
}
final int viewType = mAdapter.getItemViewInternalType(adapterPosition);
if (viewType == TYPE_HEADER) {
final View v = recycler.getViewForPosition(adapterPosition);
if (isTop) {
addView(v, mHeadersStartPosition);
}
else {
addView(v);
}
measureChildWithMargins(v, 0, 0);
final int height = getDecoratedMeasuredHeight(v);
final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height;
if (isTop) {
layoutDecorated(v, left, top - height + margin, right, top + margin);
mLayoutRows.add(0, new LayoutRow(v, adapterPosition, 1, top - height + margin, top));
}
else {
layoutDecorated(v, left, top, right, top + height);
mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, top + height - margin));
}
mAverageHeaderHeight = height - margin;
}
else {
if (isTop) {
final FillResult result = fillTopRow(recycler, state, adapterPosition, top);
mLayoutRows.add(0, new LayoutRow(result.adapterPosition, result.length, top - result.height, top));
}
else {
final FillResult result = fillBottomRow(recycler, state, adapterPosition, top);
mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, top + result.height));
}
}
}
private void addOffScreenRows(RecyclerView.Recycler recycler, RecyclerView.State state, int recyclerTop, int recyclerBottom, boolean bottom) {
if (bottom) {
// Bottom
while (true) {
final LayoutRow bottomRow = getBottomRow();
final int adapterPosition = bottomRow.adapterPosition + bottomRow.length;
if (bottomRow.bottom >= recyclerBottom + getExtraLayoutSpace(state) || adapterPosition >= state.getItemCount()) {
break;
}
addRow(recycler, state, false, adapterPosition, bottomRow.bottom);
}
}
else {
// Top
while (true) {
final LayoutRow topRow = getTopRow();
final int adapterPosition = topRow.adapterPosition - 1;
if (topRow.top < recyclerTop - getExtraLayoutSpace(state) || adapterPosition < 0) {
break;
}
addRow(recycler, state, true, adapterPosition, topRow.top);
}
}
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
int scrolled = 0;
int left = getPaddingLeft();
int right = getWidth() - getPaddingRight();
final int recyclerTop = getPaddingTop();
final int recyclerBottom = getHeight() - getPaddingBottom();
// If we have simple header stick, offset it back
final int firstHeader = getFirstVisibleSectionHeader();
if (firstHeader != NO_POSITION) {
mLayoutRows.get(firstHeader).headerView.offsetTopAndBottom(-mStickOffset);
}
if (dy >= 0) {
// Up
while (scrolled < dy) {
final LayoutRow bottomRow = getBottomRow();
final int scrollChunk = -Math.min(Math.max(bottomRow.bottom - recyclerBottom, 0), dy - scrolled);
offsetRowsVertical(scrollChunk);
scrolled -= scrollChunk;
final int adapterPosition = bottomRow.adapterPosition + bottomRow.length;
if (scrolled >= dy || adapterPosition >= state.getItemCount()) {
break;
}
addRow(recycler, state, false, adapterPosition, bottomRow.bottom);
}
}
else {
// Down
while (scrolled > dy) {
final LayoutRow topRow = getTopRow();
final int scrollChunk = Math.min(Math.max(-topRow.top + recyclerTop, 0), scrolled - dy);
offsetRowsVertical(scrollChunk);
scrolled -= scrollChunk;
final int adapterPosition = topRow.adapterPosition - 1;
if (scrolled <= dy || adapterPosition >= state.getItemCount() || adapterPosition < 0) {
break;
}
addRow(recycler, state, true, adapterPosition, topRow.top);
}
}
// Fill extra offscreen rows for smooth scroll
if (scrolled == dy) {
addOffScreenRows(recycler, state, recyclerTop, recyclerBottom, dy >= 0);
}
clearViewsAndStickHeaders(recycler, state, dy >= 0);
return scrolled;
}
/**
* Returns first visible item excluding headers.
*
* @param visibleTop Whether item top edge should be visible or not
* @return The first visible item adapter position closest to top of the layout.
*/
public int getFirstVisibleItemPosition(boolean visibleTop) {
return getFirstVisiblePosition(TYPE_ITEM, visibleTop);
}
/**
* Returns last visible item excluding headers.
*
* @return The last visible item adapter position closest to bottom of the layout.
*/
public int getLastVisibleItemPosition() {
return getLastVisiblePosition(TYPE_ITEM);
}
/**
* Returns first visible header.
*
* @param visibleTop Whether header top edge should be visible or not
* @return The first visible header adapter position closest to top of the layout.
*/
public int getFirstVisibleHeaderPosition(boolean visibleTop) {
return getFirstVisiblePosition(TYPE_HEADER, visibleTop);
}
/**
* Returns last visible header.
*
* @return The last visible header adapter position closest to bottom of the layout.
*/
public int getLastVisibleHeaderPosition() {
return getLastVisiblePosition(TYPE_HEADER);
}
private int getFirstVisiblePosition(int type, boolean visibleTop) {
if (type == TYPE_ITEM && mHeadersStartPosition <= 0) {
return NO_POSITION;
}
else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) {
return NO_POSITION;
}
int viewFrom = type == TYPE_ITEM ? 0 : mHeadersStartPosition;
int viewTo = type == TYPE_ITEM ? mHeadersStartPosition : getChildCount();
final int recyclerTop = getPaddingTop();
for (int i = viewFrom; i < viewTo; ++i) {
final View v = getChildAt(i);
final int adapterPosition = getPosition(v);
final int headerHeight = getPositionSectionHeaderHeight(adapterPosition);
final int top = getDecoratedTop(v);
final int bottom = getDecoratedBottom(v);
if (visibleTop) {
if (top >= recyclerTop + headerHeight) {
return adapterPosition;
}
}
else {
if (bottom >= recyclerTop + headerHeight) {
return adapterPosition;
}
}
}
return NO_POSITION;
}
private int getLastVisiblePosition(int type) {
if (type == TYPE_ITEM && mHeadersStartPosition <= 0) {
return NO_POSITION;
}
else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) {
return NO_POSITION;
}
int viewFrom = type == TYPE_ITEM ? mHeadersStartPosition - 1 : getChildCount() - 1;
int viewTo = type == TYPE_ITEM ? 0 : mHeadersStartPosition;
final int recyclerBottom = getHeight() - getPaddingBottom();
for (int i = viewFrom; i >= viewTo; --i) {
final View v = getChildAt(i);
final int top = getDecoratedTop(v);
if (top < recyclerBottom) {
return getPosition(v);
}
}
return NO_POSITION;
}
private LayoutRow getFirstVisibleRow() {
final int recyclerTop = getPaddingTop();
for (LayoutRow row : mLayoutRows) {
if (row.bottom > recyclerTop) {
return row;
}
}
return null;
}
private int getFirstVisibleSectionHeader() {
final int recyclerTop = getPaddingTop();
int header = NO_POSITION;
for (int i = 0, n = mLayoutRows.size(); i < n; ++i) {
final LayoutRow row = mLayoutRows.get(i);
if (row.header) {
header = i;
}
if (row.bottom > recyclerTop) {
return header;
}
}
return NO_POSITION;
}
private LayoutRow getNextVisibleSectionHeader(int headerFrom) {
for (int i = headerFrom + 1, n = mLayoutRows.size(); i < n; ++i) {
final LayoutRow row = mLayoutRows.get(i);
if (row.header) {
return row;
}
}
return null;
}
private LayoutRow getHeaderRow(int adapterPosition) {
for (int i = 0, n = mLayoutRows.size(); i < n; ++i) {
final LayoutRow row = mLayoutRows.get(i);
if (row.header && row.adapterPosition == adapterPosition) {
return row;
}
}
return null;
}
private void removeFloatingHeader(RecyclerView.Recycler recycler) {
if (mFloatingHeaderView == null) {
return;
}
final View view = mFloatingHeaderView;
mFloatingHeaderView = null;
mFloatingHeaderPosition = NO_POSITION;
removeAndRecycleView(view, recycler);
}
private void onHeaderChanged(int section, View view, HeaderState state, int pushOffset) {
if (mStickyHeaderSection != NO_POSITION && section != mStickyHeaderSection) {
onHeaderUnstick();
}
final boolean headerStateChanged = mStickyHeaderSection != section || !mStickyHeadeState.equals(state) || state.equals(HeaderState.PUSHED);
mStickyHeaderSection = section;
mStickyHeaderView = view;
mStickyHeadeState = state;
if (headerStateChanged && mHeaderStateListener != null) {
mHeaderStateListener.onHeaderStateChanged(section, view, state, pushOffset);
}
}
private void onHeaderUnstick() {
if (mStickyHeaderSection != NO_POSITION) {
if (mHeaderStateListener != null) {
mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0);
}
mStickyHeaderSection = NO_POSITION;
mStickyHeaderView = null;
mStickyHeadeState = HeaderState.NORMAL;
}
}
private void stickTopHeader(RecyclerView.Recycler recycler) {
final int firstHeader = getFirstVisibleSectionHeader();
final int top = getPaddingTop();
final int left = getPaddingLeft();
final int right = getWidth() - getPaddingRight();
int notifySection = NO_POSITION;
View notifyView = null;
HeaderState notifyState = HeaderState.NORMAL;
int notifyOffset = 0;
if (firstHeader != NO_POSITION) {
// Top row is header, floating header is not visible, remove
removeFloatingHeader(recycler);
final LayoutRow firstHeaderRow = mLayoutRows.get(firstHeader);
final int section = mAdapter.getAdapterPositionSection(firstHeaderRow.adapterPosition);
if (mAdapter.isSectionHeaderSticky(section)) {
final LayoutRow nextHeaderRow = getNextVisibleSectionHeader(firstHeader);
int offset = 0;
if (nextHeaderRow != null) {
final int height = firstHeaderRow.getHeight();
offset = Math.min(Math.max(top - nextHeaderRow.top, -height) + height, height);
}
mStickOffset = top - firstHeaderRow.top - offset;
firstHeaderRow.headerView.offsetTopAndBottom(mStickOffset);
onHeaderChanged(section, firstHeaderRow.headerView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset);
}
else {
onHeaderUnstick();
mStickOffset = 0;
}
}
else {
// We don't have first visible sector header in layout, create floating
final LayoutRow firstVisibleRow = getFirstVisibleRow();
if (firstVisibleRow != null) {
final int section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition);
if (mAdapter.isSectionHeaderSticky(section)) {
final int headerPosition = mAdapter.getSectionHeaderPosition(section);
if (mFloatingHeaderView == null || mFloatingHeaderPosition != headerPosition) {
removeFloatingHeader(recycler);
// Create floating header
final View v = recycler.getViewForPosition(headerPosition);
addView(v, mHeadersStartPosition);
measureChildWithMargins(v, 0, 0);
mFloatingHeaderView = v;
mFloatingHeaderPosition = headerPosition;
}
// Push floating header up, if needed
final int height = getDecoratedMeasuredHeight(mFloatingHeaderView);
int offset = 0;
if (getChildCount() - mHeadersStartPosition > 1) {
final View nextHeader = getChildAt(mHeadersStartPosition + 1);
final int contentHeight = Math.max(0, height - mHeaderOverlapMargin);
offset = Math.max(top - getDecoratedTop(nextHeader), -contentHeight) + contentHeight;
}
layoutDecorated(mFloatingHeaderView, left, top - offset, right, top + height - offset);
onHeaderChanged(section, mFloatingHeaderView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset);
}
else {
onHeaderUnstick();
}
}
else {
onHeaderUnstick();
}
}
}
private void updateTopPosition() {
if (getChildCount() == 0) {
mAnchor.reset();
}
final LayoutRow firstVisibleRow = getFirstVisibleRow();
if (firstVisibleRow != null) {
mAnchor.section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition);
mAnchor.item = mAdapter.getItemSectionOffset(mAnchor.section, firstVisibleRow.adapterPosition);
mAnchor.offset = Math.min(firstVisibleRow.top - getPaddingTop(), 0);
}
}
private int getViewType(View view) {
return getItemViewType(view) & 0xFF;
}
private int getViewType(int position) {
return mAdapter.getItemViewType(position) & 0xFF;
}
private void clearState() {
mHeadersStartPosition = 0;
mStickOffset = 0;
mFloatingHeaderView = null;
mFloatingHeaderPosition = -1;
mAverageHeaderHeight = 0;
mLayoutRows.clear();
if (mStickyHeaderSection != NO_POSITION) {
if (mHeaderStateListener != null) {
mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0);
}
mStickyHeaderSection = NO_POSITION;
mStickyHeaderView = null;
mStickyHeadeState = HeaderState.NORMAL;
}
}
@Override
public int computeVerticalScrollExtent(RecyclerView.State state) {
if (mHeadersStartPosition == 0 || state.getItemCount() == 0) {
return 0;
}
final View startChild = getChildAt(0);
final View endChild = getChildAt(mHeadersStartPosition - 1);
if (startChild == null || endChild == null) {
return 0;
}
return Math.abs(getPosition(startChild) - getPosition(endChild)) + 1;
}
@Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
if (mHeadersStartPosition == 0 || state.getItemCount() == 0) {
return 0;
}
final View startChild = getChildAt(0);
final View endChild = getChildAt(mHeadersStartPosition - 1);
if (startChild == null || endChild == null) {
return 0;
}
final int recyclerTop = getPaddingTop();
final LayoutRow topRow = getTopRow();
final int scrollChunk = Math.max(-topRow.top + recyclerTop, 0);
if (scrollChunk == 0) {
return 0;
}
final int minPosition = Math.min(getPosition(startChild), getPosition(endChild));
final int maxPosition = Math.max(getPosition(startChild), getPosition(endChild));
return Math.max(0, minPosition);
}
@Override
public int computeVerticalScrollRange(RecyclerView.State state) {
if (mHeadersStartPosition == 0 || state.getItemCount() == 0) {
return 0;
}
final View startChild = getChildAt(0);
final View endChild = getChildAt(mHeadersStartPosition - 1);
if (startChild == null || endChild == null) {
return 0;
}
return state.getItemCount();
}
public static class LayoutParams extends RecyclerView.LayoutParams {
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);
}
public int getSpanIndex() {
return mSpanIndex;
}
public int getSpanSize() {
return mSpanSize;
}
}
public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
@Override
public int getSpanSize(int section, int position) {
return 1;
}
@Override
public int getSpanIndex(int section, int position, int spanCount) {
return position % spanCount;
}
}
/**
* An interface to provide the number of spans each item occupies.
* <p>
* Default implementation sets each item to occupy exactly 1 span.
*
* @see StickyHeaderGridLayoutManager#setSpanSizeLookup(StickyHeaderGridLayoutManager.SpanSizeLookup)
*/
public static abstract class SpanSizeLookup {
/**
* Returns the number of span occupied by the item in <code>section</code> at <code>position</code>.
*
* @param section The adapter section of the item
* @param position The adapter position of the item in section
* @return The number of spans occupied by the item at the provided section and position
*/
abstract public int getSpanSize(int section, int position);
/**
* Returns the final span index of the provided position.
*
* <p>
* If you override this method, you need to make sure it is consistent with
* {@link #getSpanSize(int, int)}. StickyHeaderGridLayoutManager 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>
*
* @param section The adapter section of the item
* @param position The adapter position of the item in section
* @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 section, int position, int spanCount) {
// TODO: cache them?
final int positionSpanSize = getSpanSize(section, position);
if (positionSpanSize >= spanCount) {
return 0;
}
int spanIndex = 0;
for (int i = 0; i < position; ++i) {
final int spanSize = getSpanSize(section, i);
spanIndex += spanSize;
if (spanIndex == spanCount) {
spanIndex = 0;
}
else if (spanIndex > spanCount) {
spanIndex = spanSize;
}
}
if (spanIndex + positionSpanSize <= spanCount) {
return spanIndex;
}
return 0;
}
}
public static class SavedState implements Parcelable {
private int mAnchorSection;
private int mAnchorItem;
private int mAnchorOffset;
public SavedState() {
}
SavedState(Parcel in) {
mAnchorSection = in.readInt();
mAnchorItem = in.readInt();
mAnchorOffset = in.readInt();
}
public SavedState(SavedState other) {
mAnchorSection = other.mAnchorSection;
mAnchorItem = other.mAnchorItem;
mAnchorOffset = other.mAnchorOffset;
}
boolean hasValidAnchor() {
return mAnchorSection >= 0;
}
void invalidateAnchor() {
mAnchorSection = NO_POSITION;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mAnchorSection);
dest.writeInt(mAnchorItem);
dest.writeInt(mAnchorOffset);
}
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];
}
};
}
private static class LayoutRow {
private boolean header;
private View headerView;
private int adapterPosition;
private int length;
private int top;
private int bottom;
public LayoutRow(int adapterPosition, int length, int top, int bottom) {
this.header = false;
this.headerView = null;
this.adapterPosition = adapterPosition;
this.length = length;
this.top = top;
this.bottom = bottom;
}
public LayoutRow(View headerView, int adapterPosition, int length, int top, int bottom) {
this.header = true;
this.headerView = headerView;
this.adapterPosition = adapterPosition;
this.length = length;
this.top = top;
this.bottom = bottom;
}
int getHeight() {
return bottom - top;
}
}
private static class FillResult {
private View edgeView;
private int adapterPosition;
private int length;
private int height;
}
private static class AnchorPosition {
private int section;
private int item;
private int offset;
public AnchorPosition() {
reset();
}
public void reset() {
section = NO_POSITION;
item = 0;
offset = 0;
}
}
}