效果图
image-20210726223212865.png
流布局是自定义ViewGroup的一种,简单来说,就是将其子View按照从左到右、从上到下依次进行排列,如果超过流布局的宽度,就换一行,继续进行排列,很多邮箱的发件人和收件人就是这种效果
实现步骤
- 继承ViewGroup,用一个列表childrenRect存储所有子View的位置信息
- 重写onLayout方法,在onLayout中遍历所有子View,使用childrenRect的位置信息依次对所有子View进行布局
- 重写generateLayoutParams方法,返回一个MarginLayoutParams对象,因为要支持Margin,就需要获得子View的Margin值,重写这个方法可以将子View的LayoutParams转为MarginLayoutParams,从而获取Margin值
- 重写onMeasure方法,在这个方法中,遍历所有子View,调用measureChildWithMargins测量子View的宽高
- 再从左到右、从上到下依次进行排列,当排到一个子View,当前行已用宽度加上该子View的宽度后超过了ViewGroup的宽度,就换行排列,并累加行高
- 最后setMeasuredDimension设置流布局本身的高度
代码
class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) {
private val childrenRect = mutableListOf<Rect>()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
//父ViewGroup必须为确定的宽度,否则直接报错
throw RuntimeException("FlowLayout's width must be EXACTLY!")
}
//父ViewGroup的宽度
val groupWidth = MeasureSpec.getSize(widthMeasureSpec)
//父ViewGroup的高度
var groupHeight = paddingTop
//当前子View行测量后进行位置摆放使用的高度
var lineHeight = paddingTop
//当前子View行测量后进行位置摆放使用的宽度
var lineWidth = paddingLeft
//当前行最高高度
var lineMaxHeight = 0
for (i in 0 until childCount) {
//遍历获取子View并测量子View的宽高
val child = getChildAt(i)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
//保存每一个子View的位置,要考虑子View的Margin
val childRect: Rect = initRect(i)
val childParams: MarginLayoutParams = child.layoutParams as MarginLayoutParams
//设置位置和尺寸
setRect(childRect, lineWidth, childParams, lineHeight, child)
//累加当前行child的宽度
lineWidth += child.measuredWidth + childParams.leftMargin + childParams.rightMargin
//如果当前行已经放满了子View,换一行放置这个子View
if (lineWidth > groupWidth) {
//清零lineWidth,但不是直接赋值为0,要考虑padding
lineWidth = paddingLeft
//将当前行使用的高度加上上一行统计的最高高度,为下一行布局做准备
lineHeight += lineMaxHeight
groupHeight += lineMaxHeight
//清零当前行最高高度,因为已经换行
lineMaxHeight = 0
//设置换行后的子View位置
setRect(childRect, lineWidth, childParams, lineHeight, child)
//累加当前行child的宽度
lineWidth += child.measuredWidth + childParams.leftMargin + childParams.rightMargin
}
//得到当前行的最高子View
var childHeight = childRect.height() + childParams.topMargin + childParams.bottomMargin
lineMaxHeight = max(lineMaxHeight, childHeight)
}
//遍历完成后,要加上最后一行的高度
groupHeight += lineMaxHeight
groupHeight = View.resolveSize(groupHeight, heightMeasureSpec)
setMeasuredDimension(groupWidth, groupHeight)
}
/**
* 为子View设置位置和尺寸
*/
private fun setRect(
childRect: Rect,
lineWidth: Int,
childParams: MarginLayoutParams,
lineHeight: Int,
child: View
) {
childRect.left = lineWidth + childParams.leftMargin
childRect.top = lineHeight + childParams.topMargin
childRect.right = childRect.left + child.measuredWidth
childRect.bottom = childRect.top + child.measuredHeight
}
/**
* 初始化第i个子View的Rect,如果已有,返回第i个子View的Rect
*
* @param i
* @return
*/
private fun initRect(i: Int): Rect {
val rect: Rect
if (childrenRect.size <= i) {
rect = Rect()
childrenRect.add(rect)
} else {
rect = childrenRect.get(i)
}
return rect
}
/**
* 遍历所有子View,进行布局
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in childrenRect.indices) {
getChildAt(i).layout(
childrenRect[i].left,
childrenRect[i].top,
childrenRect[i].right,
childrenRect[i].bottom
)
}
}
/**
* 因为要支持Margin,返回一个MarginLayoutParams对象
*/
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}











网友评论