在 View - RecyclerView(2):ItemDecoration 中附加的 LayoutItemDecoration 小工具对于 StaggeredGridLayoutManager 瀑布流布局是不适用的并且也没有处理布局方向的问题。重新改版了一下使其支持 orientation 和 StaggeredGridLayoutManager。先附上代码:
/**
* date: 2020/11/28
* author: ice_coffee
* remark: 通用 ItemDecoration 工具
* @param spacing: 间距
* @param includeEdge: 是否设置边缘间距
*/
class LayoutItemDecoration(private val spacing: Int, private val includeEdge: Boolean) : RecyclerView.ItemDecoration() {
/**
* position 和对应 view 所在列的对应关系
*/
private var positionToColumn: MutableMap<Int, Int>? = null
/**
* 下标对应列, 值对应某列当前最远的距离
*/
private var endLineToColum: IntArray? = null
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
//item view 总数
val itemCount = parent.adapter?.itemCount ?: 0
//当前 item view 的 adapter 位置
val position = parent.getChildAdapterPosition(view)
when (val layoutManager = parent.layoutManager) {
is GridLayoutManager -> {
setGridItemDecoration(layoutManager, outRect, layoutManager.spanCount, position, itemCount)
}
is StaggeredGridLayoutManager -> {
setStaggeredGridItemDecoration(layoutManager, outRect, layoutManager.spanCount, position, itemCount)
}
is LinearLayoutManager -> {
setLinearItemDecoration(outRect, layoutManager.orientation, position, itemCount)
}
else -> {
outRect.set(spacing, spacing, spacing, spacing)
}
}
}
/**
* 设置线性布局分割线
*/
private fun setLinearItemDecoration(outRect: Rect, orientation: Int, position: Int, itemCount: Int) {
if (orientation == LinearLayoutManager.HORIZONTAL) {
val top = if (includeEdge) spacing else 0
val bottom = if (includeEdge) spacing else 0
when (position) {
0 -> outRect.set(if (includeEdge) spacing else 0, top, spacing, bottom)
itemCount - 1 -> outRect.set(0, top, if (includeEdge) spacing else 0, bottom)
else -> outRect.set(0, top, spacing, bottom)
}
} else {
val left = if (includeEdge) spacing else 0
val right = if (includeEdge) spacing else 0
when (position) {
0 -> outRect.set(left, if (includeEdge) spacing else 0, right, spacing)
itemCount - 1 -> outRect.set(left, 0, right, if (includeEdge) spacing else 0)
else -> outRect.set(left, 0, right, spacing)
}
}
}
/**
* 设置网格布局分割线
*/
private fun setGridItemDecoration(layoutManager: GridLayoutManager, outRect: Rect, spanCount: Int, position: Int, itemCount: Int) {
//当前列
val column = position % spanCount
calculateGridOutRectByColum(outRect, spanCount, itemCount, position, layoutManager.orientation, column)
}
/**
* 设置瀑布流布局分割线
*/
private fun setStaggeredGridItemDecoration(layoutManager: StaggeredGridLayoutManager, outRect: Rect, spanCount: Int, position: Int, itemCount: Int) {
if (null == positionToColumn) {
positionToColumn = HashMap()
}
if (null == endLineToColum) {
endLineToColum = IntArray(spanCount)
}
//当前 view 应处的列
var column = 0
if (positionToColumn!!.containsKey(position)) {
column = positionToColumn!![position]!!
} else {
if (position > 0) {
val lastViewPosition = position - 1
val lastView = layoutManager.findViewByPosition(lastViewPosition)
if (null != lastView) {
val lastViewColumn = positionToColumn!![lastViewPosition]!!
if (layoutManager.orientation == StaggeredGridLayoutManager.VERTICAL) {
endLineToColum!![lastViewColumn] = endLineToColum!![lastViewColumn] + lastView.height
} else {
endLineToColum!![lastViewColumn] = endLineToColum!![lastViewColumn] + lastView.width
}
}
}
//首行判断
if (position < spanCount) {
column = position
} else {
var minLine = Int.MAX_VALUE
for (i in spanCount - 1 downTo 0) {
if (endLineToColum!![i] <= minLine) {
minLine = endLineToColum!![i]
column = i
}
}
}
positionToColumn!![position] = column
}
calculateGridOutRectByColum(outRect, spanCount, itemCount, position, layoutManager.orientation, column)
}
/**
* 计算间隔
*/
private fun calculateGridOutRectByColum(outRect: Rect, spanCount: Int, itemCount: Int, position: Int, orientation: Int, column: Int) {
//是否需要包含边界
if (includeEdge) {
if (orientation == StaggeredGridLayoutManager.VERTICAL) {
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
//第一行判断
if (position < spanCount) {
outRect.top = spacing
}
outRect.bottom = spacing
} else {
outRect.top = spacing - column * spacing / spanCount
outRect.bottom = (column + 1) * spacing / spanCount
//第一行判断
if (position < spanCount) {
outRect.left = spacing
}
outRect.right = spacing
}
} else {
if (orientation == StaggeredGridLayoutManager.VERTICAL) {
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
outRect.top = 0
//最后一行判断
if (position + spanCount < itemCount) {
outRect.bottom = spacing
}
} else {
outRect.top = column * spacing / spanCount
outRect.bottom = spacing - (column + 1) * spacing / spanCount
outRect.left = 0
//最后一行判断
if (position + spanCount < itemCount) {
outRect.right = spacing
}
}
}
}
}
支持 orientation 比较简单,配合 LayoutManager.orientation 判断就可以了。
StaggeredGridLayoutManager 和 GridLayoutManager 设置 ItemDeration 最重要的点都在于计算当前 ItemView 应被添加到的列 column。
GridLayoutManager 计算 colum 很简单,因为它的布局是有序的(RTL \ LTR 依次添加):
val column = position % spanCount
而 StaggeredGridLayoutManager 会将 ItemView 添加到当前最短的一列。上代码。直接定位到 fill() 方法(Android 提供的 LayoutManager 子 View 的布局逻辑都在 fill() 方法中)。
private int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state) {
。。。
while (layoutState.hasMore(state)
&& (mLayoutState.mInfinite || !mRemainingSpans.isEmpty())) {
View view = layoutState.next(recycler);
LayoutParams lp = ((LayoutParams) view.getLayoutParams());
final int position = lp.getViewLayoutPosition();
final int spanIndex = mLazySpanLookup.getSpan(position);
Span currentSpan;
final boolean assignSpan = spanIndex == LayoutParams.INVALID_SPAN_ID;
if (assignSpan) {
currentSpan = lp.mFullSpan ? mSpans[0] : getNextSpan(layoutState);
mLazySpanLookup.setSpan(position, currentSpan);
if (DEBUG) {
Log.d(TAG, "assigned " + currentSpan.mIndex + " for " + position);
}
} else {
if (DEBUG) {
Log.d(TAG, "using " + spanIndex + " for pos " + position);
}
currentSpan = mSpans[spanIndex];
}
}
。。。
}
可以看到 fill() 方法中通过 while 循环添加 ItemView 到 RecyclerView 中,currentSpan 即代表从 layoutState.next(recycler) 中获取的 View 应被添加到那列中。接下来查看 getNextSpan() 它是获取 currentSpan 的重要方法。
private Span getNextSpan(LayoutState layoutState) {
final boolean preferLastSpan = preferLastSpan(layoutState.mLayoutDirection);
final int startIndex, endIndex, diff;
if (preferLastSpan) {
startIndex = mSpanCount - 1;
endIndex = -1;
diff = -1;
} else {
startIndex = 0;
endIndex = mSpanCount;
diff = 1;
}
if (layoutState.mLayoutDirection == LAYOUT_END) {
Span min = null;
int minLine = Integer.MAX_VALUE;
final int defaultLine = mPrimaryOrientation.getStartAfterPadding();
for (int i = startIndex; i != endIndex; i += diff) {
final Span other = mSpans[i];
int otherLine = other.getEndLine(defaultLine);
if (otherLine < minLine) {
min = other;
minLine = otherLine;
}
}
return min;
} else {
Span max = null;
int maxLine = Integer.MIN_VALUE;
final int defaultLine = mPrimaryOrientation.getEndAfterPadding();
for (int i = startIndex; i != endIndex; i += diff) {
final Span other = mSpans[i];
int otherLine = other.getStartLine(defaultLine);
if (otherLine > maxLine) {
max = other;
maxLine = otherLine;
}
}
return max;
}
}
可以看到 getNextSpan() 方法中的逻辑是通过遍历所有列找到当前长度最短(长度代表所添加子元素的长度总和,具体是高还是宽的总和看布局方向)的一列并返回。然后就是根绝所获取的 Span 执行
- 添加:
addView() - 测量:
measureChildWithDecorationsAndMargin() - 布局:
layoutDecoratedWithMargins()
等一系列操作。
可以看到 StaggeredGridLayoutManager 会将 ItemView 添加到最短列中,那么如何确定当前 ItemView 应被添加到那列呢。我是参考 getNextSpan() 的逻辑,通过记录每列当前的长度来确定 colum 的值的。具体实现逻辑可以看上面的代码 ^_^。