前言

2023已过半,才发现我已经大半年没写博客了,痛定思痛决定水一篇。

不知道大家平时干活的时候有没有被RecyclerView列表的分隔线困扰过,app里一般都会有各式各样的列表,横的竖的、网格、瀑布流款式的,每次要给列表设分隔线时都要自己写一个特化的ItemDecoration,既费事又难以复用,那能不能写一个适用大多数场景的ItemDecoration来减轻这类负担呢?

别急,本篇文章就给大家带来一个我自用的通用ItemDecoration,支撑linear grid staggered LayoutManager,支撑横竖向、跨列等状况;支撑边际、横纵向分隔线不同宽度,运用也十分简略。

效果图和代码

代码

单个类能够直接运用,库房包括demo

效果图 网格款式

写一个万用RecyclerView分隔线,支持linear grid staggered

瀑布流

写一个万用RecyclerView分隔线,支持linear grid staggered

这个ItemDecoration暂时没有完成分隔线上色,因为我觉得这种场景其实很少就把相关代码删掉了,要加的话建议经过继承完成。

完成和注意点

首要,因为要支撑横竖向,所以定义两个轴,主轴代表可滑动的那个轴,穿插轴代表另一个轴,这样无论是横向还是竖向都能坚持语义共同

// 主轴方向分割线宽度
protected var mainWidth = 0
// 穿插轴方向分割线宽度
protected var crossWidth = 0
// 边际宽度
protected var mainPadding = 0
protected var crossPadding = 0

主轴的距离

主轴的分隔线很简略,榜首行的item和最终一行的item设置边际距离,其他每个item在主轴同一方向上设置分隔线距离,关键点在于首行和末行的判别。

LinearLayoutManager状况下最简略,判别position是首个或者最终一个就ok了,但是GridLayoutManager和StaggeredGridLayoutManager都存在跨列问题。比如说列表有5列,但是榜首个item就占满了整行,那么本该在榜首行的2-5个item实际上就不在榜首行了;末行判别同理。

GridLayoutManager经过它的SpanSizeLookup来判别,groupIndex==0在首行,groupIndex==lastGroupIndex在最终一行

// 当时item在哪一行
val groupIndex = manager.spanSizeLookup.getSpanGroupIndex(position, spanCount)
// 最终一个item在哪一行
val lastGroupIndex = manager.spanSizeLookup.getSpanGroupIndex(size - 1, spanCount)

StaggeredGridLayoutManager相对费事一些,看下面的注释,spanIndex代表当时item在本行内的下标

val lp = view.layoutParams
if (lp is StaggeredGridLayoutManager.LayoutParams) {
    val spanCount = manager.spanCount
    // 前面没有跨列item时当时item的期望下标
    val exceptSpanIndex = position % spanCount
    // 实在的item下标
    val spanIndex = lp.spanIndex
    // position原归于榜首行而且此item之前没有跨列的状况,当时item才归于榜首行
    val isFirstGroup = position < spanCount && exceptSpanIndex == spanIndex
    var isLastGroup = false
    if (size - position <= spanCount) {
        // position原归于最终一行
        val lastItemView = manager.findViewByPosition(size - 1)
        if (lastItemView != null) {
            val lastLp = lastItemView.layoutParams
            if (lastLp is StaggeredGridLayoutManager.LayoutParams) {
                // 列表最终一个item和当时item的spanIndex差等于position之差说明它们之间没有跨列的状况,当时item归于最终一行
                if (lastLp.spanIndex - spanIndex == size - 1 - position) {
                    isLastGroup = true
                }
            }
        }
    }
}

接下来就很简略了,设置主轴上的距离

if (isFirstGroup) {
    // 是榜首行
    if (isVertical) {
        outRect.top = mainPadding
    } else {
        outRect.left = mainPadding
    }
} else if (isLastGroup) {
    // 是最终一行要加边际
    if (isVertical) {
        outRect.top = mainWidth
        outRect.bottom = mainPadding
    } else {
        outRect.left = mainWidth
        outRect.right = mainPadding
    }
} else {
    if (isVertical) {
        outRect.top = mainWidth
    } else {
        outRect.left = mainWidth
    }
}

穿插轴的距离

穿插轴的分隔线最简略的是LinearLayoutManager,因为不存在多列直接设置为边际距离就能够了

if (isVertical) {
    outRect.left = crossPadding
    outRect.right = crossPadding
} else {
    outRect.top = crossPadding
    outRect.bottom = crossPadding
}

GridLayoutManager和StaggeredGridLayoutManager的穿插轴分隔线计算方法是相同的,能够统一处理,需求遵循的规则有两个

  1. 每列占用的左右距离之和持平
  2. 每个item占用的右距离和它相邻item占用的左距离之和等于给定的距离宽度

以下图为例,列表共4列,边际距离是15,item距离是10,第二个item跨两列,每列应该占用的空间为15。

写一个万用RecyclerView分隔线,支持linear grid staggered

以第3个item为例,怎么计算出它的左距离和右距离,公式如下

左距离:到当时item的左面停止的总距离(crossWidth * spanIndex + crossPadding)减去 到上一个item停止需求运用的总距离(spanUsedWidth * spanIndex),这个比如中这两个值持平

同理右距离:到当时item停止需求运用的总距离(spanUsedWidth * (spanIndex + spanSize)) 减去 到当时item右边停止的总距离(crossWidth * (spanIndex + spanSize – 1) + crossPadding);当然也能够用 当时item需求运用的总距离( spanUsedWidth * spanSize) – 当时item已经运用的总距离( crossWidth * (spanSize – 1) + lt)

这样经过归纳只运用两行代码就统合了一切状况

/**
 * 穿插轴距离
 * [spanIndex] 当时item的以第几列开端
 * [spanSize] 当时item占用的列数
 */
private fun getItemCrossOffsets(outRect: Rect, isVertical: Boolean, spanCount: Int, spanIndex: Int, spanSize: Int) {
    // 每列占用的距离
    val spanUsedWidth = (crossPadding * 2 + crossWidth * (spanCount - 1)) / spanCount
    // 到当时item的左面停止的总距离 - 到上一个item停止需求运用的总距离
    val lt = crossWidth * spanIndex + crossPadding - spanUsedWidth * spanIndex
    // 到当时item停止需求运用的总距离 - 到当时item右边停止的总距离
//        val rb = spanUsedWidth * (spanIndex + spanSize) - crossWidth * (spanIndex + spanSize - 1) - crossPadding
    // 当时item需求运用的总距离 - 当时item已经运用的总距离
    val rb = spanUsedWidth * spanSize - crossWidth * (spanSize - 1) - lt
    if (isVertical) {
        outRect.left = lt
        outRect.right = rb
    } else {
        outRect.top = lt
        outRect.bottom = rb
    }
}