Created
December 19, 2015 13:06
-
-
Save ityancs/dd33fa487f20310051df to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation | |
* that places children in a two-dimensional grid, sized to a fixed column count | |
* value. User scrolling is possible in both horizontal and vertical directions | |
* to view the data set. | |
* | |
* <p>The column count is controllable via {@link #setTotalColumnCount(int)}. The layout manager | |
* will generate the number of rows necessary to accommodate the data set based on | |
* the fixed column count. | |
* | |
* <p>This manager does make some assumptions to simplify the implementation: | |
* <ul> | |
* <li>All child views are assumed to be the same size</li> | |
* <li>The window of visible views is a constant</li> | |
* </ul> | |
*/ | |
public class FixedGridLayoutManager extends RecyclerView.LayoutManager { | |
private static final String TAG = FixedGridLayoutManager.class.getSimpleName(); | |
private static final int DEFAULT_COUNT = 1; | |
/* View Removal Constants */ | |
private static final int REMOVE_VISIBLE = 0; | |
private static final int REMOVE_INVISIBLE = 1; | |
/* Fill Direction Constants */ | |
private static final int DIRECTION_NONE = -1; | |
private static final int DIRECTION_START = 0; | |
private static final int DIRECTION_END = 1; | |
private static final int DIRECTION_UP = 2; | |
private static final int DIRECTION_DOWN = 3; | |
/* First (top-left) position visible at any point */ | |
private int mFirstVisiblePosition; | |
/* Consistent size applied to all child views */ | |
private int mDecoratedChildWidth; | |
private int mDecoratedChildHeight; | |
/* Number of columns that exist in the grid */ | |
private int mTotalColumnCount = DEFAULT_COUNT; | |
/* Metrics for the visible window of our data */ | |
private int mVisibleColumnCount; | |
private int mVisibleRowCount; | |
/* Flag to force current scroll offsets to be ignored on re-layout */ | |
private boolean mForceClearOffsets; | |
/* Used for tracking off-screen change events */ | |
private int mFirstChangedPosition; | |
private int mChangedPositionCount; | |
/** | |
* Set the number of columns the layout manager will use. This will | |
* trigger a layout update. | |
* @param count Number of columns. | |
*/ | |
public void setTotalColumnCount(int count) { | |
mTotalColumnCount = count; | |
requestLayout(); | |
} | |
/* | |
* You must return true from this method if you want your | |
* LayoutManager to support anything beyond "simple" item | |
* animations. Enabling this causes onLayoutChildren() to | |
* be called twice on each animated change; once for a | |
* pre-layout, and again for the real layout. | |
*/ | |
@Override | |
public boolean supportsPredictiveItemAnimations() { | |
return true; | |
} | |
/* | |
* Called by RecyclerView when a view removal is triggered. This is called | |
* before onLayoutChildren() in pre-layout if the views removed are not visible. We | |
* use it in this case to inform pre-layout that a removal took place. | |
* | |
* This method is still called if the views removed were visible, but it will | |
* happen AFTER pre-layout. | |
*/ | |
@Override | |
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { | |
mFirstChangedPosition = positionStart; | |
mChangedPositionCount = itemCount; | |
} | |
/* | |
* This method is your initial call from the framework. You will receive it when you | |
* need to start laying out the initial set of views. This method will not be called | |
* repeatedly, so don't rely on it to continually process changes during user | |
* interaction. | |
* | |
* This method will be called when the data set in the adapter changes, so it can be | |
* used to update a layout based on a new item count. | |
* | |
* If predictive animations are enabled, you will see this called twice. First, with | |
* state.isPreLayout() returning true to lay out children in their initial conditions. | |
* Then again to lay out children in their final locations. | |
*/ | |
@Override | |
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { | |
//We have nothing to show for an empty data set but clear any existing views | |
if (getItemCount() == 0) { | |
detachAndScrapAttachedViews(recycler); | |
return; | |
} | |
if (getChildCount() == 0 && state.isPreLayout()) { | |
//Nothing to do during prelayout when empty | |
return; | |
} | |
//Clear change tracking state when a real layout occurs | |
if (!state.isPreLayout()) { | |
mFirstChangedPosition = mChangedPositionCount = 0; | |
} | |
if (getChildCount() == 0) { //First or empty layout | |
//Scrap measure one child | |
View scrap = recycler.getViewForPosition(0); | |
addView(scrap); | |
measureChildWithMargins(scrap, 0, 0); | |
/* | |
* We make some assumptions in this code based on every child | |
* view being the same size (i.e. a uniform grid). This allows | |
* us to compute the following values up front because they | |
* won't change. | |
*/ | |
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap); | |
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap); | |
detachAndScrapView(scrap, recycler); | |
} | |
//Always update the visible row/column counts | |
updateWindowSizing(); | |
SparseIntArray removedCache = null; | |
/* | |
* During pre-layout, we need to take note of any views that are | |
* being removed in order to handle predictive animations | |
*/ | |
if (state.isPreLayout()) { | |
removedCache = new SparseIntArray(getChildCount()); | |
for (int i=0; i < getChildCount(); i++) { | |
final View view = getChildAt(i); | |
LayoutParams lp = (LayoutParams) view.getLayoutParams(); | |
if (lp.isItemRemoved()) { | |
//Track these view removals as visible | |
removedCache.put(lp.getViewLayoutPosition(), REMOVE_VISIBLE); | |
} | |
} | |
//Track view removals that happened out of bounds (i.e. off-screen) | |
if (removedCache.size() == 0 && mChangedPositionCount > 0) { | |
for (int i = mFirstChangedPosition; i < (mFirstChangedPosition + mChangedPositionCount); i++) { | |
removedCache.put(i, REMOVE_INVISIBLE); | |
} | |
} | |
} | |
int childLeft; | |
int childTop; | |
if (getChildCount() == 0) { //First or empty layout | |
//Reset the visible and scroll positions | |
mFirstVisiblePosition = 0; | |
childLeft = childTop = 0; | |
} else if (!state.isPreLayout() | |
&& getVisibleChildCount() >= state.getItemCount()) { | |
//Data set is too small to scroll fully, just reset position | |
mFirstVisiblePosition = 0; | |
childLeft = childTop = 0; | |
} else { //Adapter data set changes | |
/* | |
* Keep the existing initial position, and save off | |
* the current scrolled offset. | |
*/ | |
final View topChild = getChildAt(0); | |
if (mForceClearOffsets) { | |
childLeft = childTop = 0; | |
mForceClearOffsets = false; | |
} else { | |
childLeft = getDecoratedLeft(topChild); | |
childTop = getDecoratedTop(topChild); | |
} | |
/* | |
* When data set is too small to scroll vertically, adjust vertical offset | |
* and shift position to the first row, preserving current column | |
*/ | |
if (!state.isPreLayout() && getVerticalSpace() > (getTotalRowCount() * mDecoratedChildHeight)) { | |
mFirstVisiblePosition = mFirstVisiblePosition % getTotalColumnCount(); | |
childTop = 0; | |
//If the shift overscrolls the column max, back it off | |
if ((mFirstVisiblePosition + mVisibleColumnCount) > state.getItemCount()) { | |
mFirstVisiblePosition = Math.max(state.getItemCount() - mVisibleColumnCount, 0); | |
childLeft = 0; | |
} | |
} | |
/* | |
* Adjust the visible position if out of bounds in the | |
* new layout. This occurs when the new item count in an adapter | |
* is much smaller than it was before, and you are scrolled to | |
* a location where no items would exist. | |
*/ | |
int maxFirstRow = getTotalRowCount() - (mVisibleRowCount-1); | |
int maxFirstCol = getTotalColumnCount() - (mVisibleColumnCount-1); | |
boolean isOutOfRowBounds = getFirstVisibleRow() > maxFirstRow; | |
boolean isOutOfColBounds = getFirstVisibleColumn() > maxFirstCol; | |
if (isOutOfRowBounds || isOutOfColBounds) { | |
int firstRow; | |
if (isOutOfRowBounds) { | |
firstRow = maxFirstRow; | |
} else { | |
firstRow = getFirstVisibleRow(); | |
} | |
int firstCol; | |
if (isOutOfColBounds) { | |
firstCol = maxFirstCol; | |
} else { | |
firstCol = getFirstVisibleColumn(); | |
} | |
mFirstVisiblePosition = firstRow * getTotalColumnCount() + firstCol; | |
childLeft = getHorizontalSpace() - (mDecoratedChildWidth * mVisibleColumnCount); | |
childTop = getVerticalSpace() - (mDecoratedChildHeight * mVisibleRowCount); | |
//Correct cases where shifting to the bottom-right overscrolls the top-left | |
// This happens on data sets too small to scroll in a direction. | |
if (getFirstVisibleRow() == 0) { | |
childTop = Math.min(childTop, 0); | |
} | |
if (getFirstVisibleColumn() == 0) { | |
childLeft = Math.min(childLeft, 0); | |
} | |
} | |
} | |
//Clear all attached views into the recycle bin | |
detachAndScrapAttachedViews(recycler); | |
//Fill the grid for the initial layout of views | |
fillGrid(DIRECTION_NONE, childLeft, childTop, recycler, state, removedCache); | |
//Evaluate any disappearing views that may exist | |
if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) { | |
final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList(); | |
final HashSet<View> disappearingViews = new HashSet<View>(scrapList.size()); | |
for (RecyclerView.ViewHolder holder : scrapList) { | |
final View child = holder.itemView; | |
final LayoutParams lp = (LayoutParams) child.getLayoutParams(); | |
if (!lp.isItemRemoved()) { | |
disappearingViews.add(child); | |
} | |
} | |
for (View child : disappearingViews) { | |
layoutDisappearingView(child); | |
} | |
} | |
} | |
@Override | |
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { | |
//Completely scrap the existing layout | |
removeAllViews(); | |
} | |
/* | |
* Rather than continuously checking how many views we can fit | |
* based on scroll offsets, we simplify the math by computing the | |
* visible grid as what will initially fit on screen, plus one. | |
*/ | |
private void updateWindowSizing() { | |
mVisibleColumnCount = (getHorizontalSpace() / mDecoratedChildWidth) + 1; | |
if (getHorizontalSpace() % mDecoratedChildWidth > 0) { | |
mVisibleColumnCount++; | |
} | |
//Allow minimum value for small data sets | |
if (mVisibleColumnCount > getTotalColumnCount()) { | |
mVisibleColumnCount = getTotalColumnCount(); | |
} | |
mVisibleRowCount = (getVerticalSpace()/ mDecoratedChildHeight) + 1; | |
if (getVerticalSpace() % mDecoratedChildHeight > 0) { | |
mVisibleRowCount++; | |
} | |
if (mVisibleRowCount > getTotalRowCount()) { | |
mVisibleRowCount = getTotalRowCount(); | |
} | |
} | |
private void fillGrid(int direction, RecyclerView.Recycler recycler, RecyclerView.State state) { | |
fillGrid(direction, 0, 0, recycler, state, null); | |
} | |
private void fillGrid(int direction, int emptyLeft, int emptyTop, | |
RecyclerView.Recycler recycler, | |
RecyclerView.State state, | |
SparseIntArray removedPositions) { | |
if (mFirstVisiblePosition < 0) mFirstVisiblePosition = 0; | |
if (mFirstVisiblePosition >= getItemCount()) mFirstVisiblePosition = (getItemCount() - 1); | |
/* | |
* First, we will detach all existing views from the layout. | |
* detachView() is a lightweight operation that we can use to | |
* quickly reorder views without a full add/remove. | |
*/ | |
SparseArray<View> viewCache = new SparseArray<View>(getChildCount()); | |
int startLeftOffset = getPaddingLeft() + emptyLeft; | |
int startTopOffset = getPaddingTop() + emptyTop; | |
if (getChildCount() != 0) { | |
final View topView = getChildAt(0); | |
startLeftOffset = getDecoratedLeft(topView); | |
startTopOffset = getDecoratedTop(topView); | |
switch (direction) { | |
case DIRECTION_START: | |
startLeftOffset -= mDecoratedChildWidth; | |
break; | |
case DIRECTION_END: | |
startLeftOffset += mDecoratedChildWidth; | |
break; | |
case DIRECTION_UP: | |
startTopOffset -= mDecoratedChildHeight; | |
break; | |
case DIRECTION_DOWN: | |
startTopOffset += mDecoratedChildHeight; | |
break; | |
} | |
//Cache all views by their existing position, before updating counts | |
for (int i=0; i < getChildCount(); i++) { | |
int position = positionOfIndex(i); | |
final View child = getChildAt(i); | |
viewCache.put(position, child); | |
} | |
//Temporarily detach all views. | |
// Views we still need will be added back at the proper index. | |
for (int i=0; i < viewCache.size(); i++) { | |
detachView(viewCache.valueAt(i)); | |
} | |
} | |
/* | |
* Next, we advance the visible position based on the fill direction. | |
* DIRECTION_NONE doesn't advance the position in any direction. | |
*/ | |
switch (direction) { | |
case DIRECTION_START: | |
mFirstVisiblePosition--; | |
break; | |
case DIRECTION_END: | |
mFirstVisiblePosition++; | |
break; | |
case DIRECTION_UP: | |
mFirstVisiblePosition -= getTotalColumnCount(); | |
break; | |
case DIRECTION_DOWN: | |
mFirstVisiblePosition += getTotalColumnCount(); | |
break; | |
} | |
/* | |
* Next, we supply the grid of items that are deemed visible. | |
* If these items were previously there, they will simply be | |
* re-attached. New views that must be created are obtained | |
* from the Recycler and added. | |
*/ | |
int leftOffset = startLeftOffset; | |
int topOffset = startTopOffset; | |
for (int i = 0; i < getVisibleChildCount(); i++) { | |
int nextPosition = positionOfIndex(i); | |
/* | |
* When a removal happens out of bounds, the pre-layout positions of items | |
* after the removal are shifted to their final positions ahead of schedule. | |
* We have to track off-screen removals and shift those positions back | |
* so we can properly lay out all current (and appearing) views in their | |
* initial locations. | |
*/ | |
int offsetPositionDelta = 0; | |
if (state.isPreLayout()) { | |
int offsetPosition = nextPosition; | |
for (int offset = 0; offset < removedPositions.size(); offset++) { | |
//Look for off-screen removals that are less-than this | |
if (removedPositions.valueAt(offset) == REMOVE_INVISIBLE | |
&& removedPositions.keyAt(offset) < nextPosition) { | |
//Offset position to match | |
offsetPosition--; | |
} | |
} | |
offsetPositionDelta = nextPosition - offsetPosition; | |
nextPosition = offsetPosition; | |
} | |
if (nextPosition < 0 || nextPosition >= state.getItemCount()) { | |
//Item space beyond the data set, don't attempt to add a view | |
continue; | |
} | |
//Layout this position | |
View view = viewCache.get(nextPosition); | |
if (view == null) { | |
/* | |
* The Recycler will give us either a newly constructed view, | |
* or a recycled view it has on-hand. In either case, the | |
* view will already be fully bound to the data by the | |
* adapter for us. | |
*/ | |
view = recycler.getViewForPosition(nextPosition); | |
addView(view); | |
/* | |
* Update the new view's metadata, but only when this is a real | |
* layout pass. | |
*/ | |
if (!state.isPreLayout()) { | |
LayoutParams lp = (LayoutParams) view.getLayoutParams(); | |
lp.row = getGlobalRowOfPosition(nextPosition); | |
lp.column = getGlobalColumnOfPosition(nextPosition); | |
} | |
/* | |
* It is prudent to measure/layout each new view we | |
* receive from the Recycler. We don't have to do | |
* this for views we are just re-arranging. | |
*/ | |
measureChildWithMargins(view, 0, 0); | |
layoutDecorated(view, leftOffset, topOffset, | |
leftOffset + mDecoratedChildWidth, | |
topOffset + mDecoratedChildHeight); | |
} else { | |
//Re-attach the cached view at its new index | |
attachView(view); | |
viewCache.remove(nextPosition); | |
} | |
if (i % mVisibleColumnCount == (mVisibleColumnCount - 1)) { | |
leftOffset = startLeftOffset; | |
topOffset += mDecoratedChildHeight; | |
//During pre-layout, on each column end, apply any additional appearing views | |
if (state.isPreLayout()) { | |
layoutAppearingViews(recycler, view, nextPosition, removedPositions.size(), offsetPositionDelta); | |
} | |
} else { | |
leftOffset += mDecoratedChildWidth; | |
} | |
} | |
/* | |
* Finally, we ask the Recycler to scrap and store any views | |
* that we did not re-attach. These are views that are not currently | |
* necessary because they are no longer visible. | |
*/ | |
for (int i=0; i < viewCache.size(); i++) { | |
final View removingView = viewCache.valueAt(i); | |
recycler.recycleView(removingView); | |
} | |
} | |
/* | |
* You must override this method if you would like to support external calls | |
* to shift the view to a given adapter position. In our implementation, this | |
* is the same as doing a fresh layout with the given position as the top-left | |
* (or first visible), so we simply set that value and trigger onLayoutChildren() | |
*/ | |
@Override | |
public void scrollToPosition(int position) { | |
if (position >= getItemCount()) { | |
Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount()); | |
return; | |
} | |
//Ignore current scroll offset, snap to top-left | |
mForceClearOffsets = true; | |
//Set requested position as first visible | |
mFirstVisiblePosition = position; | |
//Trigger a new view layout | |
requestLayout(); | |
} | |
/* | |
* You must override this method if you would like to support external calls | |
* to animate a change to a new adapter position. The framework provides a | |
* helper scroller implementation (LinearSmoothScroller), which we leverage | |
* to do the animation calculations. | |
*/ | |
@Override | |
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) { | |
if (position >= getItemCount()) { | |
Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount()); | |
return; | |
} | |
/* | |
* LinearSmoothScroller's default behavior is to scroll the contents until | |
* the child is fully visible. It will snap to the top-left or bottom-right | |
* of the parent depending on whether the direction of travel was positive | |
* or negative. | |
*/ | |
LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { | |
/* | |
* LinearSmoothScroller, at a minimum, just need to know the vector | |
* (x/y distance) to travel in order to get from the current positioning | |
* to the target. | |
*/ | |
@Override | |
public PointF computeScrollVectorForPosition(int targetPosition) { | |
final int rowOffset = getGlobalRowOfPosition(targetPosition) | |
- getGlobalRowOfPosition(mFirstVisiblePosition); | |
final int columnOffset = getGlobalColumnOfPosition(targetPosition) | |
- getGlobalColumnOfPosition(mFirstVisiblePosition); | |
return new PointF(columnOffset * mDecoratedChildWidth, rowOffset * mDecoratedChildHeight); | |
} | |
}; | |
scroller.setTargetPosition(position); | |
startSmoothScroll(scroller); | |
} | |
/* | |
* Use this method to tell the RecyclerView if scrolling is even possible | |
* in the horizontal direction. | |
*/ | |
@Override | |
public boolean canScrollHorizontally() { | |
//We do allow scrolling | |
return true; | |
} | |
/* | |
* This method describes how far RecyclerView thinks the contents should scroll horizontally. | |
* You are responsible for verifying edge boundaries, and determining if this scroll | |
* event somehow requires that new views be added or old views get recycled. | |
*/ | |
@Override | |
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { | |
if (getChildCount() == 0) { | |
return 0; | |
} | |
//Take leftmost measurements from the top-left child | |
final View topView = getChildAt(0); | |
//Take rightmost measurements from the top-right child | |
final View bottomView = getChildAt(mVisibleColumnCount-1); | |
//Optimize the case where the entire data set is too small to scroll | |
int viewSpan = getDecoratedRight(bottomView) - getDecoratedLeft(topView); | |
if (viewSpan < getHorizontalSpace()) { | |
//We cannot scroll in either direction | |
return 0; | |
} | |
int delta; | |
boolean leftBoundReached = getFirstVisibleColumn() == 0; | |
boolean rightBoundReached = getLastVisibleColumn() >= getTotalColumnCount(); | |
if (dx > 0) { // Contents are scrolling left | |
//Check right bound | |
if (rightBoundReached) { | |
//If we've reached the last column, enforce limits | |
int rightOffset = getHorizontalSpace() - getDecoratedRight(bottomView) + getPaddingRight(); | |
delta = Math.max(-dx, rightOffset); | |
} else { | |
//No limits while the last column isn't visible | |
delta = -dx; | |
} | |
} else { // Contents are scrolling right | |
//Check left bound | |
if (leftBoundReached) { | |
int leftOffset = -getDecoratedLeft(topView) + getPaddingLeft(); | |
delta = Math.min(-dx, leftOffset); | |
} else { | |
delta = -dx; | |
} | |
} | |
offsetChildrenHorizontal(delta); | |
if (dx > 0) { | |
if (getDecoratedRight(topView) < 0 && !rightBoundReached) { | |
fillGrid(DIRECTION_END, recycler, state); | |
} else if (!rightBoundReached) { | |
fillGrid(DIRECTION_NONE, recycler, state); | |
} | |
} else { | |
if (getDecoratedLeft(topView) > 0 && !leftBoundReached) { | |
fillGrid(DIRECTION_START, recycler, state); | |
} else if (!leftBoundReached) { | |
fillGrid(DIRECTION_NONE, recycler, state); | |
} | |
} | |
/* | |
* Return value determines if a boundary has been reached | |
* (for edge effects and flings). If returned value does not | |
* match original delta (passed in), RecyclerView will draw | |
* an edge effect. | |
*/ | |
return -delta; | |
} | |
/* | |
* Use this method to tell the RecyclerView if scrolling is even possible | |
* in the vertical direction. | |
*/ | |
@Override | |
public boolean canScrollVertically() { | |
//We do allow scrolling | |
return true; | |
} | |
/* | |
* This method describes how far RecyclerView thinks the contents should scroll vertically. | |
* You are responsible for verifying edge boundaries, and determining if this scroll | |
* event somehow requires that new views be added or old views get recycled. | |
*/ | |
@Override | |
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { | |
if (getChildCount() == 0) { | |
return 0; | |
} | |
//Take top measurements from the top-left child | |
final View topView = getChildAt(0); | |
//Take bottom measurements from the bottom-right child. | |
final View bottomView = getChildAt(getChildCount()-1); | |
//Optimize the case where the entire data set is too small to scroll | |
int viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView); | |
if (viewSpan < getVerticalSpace()) { | |
//We cannot scroll in either direction | |
return 0; | |
} | |
int delta; | |
int maxRowCount = getTotalRowCount(); | |
boolean topBoundReached = getFirstVisibleRow() == 0; | |
boolean bottomBoundReached = getLastVisibleRow() >= maxRowCount; | |
if (dy > 0) { // Contents are scrolling up | |
//Check against bottom bound | |
if (bottomBoundReached) { | |
//If we've reached the last row, enforce limits | |
int bottomOffset; | |
if (rowOfIndex(getChildCount() - 1) >= (maxRowCount - 1)) { | |
//We are truly at the bottom, determine how far | |
bottomOffset = getVerticalSpace() - getDecoratedBottom(bottomView) | |
+ getPaddingBottom(); | |
} else { | |
/* | |
* Extra space added to account for allowing bottom space in the grid. | |
* This occurs when the overlap in the last row is not large enough to | |
* ensure that at least one element in that row isn't fully recycled. | |
*/ | |
bottomOffset = getVerticalSpace() - (getDecoratedBottom(bottomView) | |
+ mDecoratedChildHeight) + getPaddingBottom(); | |
} | |
delta = Math.max(-dy, bottomOffset); | |
} else { | |
//No limits while the last row isn't visible | |
delta = -dy; | |
} | |
} else { // Contents are scrolling down | |
//Check against top bound | |
if (topBoundReached) { | |
int topOffset = -getDecoratedTop(topView) + getPaddingTop(); | |
delta = Math.min(-dy, topOffset); | |
} else { | |
delta = -dy; | |
} | |
} | |
offsetChildrenVertical(delta); | |
if (dy > 0) { | |
if (getDecoratedBottom(topView) < 0 && !bottomBoundReached) { | |
fillGrid(DIRECTION_DOWN, recycler, state); | |
} else if (!bottomBoundReached) { | |
fillGrid(DIRECTION_NONE, recycler, state); | |
} | |
} else { | |
if (getDecoratedTop(topView) > 0 && !topBoundReached) { | |
fillGrid(DIRECTION_UP, recycler, state); | |
} else if (!topBoundReached) { | |
fillGrid(DIRECTION_NONE, recycler, state); | |
} | |
} | |
/* | |
* Return value determines if a boundary has been reached | |
* (for edge effects and flings). If returned value does not | |
* match original delta (passed in), RecyclerView will draw | |
* an edge effect. | |
*/ | |
return -delta; | |
} | |
/* | |
* This is a helper method used by RecyclerView to determine | |
* if a specific child view can be returned. | |
*/ | |
@Override | |
public View findViewByPosition(int position) { | |
for (int i=0; i < getChildCount(); i++) { | |
if (positionOfIndex(i) == position) { | |
return getChildAt(i); | |
} | |
} | |
return null; | |
} | |
/** Boilerplate to extend LayoutParams for tracking row/column of attached views */ | |
/* | |
* Even without extending LayoutParams, we must override this method | |
* to provide the default layout parameters that each child view | |
* will receive when added. | |
*/ | |
@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; | |
} | |
public static class LayoutParams extends RecyclerView.LayoutParams { | |
//Current row in the grid | |
public int row; | |
//Current column in the grid | |
public int column; | |
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); | |
} | |
} | |
/** Animation Layout Helpers */ | |
/* Helper to obtain and place extra appearing views */ | |
private void layoutAppearingViews(RecyclerView.Recycler recycler, View referenceView, int referencePosition, int extraCount, int offset) { | |
//Nothing to do... | |
if (extraCount < 1) return; | |
//FIXME: This code currently causes double layout of views that are still visible… | |
for (int extra = 1; extra <= extraCount; extra++) { | |
//Grab the next position after the reference | |
final int extraPosition = referencePosition + extra; | |
if (extraPosition < 0 || extraPosition >= getItemCount()) { | |
//Can't do anything with this | |
continue; | |
} | |
/* | |
* Obtain additional position views that we expect to appear | |
* as part of the animation. | |
*/ | |
View appearing = recycler.getViewForPosition(extraPosition); | |
addView(appearing); | |
//Find layout delta from reference position | |
final int newRow = getGlobalRowOfPosition(extraPosition + offset); | |
final int rowDelta = newRow - getGlobalRowOfPosition(referencePosition + offset); | |
final int newCol = getGlobalColumnOfPosition(extraPosition + offset); | |
final int colDelta = newCol - getGlobalColumnOfPosition(referencePosition + offset); | |
layoutTempChildView(appearing, rowDelta, colDelta, referenceView); | |
} | |
} | |
/* Helper to place a disappearing view */ | |
private void layoutDisappearingView(View disappearingChild) { | |
/* | |
* LayoutManager has a special method for attaching views that | |
* will only be around long enough to animate. | |
*/ | |
addDisappearingView(disappearingChild); | |
//Adjust each disappearing view to its proper place | |
final LayoutParams lp = (LayoutParams) disappearingChild.getLayoutParams(); | |
final int newRow = getGlobalRowOfPosition(lp.getViewAdapterPosition()); | |
final int rowDelta = newRow - lp.row; | |
final int newCol = getGlobalColumnOfPosition(lp.getViewAdapterPosition()); | |
final int colDelta = newCol - lp.column; | |
layoutTempChildView(disappearingChild, rowDelta, colDelta, disappearingChild); | |
} | |
/* Helper to lay out appearing/disappearing children */ | |
private void layoutTempChildView(View child, int rowDelta, int colDelta, View referenceView) { | |
//Set the layout position to the global row/column difference from the reference view | |
int layoutTop = getDecoratedTop(referenceView) + rowDelta * mDecoratedChildHeight; | |
int layoutLeft = getDecoratedLeft(referenceView) + colDelta * mDecoratedChildWidth; | |
measureChildWithMargins(child, 0, 0); | |
layoutDecorated(child, layoutLeft, layoutTop, | |
layoutLeft + mDecoratedChildWidth, | |
layoutTop + mDecoratedChildHeight); | |
} | |
/** Private Helpers and Metrics Accessors */ | |
/* Return the overall column index of this position in the global layout */ | |
private int getGlobalColumnOfPosition(int position) { | |
return position % mTotalColumnCount; | |
} | |
/* Return the overall row index of this position in the global layout */ | |
private int getGlobalRowOfPosition(int position) { | |
return position / mTotalColumnCount; | |
} | |
/* | |
* Mapping between child view indices and adapter data | |
* positions helps fill the proper views during scrolling. | |
*/ | |
private int positionOfIndex(int childIndex) { | |
int row = childIndex / mVisibleColumnCount; | |
int column = childIndex % mVisibleColumnCount; | |
return mFirstVisiblePosition + (row * getTotalColumnCount()) + column; | |
} | |
private int rowOfIndex(int childIndex) { | |
int position = positionOfIndex(childIndex); | |
return position / getTotalColumnCount(); | |
} | |
private int getFirstVisibleColumn() { | |
return (mFirstVisiblePosition % getTotalColumnCount()); | |
} | |
private int getLastVisibleColumn() { | |
return getFirstVisibleColumn() + mVisibleColumnCount; | |
} | |
private int getFirstVisibleRow() { | |
return (mFirstVisiblePosition / getTotalColumnCount()); | |
} | |
private int getLastVisibleRow() { | |
return getFirstVisibleRow() + mVisibleRowCount; | |
} | |
private int getVisibleChildCount() { | |
return mVisibleColumnCount * mVisibleRowCount; | |
} | |
private int getTotalColumnCount() { | |
if (getItemCount() < mTotalColumnCount) { | |
return getItemCount(); | |
} | |
return mTotalColumnCount; | |
} | |
private int getTotalRowCount() { | |
if (getItemCount() == 0 || mTotalColumnCount == 0) { | |
return 0; | |
} | |
int maxRow = getItemCount() / mTotalColumnCount; | |
//Bump the row count if it's not exactly even | |
if (getItemCount() % mTotalColumnCount != 0) { | |
maxRow++; | |
} | |
return maxRow; | |
} | |
private int getHorizontalSpace() { | |
return getWidth() - getPaddingRight() - getPaddingLeft(); | |
} | |
private int getVerticalSpace() { | |
return getHeight() - getPaddingBottom() - getPaddingTop(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment