- 前言
- 一、使用原因
- 1.流式布局中放了许多小的view,要求我们把这些子view妥善的摆放在一个viewgroup中,如果我们在xmL中去实现这个效果,这就要求我们去对每个子view设置margin,padding,还有位置属性。这可能需要花很多时间去摆放,去设置。
- 2.如果我们做一个搜索内容的历史记录,那么我们事先是不知道子view的条目和具体内容的,所以我们也没法在XML中去书写,那么我们就需要一个viewgroup去对数据进行操作,取自动生成子view,并把他们的位置摆放妥当。
- 二、使用步骤
- 1.创建一个类继承viewgroup
- 2.重写onMeasure方法
- 小结
- 1.在onMeasure中测量每个子view的大小和位置,并用一个Rect的List用来记录,以供后面onlayout中使用,并且在测量完所有子view后得到viewgroup最合适的大小
- 2.该处使用0作为第三个参数widthUsed,这是为什么呢?
- 3.重写onLayout方法,对子view进行摆放
- 4.运行效果
- 完整代码
- 总结
前言
流式布局广泛运用于APP中,例如APP中搜索内容的历史记录,之前我已经用Java实现过这个布局了,如今用kotlin再来试试,来看看还有什么坑
一、使用原因 1.流式布局中放了许多小的view,要求我们把这些子view妥善的摆放在一个viewgroup中,如果我们在xmL中去实现这个效果,这就要求我们去对每个子view设置margin,padding,还有位置属性。这可能需要花很多时间去摆放,去设置。 2.如果我们做一个搜索内容的历史记录,那么我们事先是不知道子view的条目和具体内容的,所以我们也没法在XML中去书写,那么我们就需要一个viewgroup去对数据进行操作,取自动生成子view,并把他们的位置摆放妥当。 二、使用步骤 1.创建一个类继承viewgroup
代码如下:
class TagLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs)2.重写onMeasure方法
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//摆放时所有子view占据的横向边界
var widthUsed = 0
//所有子view占据的纵向高度
var heightUsed = 0
//当前行的子view占据的宽度
var lineWidthUsed = 0
//当前行占据的纵向高度
var lineMaxHeight = 0
//拿到布局的期望宽度
val widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec)
for ((index, child) in children.withIndex()) {
//给child进行测量
measureChildWithMargins(
child,
widthMeasureSpec,
0,
heightMeasureSpec,
heightUsed
)
//超出边界时换行
if (child.measuredWidth + lineWidthUsed > widthMeasureSpecSize) {
//累加这行的高度
heightUsed += lineMaxHeight
//新开一行,还没有子view,下面两个值置为零
lineWidthUsed = 0
lineMaxHeight = 0
//由于新开了一行,则对子view进行重新测量
measureChildWithMargins(
child,
widthMeasureSpec,
lineWidthUsed,
heightMeasureSpec,
heightUsed
)
}
//如果存放子view对应位置的childrenBounds中还没有存放过这个子view的Rect,则进行添加,避免在onMeasure中对对象进行重复创建。
if (index >= childrenBounds.size) {
childrenBounds.add(
Rect()
)
}
//设置child 的位置和大小
childrenBounds[index].set(
lineWidthUsed,
heightUsed,
lineWidthUsed + child.measuredWidth,
heightUsed + child.measuredHeight
)
//把新加的子view的宽度进行累加
lineWidthUsed += child.measuredWidth
//更新已加入的所有子view占据的宽度
widthUsed = max(widthUsed, lineWidthUsed)
//更新当前行的view中最高的view高度,既这行子view所占据的高度
lineMaxHeight = max(lineMaxHeight, child.measuredHeight)
}
//获取到所有子view占据的高度,因为最后一行的view高度没有在循环中进行累加,所有循环结束要记得加上
val selfHeight = heightUsed + lineMaxHeight
//获取所有子view占据的宽度
val selfWidth = widthUsed
setMeasuredDimension(selfWidth, selfHeight)
}
小结
1.在onMeasure中测量每个子view的大小和位置,并用一个Rect的List用来记录,以供后面onlayout中使用,并且在测量完所有子view后得到viewgroup最合适的大小
2.该处使用0作为第三个参数widthUsed,这是为什么呢?
measureChildWithMargins(
child,
widthMeasureSpec,
0,
heightMeasureSpec,
heightUsed
)
如果使用本地记录的这行已用高度lineWidthUsed,那么如果新加入的子view在这行空间不足的情况下就会分两行进行摆放,这里用0的意思就是把整个一行的空间都给要添加的子view进行测量,看他在一行中能占据多大空间,如果一行都无法容纳,再自行换行
来看看,第三个参数填写lineWidthUsed后的效果
果然一个子view进行了多行显示,和我们想要的效果相差甚远。
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//根据onMeasure中获取到的每个子view的高度和大小进行摆放
for ((index, child) in children.withIndex()) {
val rect = childrenBounds[index]
child.layout(rect.left, rect.top, rect.right, rect.bottom)
}
}
4.运行效果
完整代码
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.core.view.children
import kotlin.math.max
class TagLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
private val childrenBounds = mutableListOf()
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//摆放时所有子view占据的横向边界
var widthUsed = 0
//所有子view占据的纵向高度
var heightUsed = 0
//当前行的子view占据的宽度
var lineWidthUsed = 0
//当前行占据的纵向高度
var lineMaxHeight = 0
//拿到布局的期望宽度
val widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec)
for ((index, child) in children.withIndex()) {
//给child进行测量
measureChildWithMargins(
child,
widthMeasureSpec,
0,
heightMeasureSpec,
heightUsed
)
//超出边界时换行
if (child.measuredWidth + lineWidthUsed > widthMeasureSpecSize) {
//累加这行的高度
heightUsed += lineMaxHeight
//新开一行,还没有子view,下面两个值置为零
lineWidthUsed = 0
lineMaxHeight = 0
//由于新开了一行,则对子view进行重新测量
measureChildWithMargins(
child,
widthMeasureSpec,
lineWidthUsed,
heightMeasureSpec,
heightUsed
)
}
//如果存放子view对应位置的childrenBounds中还没有存放过这个子view的Rect,则进行添加,避免在onMeasure中对对象进行重复创建。
if (index >= childrenBounds.size) {
childrenBounds.add(
Rect()
)
}
//设置child 的位置和大小
childrenBounds[index].set(
lineWidthUsed,
heightUsed,
lineWidthUsed + child.measuredWidth,
heightUsed + child.measuredHeight
)
//把新加的子view的宽度进行累加
lineWidthUsed += child.measuredWidth
//更新已加入的所有子view占据的宽度
widthUsed = max(widthUsed, lineWidthUsed)
//更新当前行的view中最高的view高度,既这行子view所占据的高度
lineMaxHeight = max(lineMaxHeight, child.measuredHeight)
}
//获取到所有子view占据的高度,因为最后一行的view高度没有在循环中进行累加,所有循环结束要记得加上
val selfHeight = heightUsed + lineMaxHeight
//获取所有子view占据的宽度
val selfWidth = widthUsed
setMeasuredDimension(selfWidth, selfHeight)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//根据onMeasure中获取到的每个子view的高度和大小进行摆放
for ((index, child) in children.withIndex()) {
val rect = childrenBounds[index]
child.layout(rect.left, rect.top, rect.right, rect.bottom)
}
}
//解决 java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}
总结
最核心的部分还是重写onMeasure。



