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

前言

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

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

别急,本篇文章就给大家带来一个我自用的通用ItemDecoration,支持linear grid staggered LayoutManager,支持横竖向、跨列等情况;支持边缘、横纵向分隔线不同宽度,使用也非常简单。

效果图和代码

代码

单个类可以直接使用,仓库包含demo

效果图 网格样式

20230621_173920.gif

瀑布流

20230621_174043.gif

这个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。

image.png

以第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
    }
}

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYFwKXjN' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片