RecyclerView使用ItemDecoration处理GridLayoutManager的多列间距

1.问题描述

在工作中遇到一个需求,需要写一个列表,要求每行三个子Item,且子Item均匀分布。
初看很简单,RecyclerView使用GridLayoutManager直接设置3列即可。
设置如下:

recyclerView?.layoutManager = GridLayoutManager(this,3)
recyclerView?.adapter = mAdapter

这时问题出现了,本来期望的是
|[] [] []|
但现在呈现的是
|[] [] [] |
|代表边缘,[]代表子Item。

2.解决方法:ItemDecoration

上面问题在于子Item彼此间的距离有问题,因此采用ItemDecoration来计算得出合适的间距。
自定义ItemDecoration时,有几个注意点:

  • 距离的设置要重写getItemOffsets,背景色或额外的图形要重写onDraw
  • getItemOffsets会在每个子Item测量绘制时都执行一次,onDraw只会执行一次。
  • fun getItemOffsets(outRect: Rect,view: View,parent: RecyclerView,state: RecyclerView.State)
    • outRect可以设置子Item的四个方向的偏移。但一般水平方向或竖直方向只设置一个值即可达到移动的目的。
  • 在默认情况下,recyclerview的竖直方向是可以延伸的,但水平方向不会。两个方向的间距计算不一样。
    • 竖直方向比较简单,偏移的距离是在前面子Item的基础上偏移。因此直接设置偏移距离就可以。
    • 水平方向的计算稍微麻烦一点,是在原布局基础上进行的偏移,而不是完全从左到右式的自动布局。
3.水平方向间距的计算

首先引入几个变量来方便计算和描述。

  • spanCount:列数,或者说一行的子Item数量。
  • column:子Item所在列,大小为0到spanCount-1.。
  • totalWidth:一行可布局宽度。由于参数里有父View parent,因此可以直接得到。
  • itemWidth:子Item的宽度。在getItemOffsets执行时,子Item还未计算出宽度,因此需要从外面直接传进来供使用。
    • 注意:本篇是在子Item宽度固定的情况下进行的推导,对于宽度自适应的子Item未必适用,请自行探索。

原布局为:|[] [] [] |
期望布局:|[] [] []|
两个行布局的总宽度是相同的,子Item个数也是相同的,差别在原布局有的间距个数是spanCount,期望布局的间距个数则是spanCount-1。
这里注意原布局的间距个数是包含了最右边的子Item的右边缘right到父View的右边缘的间距的。
增加两个变量来代表两个间距。

  • oriSpace:原布局的间距,易得出oriSpace = totalWidth/spanCount - itemWidth
  • averageHorizontalSpace:期望布局的间距,易得出averageHorizontalSpace = (totalWidth - itemWidth*spanCount)/(spanCount - 1)
    对于一个column列的子Item来说,到父View的距离,也就是自己的左边缘水平方向坐标,两布局分别为
    column*(itemWidth + oriSpace)column*(itemWidth + averageHorizontalSpace)
    两者的差值即为子Item应移动的距离:
    column * (averageHorizontalSpace - oriSpace)
    这也就是getItemOffsets里outRect应设置的left。
4.多想一步

如果父View不留paddingLeft和right,所有距离完全由子Item均匀布局呢,也就是如下示意图
| [] [] [] |
这个期望布局里,最左最右两个子Item到父View的边缘的间距为中间间距的一半。
思考推导方式是一样的。
中间间距和原布局相比是一样的,都是
averageHorizontalSpace = totalWidth/spanCount - itemWidth
此时averageHorizontalSpace = oriSpace
新布局里,在column列的子Item的left为
column * (itemWidth + averageHorizontalSpace) + averageHorizontalSpace/2
和原布局相比,left差值为averageHorizontalSpace/2
也就是每个子Item的outRect.left为固定值averageHorizontalSpace/2
想想也是,间距相同的话,不就是整体移动了开始的那块距离嘛。

5.Show me the code

下面为自定义ItemDecoration的代码。
请注意下里面额外加了点父View的paddingLeft和right的计算,但并不影响上面的推导流程。

package ***.***.***

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.util.Log
import android.view.View
import androidx.recyclerview.widget.RecyclerView

/**
 * 适用于父类和子Item宽度固定,确保子Item可以均匀分布在一行内的布局
 * itemWidth为子Item的宽度
 * rowSpacing为每行之间的间距
 * isFirstLeftAndEndRightZero,是否每行首尾两个子Item紧贴父View的边缘。
 * 是的话,需要时在外面单独设置父View的paddingLeft和right即可。适用于子Item外面背景不要求展示下相同背景的情况
 * 否的话,就是每个子Item的左右边距相同,适用于子Item外面背景展示相同大小。此时一般还需要重写onDraw
 *
 */
class GridAverageSpaceItemDecoration (val spanCount:Int,val itemWidth:Int,val rowSpacing:Int,val isFirstLeftAndEndRightZero:Boolean = true):
    RecyclerView.ItemDecoration() {

    private var averageHorizontalSpace:Int = 0

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position = parent.getChildAdapterPosition(view)
        val column = position%spanCount
        var totalWidth = parent.width
        averageHorizontalSpace = 0
        //初始位置时的子Item之间的边距,后面的移动是在此摆放条件下移动的,所以要根据不同目的进行计算得到每个子Item移动的位置
        if (isFirstLeftAndEndRightZero){
            //父View设置了左右边距或不需要,每一行第一个子View的左边距和最后一个子View的右边距是0
            totalWidth = totalWidth - parent.paddingLeft - parent.paddingRight
            averageHorizontalSpace = (totalWidth - itemWidth*spanCount)/(spanCount - 1)
            val oriSpace = totalWidth/spanCount - itemWidth
            outRect.left = column * (averageHorizontalSpace - oriSpace)
        } else {
            //父View没有设置左右边距或不需要,每一行第一个子View的左边距和最后一个子View的右边距是彼此之间间隙的一半
            averageHorizontalSpace = (totalWidth - itemWidth*spanCount)/spanCount
            outRect.left = averageHorizontalSpace/2
            //只需要移动子Item时,就不用再设置right了。
            //需要绘制每个子Item的外面背景时,需要重写onDraw
            //outRect.right = averageHorizontalSpace/2
        }

        //纵向,每行之间的距离,第一行不设置
        if (position >= spanCount) {
            outRect.top = rowSpacing
        }
    }


    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
        if (!isFirstLeftAndEndRightZero){
            val paint = Paint()
            for (i in 0 until parent.childCount){
                val child = parent.getChildAt(i)
                paint.color = getColor(i)
                c.drawRect(child.left.toFloat()-averageHorizontalSpace/2, child.top.toFloat()-rowSpacing/2,
                    child.right.toFloat()+averageHorizontalSpace/2, child.bottom.toFloat()+rowSpacing/2,paint)
            }
        }

    }

    private fun getColor(index:Int):Int{
        val colorList = mutableListOf<Int>(Color.RED,Color.GREEN,Color.BLUE,Color.GRAY)
        return colorList[index%colorList.size]
    }

}

引用ItemDecoration代码如下

recyclerView?.addItemDecoration(
    GridAverageSpaceItemDecoration(3,dp2px(100f),dp2px(16f))
)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值