Convert FixedGridLayoutManager to kotlin

This commit is contained in:
Lucas Lima 2020-04-23 23:56:35 -03:00
parent 4b63e79631
commit bf5422bc79
No known key found for this signature in database
GPG key ID: C5EEF4C30BFBF8D7
2 changed files with 627 additions and 927 deletions

View file

@ -1,927 +0,0 @@
package dev.lucasnlm.antimine.common.level.widget;
import android.content.Context;
import android.graphics.PointF;
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 androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import java.util.HashSet;
import java.util.List;
public class FixedGridLayoutManager extends RecyclerView.LayoutManager {
private static final String TAG = FixedGridLayoutManager.class.getSimpleName();
public FixedGridLayoutManager(int columnCount, int horizontalPadding, int verticalPadding) {
this.mTotalColumnCount = columnCount;
this.horizontalPadding = horizontalPadding;
this.verticalPadding = verticalPadding;
}
private final int verticalPadding;
private final int horizontalPadding;
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 = horizontalPadding;
childTop = verticalPadding;
} else if (!state.isPreLayout()
&& getVisibleChildCount() >= state.getItemCount()) {
//Data set is too small to scroll fully, just reset position
mFirstVisiblePosition = 0;
childLeft = horizontalPadding;
childTop = verticalPadding;
} 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 = verticalPadding;
//If the shift overscrolls the column max, back it off
if ((mFirstVisiblePosition + mVisibleColumnCount) > state.getItemCount()) {
mFirstVisiblePosition = Math.max(state.getItemCount() - mVisibleColumnCount, 0);
childLeft = horizontalPadding;
}
}
/*
* 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, verticalPadding);
}
if (getFirstVisibleColumn() == 0) {
childLeft = Math.min(childLeft, horizontalPadding);
}
}
}
//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) + horizontalPadding;
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) + horizontalPadding;
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)
+ verticalPadding;
} 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) + verticalPadding;
}
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) + verticalPadding;
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();
}
private int getVerticalSpace() {
return getHeight();
}
}

View file

@ -0,0 +1,627 @@
package dev.lucasnlm.antimine.common.level.widget
import android.content.Context
import android.graphics.PointF
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 android.view.ViewGroup.MarginLayoutParams
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Recycler
class FixedGridLayoutManager(
private var mTotalColumnCount: Int,
private val horizontalPadding: Int,
private val verticalPadding: Int
) : RecyclerView.LayoutManager() {
private var mFirstVisiblePosition = 0
private var mDecoratedChildWidth = 0
private var mDecoratedChildHeight = 0
private var mVisibleColumnCount = 0
private var mVisibleRowCount = 0
private var mFirstChangedPosition = 0
private var mChangedPositionCount = 0
override fun supportsPredictiveItemAnimations(): Boolean = true
override fun onItemsRemoved(recyclerView: RecyclerView, positionStart: Int, itemCount: Int) {
mFirstChangedPosition = positionStart
mChangedPositionCount = itemCount
}
override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) {
if (itemCount == 0) {
detachAndScrapAttachedViews(recycler)
return
}
if (childCount == 0 && state.isPreLayout) {
return
}
if (!state.isPreLayout) {
mChangedPositionCount = 0
mFirstChangedPosition = 0
}
if (childCount == 0) {
recycler.getViewForPosition(0).run {
addView(this)
measureChildWithMargins(this, 0, 0)
mDecoratedChildWidth = getDecoratedMeasuredWidth(this)
mDecoratedChildHeight = getDecoratedMeasuredHeight(this)
detachAndScrapView(this, recycler)
}
}
updateWindowSizing()
var removedCache: SparseIntArray? = null
if (state.isPreLayout) {
removedCache = SparseIntArray(childCount)
(0 until childCount)
.mapNotNull { getChildAt(it) }
.map { it.layoutParams as LayoutParams }
.filter { it.isItemRemoved }
.forEach { removedCache.put(it.viewLayoutPosition, REMOVE_VISIBLE) }
if (removedCache.size() == 0 && mChangedPositionCount > 0) {
(mFirstChangedPosition until mFirstChangedPosition + mChangedPositionCount)
.forEach { index ->
removedCache.put(index, REMOVE_INVISIBLE)
}
}
}
var childLeft: Int? = null
var childTop: Int? = null
if (childCount == 0) {
mFirstVisiblePosition = 0
childLeft = horizontalPadding
childTop = verticalPadding
} else if (!state.isPreLayout && visibleChildCount >= state.itemCount) {
//Data set is too small to scroll fully, just reset position
mFirstVisiblePosition = 0
childLeft = horizontalPadding
childTop = verticalPadding
} else {
getChildAt(0)?.let { topChild ->
childLeft = getDecoratedLeft(topChild)
childTop = getDecoratedTop(topChild)
}
if (!state.isPreLayout && verticalSpace > totalRowCount * mDecoratedChildHeight) {
mFirstVisiblePosition %= totalColumnCount
childTop = verticalPadding
if (mFirstVisiblePosition + mVisibleColumnCount > state.itemCount) {
mFirstVisiblePosition = (state.itemCount - mVisibleColumnCount).coerceAtLeast(0)
childLeft = horizontalPadding
}
}
val maxFirstRow = totalRowCount - (mVisibleRowCount - 1)
val maxFirstCol = totalColumnCount - (mVisibleColumnCount - 1)
val isOutOfRowBounds = firstVisibleRow > maxFirstRow
val isOutOfColBounds = firstVisibleColumn > maxFirstCol
if (isOutOfRowBounds || isOutOfColBounds) {
val firstRow: Int = if (isOutOfRowBounds) {
maxFirstRow
} else {
firstVisibleRow
}
val firstCol: Int = if (isOutOfColBounds) {
maxFirstCol
} else {
firstVisibleColumn
}
mFirstVisiblePosition = firstRow * totalColumnCount + firstCol
childLeft = horizontalSpace - mDecoratedChildWidth * mVisibleColumnCount
childTop = verticalSpace - mDecoratedChildHeight * mVisibleRowCount
if (firstVisibleRow == 0) {
childTop = childTop?.coerceAtMost(verticalPadding) ?: verticalSpace
}
if (firstVisibleColumn == 0) {
childLeft = childLeft?.coerceAtMost(horizontalPadding) ?: horizontalSpace
}
}
}
//Clear all attached views into the recycle bin
detachAndScrapAttachedViews(recycler)
//Fill the grid for the initial layout of views
val finalChildLeft = childLeft
val finalChildTop = childTop
if (finalChildLeft != null && finalChildTop != null) {
fillGrid(DIRECTION_NONE, finalChildLeft, finalChildTop, recycler, state, removedCache)
}
//Evaluate any disappearing views that may exist
if (!state.isPreLayout && recycler.scrapList.isNotEmpty()) {
recycler.scrapList
.map { it.itemView }
.filterNot { (it.layoutParams as LayoutParams).isItemRemoved }
.forEach { layoutDisappearingView(it) }
}
}
override fun onAdapterChanged(oldAdapter: RecyclerView.Adapter<*>?, newAdapter: RecyclerView.Adapter<*>?) {
//Completely scrap the existing layout
removeAllViews()
}
private fun updateWindowSizing() {
mVisibleColumnCount = horizontalSpace / mDecoratedChildWidth + 1
if (horizontalSpace % mDecoratedChildWidth > 0) {
mVisibleColumnCount++
}
//Allow minimum value for small data sets
mVisibleColumnCount = mVisibleColumnCount.coerceAtMost(totalColumnCount)
mVisibleRowCount = verticalSpace / mDecoratedChildHeight + 1
if (verticalSpace % mDecoratedChildHeight > 0) {
mVisibleRowCount++
}
mVisibleRowCount.coerceAtMost(totalRowCount)
}
private fun fillGrid(direction: Int, recycler: Recycler, state: RecyclerView.State) {
fillGrid(direction, 0, 0, recycler, state, null)
}
private fun fillGrid(
direction: Int, emptyLeft: Int, emptyTop: Int,
recycler: Recycler,
state: RecyclerView.State,
removedPositions: SparseIntArray?
) {
mFirstVisiblePosition = mFirstVisiblePosition.coerceIn(0, itemCount - 1)
var startLeftOffset = emptyLeft
var startTopOffset = emptyTop
val viewCache = SparseArray<View?>(childCount)
if (childCount != 0) {
getChildAt(0)?.let { topView ->
startLeftOffset = getDecoratedLeft(topView)
startTopOffset = getDecoratedTop(topView)
}
when (direction) {
DIRECTION_START -> startLeftOffset -= mDecoratedChildWidth
DIRECTION_END -> startLeftOffset += mDecoratedChildWidth
DIRECTION_UP -> startTopOffset -= mDecoratedChildHeight
DIRECTION_DOWN -> startTopOffset += mDecoratedChildHeight
}
//Cache all views by their existing position, before updating counts
(0 until childCount).forEach { index ->
val position = positionOfIndex(index)
val child = getChildAt(index)
viewCache.put(position, child)
}
//Temporarily detach all views.
// Views we still need will be added back at the proper index.
(0 until viewCache.size())
.mapNotNull { index -> viewCache.valueAt(index) }
.forEach(::detachView)
}
when (direction) {
DIRECTION_START -> mFirstVisiblePosition--
DIRECTION_END -> mFirstVisiblePosition++
DIRECTION_UP -> mFirstVisiblePosition -= totalColumnCount
DIRECTION_DOWN -> mFirstVisiblePosition += totalColumnCount
}
/*
* 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.
*/
var leftOffset = startLeftOffset
var topOffset = startTopOffset
for (index in 0 until visibleChildCount) {
var nextPosition = positionOfIndex(index)
var offsetPositionDelta = 0
if (state.isPreLayout) {
var offsetPosition = nextPosition
removedPositions?.let {
for (offset in 0 until it.size()) {
//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.itemCount) {
//Item space beyond the data set, don't attempt to add a view
continue
}
//Layout this position
var view = viewCache[nextPosition]
if (view == null) {
view = recycler.getViewForPosition(nextPosition)
addView(view)
if (!state.isPreLayout) {
val layoutParams = view.layoutParams as LayoutParams
layoutParams.apply {
row = getGlobalRowOfPosition(nextPosition)
column = getGlobalColumnOfPosition(nextPosition)
}
}
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 (index % mVisibleColumnCount == mVisibleColumnCount - 1) {
leftOffset = startLeftOffset
topOffset += mDecoratedChildHeight
if (state.isPreLayout) {
layoutAppearingViews(
recycler,
view,
nextPosition,
removedPositions?.size() ?: 0,
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 (i in 0 until viewCache.size()) {
val removingView = viewCache.valueAt(i)
recycler.recycleView(removingView!!)
}
}
override fun scrollToPosition(position: Int) {
if (position >= itemCount) {
Log.e(TAG, "Cannot scroll to $position, item count is $itemCount")
return
}
mFirstVisiblePosition = position
removeAllViews()
requestLayout()
}
override fun smoothScrollToPosition(
recyclerView: RecyclerView,
state: RecyclerView.State,
position: Int
) {
if (position >= itemCount) {
Log.e(TAG, "Cannot scroll to $position, item count is $itemCount")
return
}
val scroller: LinearSmoothScroller = object : LinearSmoothScroller(recyclerView.context) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
val rowOffset = (getGlobalRowOfPosition(targetPosition) - getGlobalRowOfPosition(mFirstVisiblePosition))
val columnOffset = (getGlobalColumnOfPosition(targetPosition) - getGlobalColumnOfPosition(mFirstVisiblePosition))
return PointF((columnOffset * mDecoratedChildWidth).toFloat(), (rowOffset * mDecoratedChildHeight).toFloat())
}
}
scroller.targetPosition = position
startSmoothScroll(scroller)
}
override fun scrollHorizontallyBy(dx: Int, recycler: Recycler, state: RecyclerView.State): Int {
if (childCount == 0) {
return 0
}
val topView = getChildAt(0)
val bottomView = getChildAt(mVisibleColumnCount - 1)
val viewSpan = getDecoratedRight(bottomView!!) - getDecoratedLeft(topView!!)
if (viewSpan < horizontalSpace) {
return 0
}
val leftBoundReached = firstVisibleColumn == 0
val rightBoundReached = lastVisibleColumn >= totalColumnCount
val delta: Int = if (dx > 0) {
if (rightBoundReached) {
val rightOffset = horizontalSpace - getDecoratedRight(bottomView) + horizontalPadding
(-dx).coerceAtLeast(rightOffset)
} else {
-dx
}
} else {
if (leftBoundReached) {
val leftOffset = -getDecoratedLeft(topView) + horizontalPadding
(-dx).coerceAtMost(leftOffset)
} else {
-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 -delta
}
override fun canScrollHorizontally(): Boolean = true
override fun canScrollVertically(): Boolean = true
override fun scrollVerticallyBy(dy: Int, recycler: Recycler, state: RecyclerView.State): Int {
if (childCount == 0) {
return 0
}
val topView = getChildAt(0)
val bottomView = getChildAt(childCount - 1)
val viewSpan = getDecoratedBottom(bottomView!!) - getDecoratedTop(topView!!)
if (viewSpan < verticalSpace) {
return 0
}
val maxRowCount = totalRowCount
val topBoundReached = firstVisibleRow == 0
val bottomBoundReached = lastVisibleRow >= maxRowCount
val delta: Int = if (dy > 0) {
if (bottomBoundReached) {
val bottomOffset: Int = if (rowOfIndex(childCount - 1) >= maxRowCount - 1) {
(verticalSpace - getDecoratedBottom(bottomView) + verticalPadding)
} else {
verticalSpace - (getDecoratedBottom(bottomView) + mDecoratedChildHeight) + verticalPadding
}
(-dy).coerceAtLeast(bottomOffset)
} else {
-dy
}
} else {
if (topBoundReached) {
val topOffset = -getDecoratedTop(topView) + verticalPadding
(-dy).coerceAtMost(topOffset)
} else {
-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 -delta
}
override fun findViewByPosition(position: Int): View? =
(0 until childCount).firstOrNull { positionOfIndex(it) == position }?.let { getChildAt(it) }
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun generateLayoutParams(context: Context, attrs: AttributeSet): RecyclerView.LayoutParams {
return LayoutParams(context, attrs)
}
override fun generateLayoutParams(layoutParams: ViewGroup.LayoutParams): RecyclerView.LayoutParams {
return if (layoutParams is MarginLayoutParams) {
LayoutParams(layoutParams)
} else {
LayoutParams(layoutParams)
}
}
override fun checkLayoutParams(layoutParams: RecyclerView.LayoutParams): Boolean {
return layoutParams is LayoutParams
}
private fun layoutAppearingViews(
recycler: Recycler,
referenceView: View,
referencePosition: Int,
extraCount: Int,
offset: Int
) {
if (extraCount > 1) {
//FIXME: This code currently causes double layout of views that are still visible…
(1..extraCount)
.map { extra -> referencePosition + extra }
.filterNot { extraPosition ->
extraPosition < 0 || extraPosition >= itemCount
}
.forEach { extraPosition ->
val appearing = recycler.getViewForPosition(extraPosition)
addView(appearing)
val newRow = getGlobalRowOfPosition(extraPosition + offset)
val rowDelta = newRow - getGlobalRowOfPosition(referencePosition + offset)
val newCol = getGlobalColumnOfPosition(extraPosition + offset)
val colDelta = newCol - getGlobalColumnOfPosition(referencePosition + offset)
layoutTempChildView(appearing, rowDelta, colDelta, referenceView)
}
}
}
private fun layoutDisappearingView(disappearingChild: View) {
addDisappearingView(disappearingChild)
val layoutParams = disappearingChild.layoutParams as LayoutParams
val newRow = getGlobalRowOfPosition(layoutParams.viewAdapterPosition)
val rowDelta = newRow - layoutParams.row
val newCol = getGlobalColumnOfPosition(layoutParams.viewAdapterPosition)
val colDelta = newCol - layoutParams.column
layoutTempChildView(disappearingChild, rowDelta, colDelta, disappearingChild)
}
private fun layoutTempChildView(
child: View,
rowDelta: Int,
colDelta: Int,
referenceView: View
) {
val layoutTop = getDecoratedTop(referenceView) + rowDelta * mDecoratedChildHeight
val layoutLeft = getDecoratedLeft(referenceView) + colDelta * mDecoratedChildWidth
measureChildWithMargins(child, 0, 0)
layoutDecorated(
child, layoutLeft, layoutTop,
layoutLeft + mDecoratedChildWidth,
layoutTop + mDecoratedChildHeight
)
}
private fun getGlobalColumnOfPosition(position: Int): Int =
position % mTotalColumnCount
private fun getGlobalRowOfPosition(position: Int): Int =
position / mTotalColumnCount
private fun positionOfIndex(childIndex: Int): Int {
val row = childIndex / mVisibleColumnCount
val column = childIndex % mVisibleColumnCount
return mFirstVisiblePosition + row * totalColumnCount + column
}
private fun rowOfIndex(childIndex: Int): Int =
positionOfIndex(childIndex) / totalColumnCount
private val firstVisibleColumn: Int
get() = mFirstVisiblePosition % totalColumnCount
private val lastVisibleColumn: Int
get() = firstVisibleColumn + mVisibleColumnCount
private val firstVisibleRow: Int
get() = mFirstVisiblePosition / totalColumnCount
private val lastVisibleRow: Int
get() = firstVisibleRow + mVisibleRowCount
private val visibleChildCount: Int
get() = mVisibleColumnCount * mVisibleRowCount
private val totalColumnCount: Int
get() = if (itemCount < mTotalColumnCount) {
itemCount
} else {
mTotalColumnCount
}
private val totalRowCount: Int
get() {
return if (itemCount == 0 || mTotalColumnCount == 0) {
0
} else {
val maxRow = itemCount / mTotalColumnCount
//Bump the row count if it's not exactly even
if (itemCount % mTotalColumnCount != 0) {
maxRow + 1
} else {
maxRow
}
}
}
private val horizontalSpace: Int
get() = width
private val verticalSpace: Int
get() = height
companion object {
private val TAG = FixedGridLayoutManager::class.simpleName
// View Removal Constants
private const val REMOVE_VISIBLE = 0
private const val REMOVE_INVISIBLE = 1
// Fill Direction Constants
private const val DIRECTION_NONE = -1
private const val DIRECTION_START = 0
private const val DIRECTION_END = 1
private const val DIRECTION_UP = 2
private const val DIRECTION_DOWN = 3
}
class LayoutParams : RecyclerView.LayoutParams {
//Current row in the grid
var row = 0
//Current column in the grid
var column = 0
constructor(c: Context?, attrs: AttributeSet?) : super(c, attrs) {}
constructor(width: Int, height: Int) : super(width, height) {}
constructor(source: MarginLayoutParams?) : super(source) {}
constructor(source: ViewGroup.LayoutParams?) : super(source) {}
}
}