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去实现。

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

相关推荐
Cao_Shixin攻城狮2 小时前
Flutter运行Android项目时显示java版本不兼容(Unsupported class file major version 65)的处理
android·java·flutter
呼啦啦呼啦啦啦啦啦啦5 小时前
利用pdfjs实现的pdf预览简单demo(包含翻页功能)
android·javascript·pdf
Wendy14417 小时前
【线性回归(最小二乘法MSE)】——机器学习
算法·机器学习·线性回归
拾光拾趣录7 小时前
括号生成算法
前端·算法
idjl7 小时前
Mysql测试题
android·adb
渣呵8 小时前
求不重叠区间总和最大值
算法
拾光拾趣录8 小时前
链表合并:双指针与递归
前端·javascript·算法
好易学·数据结构8 小时前
可视化图解算法56:岛屿数量
数据结构·算法·leetcode·力扣·回溯·牛客网
香蕉可乐荷包蛋9 小时前
AI算法之图像识别与分类
人工智能·学习·算法
游戏开发爱好者810 小时前
iOS App 电池消耗管理与优化 提升用户体验的完整指南
android·ios·小程序·https·uni-app·iphone·webview