package mcxtzhang.recyclerviewdemo; import android.content.Context; import android.graphics.PointF; import android.support.v7.widget.LinearSmoothScroller; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.View; import android.view.ViewGroup; import java.util.HashSet; import java.util.List; /** * 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; /* 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 = getPaddingLeft(); childTop = getPaddingTop(); } else if (!state.isPreLayout() && getVisibleChildCount() >= state.getItemCount()) { //Data set is too small to scroll fully, just reset position mFirstVisiblePosition = 0; childLeft = getPaddingLeft(); childTop = getPaddingTop(); } else { //Adapter data set changes /* * Keep the existing initial position, and save off * the current scrolled offset. */ final View topChild = getChildAt(0); 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 = getPaddingTop(); //If the shift overscrolls the column max, back it off if ((mFirstVisiblePosition + mVisibleColumnCount) > state.getItemCount()) { mFirstVisiblePosition = Math.max(state.getItemCount() - mVisibleColumnCount, 0); childLeft = getPaddingLeft(); } } /* * 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, getPaddingTop()); } if (getFirstVisibleColumn() == 0) { childLeft = Math.min(childLeft, getPaddingLeft()); } } } //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 = emptyLeft; int startTopOffset = 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; } //Set requested position as first visible mFirstVisiblePosition = position; //Toss all existing views away removeAllViews(); //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(); } }