Android ReyclerView分割线竟然暗藏算法

前言

事情是这样的,前段时间正好有个RecyclerView用GridLayoutManager实现网格布局的需求,然后要做分割线,一般这种都是信手捏来的东西,然后我发现这个分割线竟然对不齐。

当然,如果要实现这样的功能,会有很多种方法,包括在itemView加margin、padding等等,都能有办法去实现分割线的效果,但是我这种人就是非要弄清楚其中的问题才舒服。

结论

因为涉及到算法,可能要讲得比较多,所以先说说最终的结论,先看看效果

就是实现这种有分割线并均分布局的效果,我研究到最后发现竟然不是简单一两句代码能解决的,其中还暗藏玄机。这里我处理这个问题会涉及一个算法,所以最终会得到一个公式,我不能保证我的公式是最优的解法,如果有其它更好的公式也可以留言告诉我。

1. 简单的处理分割线

我这里的场景是ItemView是填充,意思就是填充除了分割线以外的布局。

ini 复制代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="match_parent"  
    android:layout_height="100dp"  
    android:background="@color/purple_200"  
    android:orientation="vertical">  
  
</LinearLayout>

假如我一开始要做分割线,我简单的去做,会是这样的效果

kotlin 复制代码
rv.addItemDecoration(object : ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        outRect.left = 60
    }
})

然后你会很自然而然的想这个做

kotlin 复制代码
rv.addItemDecoration(object : ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val pos = parent.getChildAdapterPosition(view)
        if (pos != 0) {
            outRect.left = 60
        }
    }
})

然后你会发现此时的布局不均分,第一个item更多

注意,我这里的处理问题思路是必须用分割线处理,不然用一些方法确实能更快做到,比如上面的情况,我加个padding也能做,但我这里的思路是要完全用outRect去处理这个问题

看到上面的效果和想象中的不同,没关系,我换个思路,我左右都加间距

kotlin 复制代码
rv.addItemDecoration(object : ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val pos = parent.getChildAdapterPosition(view)
        outRect.left = 30
        outRect.right = 30
    }
})

可以看到是均分了,但是如果我的场景需要首尾两个item贴边,那这样就不合适,但是你可能会很快的想到这样做,去判断首尾Item

恭喜你,又失败了,可以看到布局又不是均分了。如果你一直按照这样的简单思路去想,是无法处理这个问题的,因为他不是一个简单的公式就能解决的,所以简单的去思考,也只是浪费时间。

首先需要的是理解他的原理

2. 设置分割线getItemOffsets方法的原理

这里简单讲,不是看源码,而是通过图片去分析(我就简单画点图,可能不是很标准,将就着看)

红色是内容,白色是间距,如果不设置的话,红色的区域就是整个白色,可以抽象的理解成它是往内去缩的,所以如果第一个Item不设置Left,最后一个Item不设置Right,他的效果就会是这样

这就是上面Demo的最后一种情况,这里给你们看一个很有意思的现象,假如我的代码这样写(在3列的情况下)

kotlin 复制代码
rv.addItemDecoration(object : ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val pos = parent.getChildAdapterPosition(view)
        if (pos == 0) {
            outRect.right = 40
        } else if (pos == 2) {
            outRect.left = 40
        } else {
            outRect.left = 20
            outRect.right = 20
        }
    }
})

可以看到这样就均分了,是不是很神奇,其实这里用图来画出来是这样的

间距是由一个Item更大的间距加上一个Item略小的间距实现的。

你可能会想,懂了,除了首尾之外,其他的就是填一半间距。真的这么简单吗,可以看看4个效果,同样的代码如果把列数从3个变成4个

你就会发现,中间的分割线会更小一点,你可以算算看,左边的分割线是 40 + 20,而中间的分割线是 20 + 20 ,所以不同。 所以我说这个问题不会这个简单

其实当时我处理不了又比较赶时间,我就去google查,找了几个老哥的代码直接拷贝下来用发现用不了,所以我看深入去思考这个问题。

3. 真正的实现分割线均分布局的操作

来了,重点来了,通过上面的原理你能知道,其实就是把首尾两个Item应该多出的间距,平均分配到每个分割线。但是它不会是一个简单的计算,会是一个偏复杂的问题,数据问题。

当我把他变成数学问题,这个问题就是,我给出固定的分割线宽度,你需要分割线宽度相同,Item的宽度也相同,注意是两个相同,这是一个解题的条件

这个问题如果从正向去解释,我觉得很难说清楚,所以我从反向来解答,假如我有10列(我这里为了方便,先用一行来举例

图画得不太准,因为准的不好手动画,假设看成间距和Item宽度都相同。我10列那就是有9个间距(9个分割线),假设每条间距是10

那我是不是可以这样分:

间距1:(L1)9 + (R1)1

间距2:(L2)8 + (R2)2

间距3:(L3)7 + (R3)3

间距4:(L4)6 + (R4)4

间距5:(L5)5 + (R5)5

间距6:(L6)4 + (R6)6

间距7:(L7)3 + (R7)7

间距8:(L8)2 + (R8)8

间距9:(L9)1 + (R9)9

他们的间距都不同,但是他们加起来都是10,这是满足了第一个条件,分割线间距相同,还有一个条件,他们的Item宽要相同

从上面的原理我们知道,Item的最终宽度就是总宽度减去左右间距的宽度。Item1的左间距是0,右间距是9,它的宽度是AllWeidth - 9 ,Item2的左间距是1,右间距是8,它的宽度是AllWeidth - (8+1),和Item1是相同的,你可以算算其他的,也是相同的。所以这样就能达到一个均分的效果。

OK,我们来凑公式。上面说过,其实这种场景就相当于10个分割线的间距,把其中一个间距分成每一份去加到其他的间距中,而每一份其实就是最小的份,你看看上面的10列,每一份就是1,所以得出一个公式

min = space / n

然后有了最小,我们还需要算出一个最大的Item的间距,从间距相等我们得知

max = space - min

等理解这两个公式之后,我们再往下看。假设我就拿前面2个Item做分析

L1 = 0 // 最左边的Item没左间距这个应该很容易理解吧
R1 = max // 从上面的模型你能看出,Item1的右间距是最大间距
L2 = space - R1 // 根据间距相等这个条件,R1确认了,L2自然就确认
R2 = max - L2 // 这个是什么意思呢,这个是保证Item的宽度相同,从这个条件根据L2来算出R2。简单来说就是根据第一个Item你知道总间距,你后面的Item也要根据左右间距加起来得到的总间距相等

后一个值要根据前一个值的结果算出,是不是很熟悉,介不是某大厂特别喜欢考的动态规划吗?我见过直接算法题的动态规划,倒是第一次见结合到代码场景里面的,没想到一个小小的RecyclerView能玩这么花。

动态规划,老熟人了,我们能根据上面的分析推出一个公式

ini 复制代码
        rv.addItemDecoration(object : ItemDecoration() {

            override fun getItemOffsets(
                outRect: Rect,
                view: View,
                parent: RecyclerView,
                state: RecyclerView.State
            ) {
                super.getItemOffsets(outRect, view, parent, state)
                val pos = parent.getChildAdapterPosition(view)

                val min : Float = space / n
                val max = space - min

                if (pos == 0) {
                    outRect.right = max.toInt()
                } else if (pos == (n - 1)) {
                    outRect.left = max.toInt()
                } else {
                    var index = 1
                    var oldLeft = 0
                    var oldRight = max
                    while (index <= pos) {
                        val left = space - oldRight
                        val right = max - left
                        oldLeft = left.toInt()
                        oldRight = right
                        index++
                    }
                    outRect.left = oldLeft
                    outRect.right = oldRight.toInt()
                }

            }

        })

这里的pos == 0这些判断是在我只有1行的前提下才这么演示的,实际别这么写。

现在分析下代码,space是间距宽度,n是列数,min和max上面分析过了,pos == 0只有右边间距并且为max,pos是最后一个只有左边间距并且为max,这个就不用解释了,主要是最后的else

当前的Item的间距需要根据前一个Item的间距算出,所以这里我用了循环,holdLef和oldRight表示前一个Item的左间距和右间距。然后就是用我们推出的公式去计算

Ln = space - R(n-1)
Rn = max - Ln

可以看看效果

可以看到是均分的啦。

优化

本来不想说pos == 0这个判断的,我怕有人直接拉代码出问题说我。上面的代码pos == 0只是为了方便演示1行的情况,如果我在多行用

所以正常使用判断要改下

ini 复制代码
                if (pos % n == 0) {
                    outRect.right = max.toInt()
                } else if ((pos + 1)  % n == 0) {
                    outRect.left = max.toInt()
                } else {
                    var index = 1
                    var oldLeft = 0
                    var oldRight = max
                    while (index <= (pos % n) ) {
                        val left = space - oldRight
                        val right = max - left
                        oldLeft = left.toInt()
                        oldRight = right
                        index++
                    }
                    outRect.left = oldLeft
                    outRect.right = oldRight.toInt()
                }

size为10,n为5

size为8,n为3

除此之外,还可以看出这个算法的复杂度是O(m*n)

因为getItemOffsets是一个循环,里面的while又是一个循环,所以这里可以优化,我有一个想法,可以用hashmap通过空间来换时间,而且你会发现超过n/2的Item都是之前反着的,所以用hashmap的话你只需要记录第一个行的一半Item的间距,我觉得还是很不错的

还要注意一点,计算时要用Float,最后再转Int,否则全程用Int算可能有点偏差

总结

首先写这篇文章的目的是觉得这其中的算法非常有意思,这个动态规划的过程要推导出这个公式,整个推导的过程能在这其中感受到开发的快乐,所以记住这个公式

L0 = 0
R0 = max
Ln = space - R(n-1)
Rn = max - Ln

其次,我也不敢保证我这个是最佳的解法、最佳的公式,但是我测试目前来看是没问题,所以想用的话可以直接把代码拷去用,当然通过其他的方式也是能处理的,不一定要把思维限制在必须使用ItemDecoration去实现。

解算法的过程是痛苦的,但是解出来之后,那就非常的爽

相关推荐
shinelord明2 分钟前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
დ旧言~8 分钟前
专题八:背包问题
算法·leetcode·动态规划·推荐算法
_WndProc25 分钟前
C++ 日志输出
开发语言·c++·算法
努力学习编程的伍大侠38 分钟前
基础排序算法
数据结构·c++·算法
XiaoLeisj1 小时前
【递归,搜索与回溯算法 & 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)
数据结构·算法·leetcode·决策树·深度优先·剪枝
Jasmine_llq1 小时前
《 火星人 》
算法·青少年编程·c#
闻缺陷则喜何志丹2 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
Lenyiin2 小时前
01.02、判定是否互为字符重排
算法·leetcode
鸽鸽程序猿2 小时前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列