最近在做需求的时候,碰到有各种筛选项的界面,类似这样
这种筛选界面也比较常见,一般我们都采用RecyclerView来实现,当筛选项比较少的时候,我们选择使用线性布局来实现,当筛选项比较多,但是每个筛选项长度一样时,我们可以采用网格布局。但是这次的需求,筛选项有长有短,并且当每一行还有空间时,不能进行换行,因此RecyclerView自带的瀑布流布局也不能满足需求。
我们知道RecyclerView的布局是由LayoutManager负责,因此如果要实现这种效果,我把它称之为流式布局,需要自定义LayoutManager。首先我们来看一下LayoutManager的几个重要的方法
方法名 | 作用 |
isAutoMeasureEnabled | 这个方法表示是否采用自动测量,一般都是true,否则就需要自己重写onMeasure方法。 |
generateDefaultLayoutParams | 这是个抽象方法,用于给ItemView设置布局参数,必须实现。 |
onMeasure |
测量RecyclerView大小 |
onLayoutChildren | 对ItemView进行布局,Item如何摆放,全看这个方法如何实现 |
那么我们要重写LayoutManager就简单需要以下几个步骤
override fun isAutoMeasureEnabled(): Boolean {
return true
}
只需要返回true,代表RecyclerView自动测量。RecyclerView的三个默认实现的布局管理器也是返回true的。
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
}
没有什么特殊要求,直接返回自适应即可,给Item设置默认的布局参数。这个方式必须实现。
由于isAutoMeasureEnabled直接返回true,因此没有必要重写onMeasure方法,启动自动测量。但我们需要重点实现onLayoutChildren方法,这是我们自定义LayoutManager的核心所在。
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State?) {
//这里是在布局前将所有在显示的HoldView从RecyclerView中剥离,将其放在缓存中,方便重新布局时使用。
detachAndScrapAttachedViews(recycler)
var curLineWidth = 0 //curLineWidth 累加item布局时的x轴偏移
var curLineTop = 0 //curLineTop 累加item布局时的y轴偏移
var lastLineMaxHeight = 0
for (i in 0 until itemCount) {
val view: View = recycler.getViewForPosition(i)
//获取每个item的布局参数,计算每个item的占用位置时需要加上margin
val params = view.layoutParams as RecyclerView.LayoutParams
addView(view)
measureChildWithMargins(view, 0, 0)
val width = getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin
val height = getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin
curLineWidth += width //累加当前行已有item的宽度
if (curLineWidth <= getWidth()) { //如果累加的宽度小于等于RecyclerView的宽度,不需要换行
layoutDecorated(
view,
curLineWidth - width + params.leftMargin,
curLineTop + params.topMargin,
curLineWidth - params.rightMargin,
curLineTop + height - params.bottomMargin
) //布局item的真实位置
//比较当前行多有item的最大高度,用于换行后计算item在y轴上的偏移量
lastLineMaxHeight = Math.max(lastLineMaxHeight, height)
} else { //换行
curLineWidth = width
if (lastLineMaxHeight == 0) {
lastLineMaxHeight = height
}
//记录当前行top
curLineTop += lastLineMaxHeight
layoutDecorated(
view,
params.leftMargin,
curLineTop + params.topMargin,
width - params.rightMargin,
curLineTop + height - params.bottomMargin
)
lastLineMaxHeight = height
}
}
}
经过这三步就可以完整的实现一个自定义的LayoutManager。
下面是我自定义的流式布局完整代码
class FlowLayoutManager : RecyclerView.LayoutManager() {
override fun isAutoMeasureEnabled(): Boolean {
return true
}
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State?) {
//这里是在布局前将所有在显示的HoldView从RecyclerView中剥离,将其放在缓存中,方便重新布局时使用。
detachAndScrapAttachedViews(recycler)
var curLineWidth = 0 //curLineWidth 累加item布局时的x轴偏移
var curLineTop = 0 //curLineTop 累加item布局时的y轴偏移
var lastLineMaxHeight = 0
for (i in 0 until itemCount) {
val view: View = recycler.getViewForPosition(i)
//获取每个item的布局参数,计算每个item的占用位置时需要加上margin
val params = view.layoutParams as RecyclerView.LayoutParams
addView(view)
measureChildWithMargins(view, 0, 0)
val width = getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin
val height = getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin
curLineWidth += width //累加当前行已有item的宽度
if (curLineWidth <= getWidth()) { //如果累加的宽度小于等于RecyclerView的宽度,不需要换行
layoutDecorated(
view,
curLineWidth - width + params.leftMargin,
curLineTop + params.topMargin,
curLineWidth - params.rightMargin,
curLineTop + height - params.bottomMargin
) //布局item的真实位置
//比较当前行多有item的最大高度,用于换行后计算item在y轴上的偏移量
lastLineMaxHeight = Math.max(lastLineMaxHeight, height)
} else { //换行
curLineWidth = width
if (lastLineMaxHeight == 0) {
lastLineMaxHeight = height
}
//记录当前行top
curLineTop += lastLineMaxHeight
layoutDecorated(
view,
params.leftMargin,
curLineTop + params.topMargin,
width - params.rightMargin,
curLineTop + height - params.bottomMargin
)
lastLineMaxHeight = height
}
}
}
}