注册

Android ReyclerView分割线竟然暗藏算法

前言


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

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


结论


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


image.png


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


1. 简单的处理分割线


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


<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>

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


rv.addItemDecoration(object : ItemDecoration() {

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


image.png


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


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更多


image.png


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


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


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
}
})

image.png


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


image.png


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


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


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


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


image.png


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


image.png


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


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
}
}
})


image.png


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


image.png


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


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


image.png


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


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


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


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


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


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


image.png


图画得不太准,因为准的不好手动画,假设看成间距和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能玩这么花。


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


        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


可以看看效果


image.png


image.png


image.png


image.png


image.png


可以看到是均分的啦。


优化


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


image.png


所以正常使用判断要改下



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


image.png


size为8,n为3


image.png


除此之外,还可以看出这个算法的复杂度是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去实现。


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


作者:流浪汉kylin
来源:juejin.cn/post/7314142205684776998

0 个评论

要回复文章请先登录注册