RecyclerView:ItemDecoration(续)

518 阅读2分钟

View - RecyclerView(2):ItemDecoration 中附加的 LayoutItemDecoration 小工具对于 StaggeredGridLayoutManager 瀑布流布局是不适用的并且也没有处理布局方向的问题。重新改版了一下使其支持 orientationStaggeredGridLayoutManager。先附上代码:

/**
 * 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 判断就可以了。

StaggeredGridLayoutManagerGridLayoutManager 设置 ItemDeration 最重要的点都在于计算当前 ItemView 应被添加到的列 columnGridLayoutManager 计算 colum 很简单,因为它的布局是有序的(RTL \ LTR 依次添加):

val column = position % spanCount

StaggeredGridLayoutManager 会将 ItemView 添加到当前最短的一列。上代码。直接定位到 fill() 方法(Android 提供的 LayoutManagerView 的布局逻辑都在 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 循环添加 ItemViewRecyclerView 中,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 的值的。具体实现逻辑可以看上面的代码 ^_^