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