概述
本文主要分享Android流式布局实现,实现效果如下:
在实现之前先来看一下View的生命周期,如下图:
流式布局属于自定义ViewGroup,重点关注onMeasure与onLayout方法
onMeasure完成子控件以及自身宽高测量
onMeasure方法中的主要工作:
- 确定子控件的widthMeasureSpec与heightMeasureSpec(重点)
- 根据childWidthMeasureSpec与childHeightMeasureSpec测量子控件
- 根据流式布局的算法计算出最大行宽和行高
- 获取自身的测量模式以及测量宽高,再根据子View的测量结果来确定最终的宽高
确定子控件的widthMeasureSpec与heightMeasureSpec
子控件对应宽高的MeasureSpec如何确定呢?追踪ViewGroup中的getChildMeasureSpec方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
由源码可知,子控件MeasureSpec是由于父控件的MeasureSpec、父控件的Padding以及自身LayoutParams对应的宽高共同确定的。
MeasureSpec是View中的内部类,基本都是二进制运算。由于int是32位的,用高两位表示mode,低30位表示size,MODE_SHIFT = 30的作用是移位,而mode包含三种模式:
- UNSPECIFIED:不对View大小做限制,系统使用
- EXACTLY:确切的大小,如:100dp
- AT_MOST:大小不可超过某数值,如:matchParent, 最大不能超过父控件
由上述的源码可知,普通View的创建规则如下:
明白了ViewMeasureSpec的创建规则后,那确认子控件MeasureSpec就非常简单了,核心代码如下:
val childView = getChildAt(i)
//对应xml布局参数
val layoutParams = childView.layoutParams
//父控件的MeasureSpec、父控件已使用的Padding、layoutParams共同确定子控件的MeasureSpec
//MeasureSpec包含mode(高两位)和size(低30位)
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight,
layoutParams.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom,
layoutParams.height
)
根据widthMeasureSpec与heightMeasureSpec测量子控件
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
根据流式布局的算法计算出最大行宽和行高
//测量完成后可获取测量的宽高
val measuredWidth = childView.measuredWidth
val measuredHeight = childView.measuredHeight
//判断是否需要换行
if (lineWidthUsed + measuredWidth + mHorizontalSpacing > selfWidth) {
//记录行数
mAllLines.add(lineViews)
//记录行高
mLineHeight.add(lineHeight)
//在每次换行时计算自身所需的宽高
parentNeedWidth = parentNeedWidth.coerceAtLeast(lineWidthUsed + mHorizontalSpacing)
parentNeedHeight += lineHeight + mVerticalSpacing
//重置参数
lineViews = mutableListOf()
lineWidthUsed = 0
lineHeight = 0
}
//记录每一行存放控件
lineViews.add(childView)
//记录每一行已使用的高度
lineWidthUsed += measuredWidth + mHorizontalSpacing
//记录每一行的最大高度
lineHeight = lineHeight.coerceAtLeast(measuredHeight)
获取自身的测量模式以及测量宽高,再根据子View的测量结果来确定最终的宽高
//获取自身的测量模式
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
//获取父控件给我的宽度
val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
//获取父控件给我的宽度
val selfHeight = MeasureSpec.getSize(heightMeasureSpec)
//确定最终的宽高
setMeasuredDimension(
if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth,
if (heightMode == MeasureSpec.EXACTLY) selfHeight else parentNeedHeight
)
onLayout完成子控件的摆放
核心代码如下:
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var curL = paddingLeft
var curT = paddingTop
//逐个将每一行的控件进行摆放
for (i in 0 until mAllLines.size) {
val lineViews = mAllLines[i]
val lineHeight = mLineHeight[i]
lineViews.forEach { view ->
val left = curL
val top = curT
val right = left + view.measuredWidth
val bottom = top + view.measuredHeight
//注意要点:在onMeasure之后能够获取measuredWidth或measuredHeight,但获取width/height无效,必须在view.layout之后才生效
view.layout(left, top, right, bottom)
//计算下一个控件的left
curL = right + mHorizontalSpacing
}
//计算下一行控件的Left
curL = paddingLeft
//计算下一行控件的Top
curT += lineHeight + mVerticalSpacing
}
需要注意在onMeasure之后能够获取控件的measuredWidth或measuredHeight,但获取width/height无效,必须在view.layout之后获取才生效,所以在控件摆放之前如果需要获取控件的宽高需要使用getMeasureWidth/getMeasureHeight
完整代码实现
class FlowLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
//记录总共多少行
private val mAllLines = mutableListOf>()
//记录每一行的最大高度,用于Layout阶段使用
private val mLineHeight = mutableListOf()
//水平间距
private val mHorizontalSpacing = dp2px(16)
//垂直间距
private val mVerticalSpacing = dp2px(8)
//自定义ViewGroup一般重新onMeasure onLayout
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//注意要点:由于测量过程是从父控件到子控件递归调用的,所以onMeasure可能被调用多次,这里参考FrameLayout源码进行清除工作
mAllLines.clear()
mLineHeight.clear()
/**
* 思路:1.先确定子控件的childWidthMeasureSpec与childHeightMeasureSpec(重点)
* 2.在根据childWidthMeasureSpec与childHeightMeasureSpec测量子控件
* 3.根据流式布局的算法计算出最大行宽和行高
* 4.获取自身的测量模式,再根据子View的测量结果来确定自身的最终宽高
*/
//获取父控件给我的宽度
val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
//获取父控件给我的宽度
val selfHeight = MeasureSpec.getSize(heightMeasureSpec)
//每一行已使用的高度
var lineWidthUsed = 0
//每一行的最大高度
var lineHeight = 0
//自身所需的宽度
var parentNeedWidth = 0
//自身所需的高度
var parentNeedHeight = 0
//记录每行控件的个数
var lineViews = mutableListOf()
for (i in 0 until childCount) {
val childView = getChildAt(i)
//对应xml布局参数
val layoutParams = childView.layoutParams
//父控件的MeasureSpec、父控件已使用的Padding、layoutParams共同确定子控件的MeasureSpec
//MeasureSpec包含mode(高两位)和size(低30位)
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight,
layoutParams.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom,
layoutParams.height
)
//测量子控件
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
//测量完成后可获取测量的宽高
val measuredWidth = childView.measuredWidth
val measuredHeight = childView.measuredHeight
//判断是否需要换行
if (lineWidthUsed + measuredWidth + mHorizontalSpacing > selfWidth) {
//记录行数
mAllLines.add(lineViews)
//记录行高
mLineHeight.add(lineHeight)
//在每次换行时计算自身所需的宽高
parentNeedWidth = parentNeedWidth.coerceAtLeast(lineWidthUsed + mHorizontalSpacing)
parentNeedHeight += lineHeight + mVerticalSpacing
//重置参数
lineViews = mutableListOf()
lineWidthUsed = 0
lineHeight = 0
}
//记录每一行存放控件
lineViews.add(childView)
//记录每一行已使用的高度
lineWidthUsed += measuredWidth + mHorizontalSpacing
//记录每一行的最大高度
lineHeight = lineHeight.coerceAtLeast(measuredHeight)
}
//先获取自身的测量模式以及大小,再根据子View的测量结果来确定自身的宽高
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
//确定最终的宽高
setMeasuredDimension(
if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth,
if (heightMode == MeasureSpec.EXACTLY) selfHeight else parentNeedHeight
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var curL = paddingLeft
var curT = paddingTop
//逐个将每一行的控件进行摆放
for (i in 0 until mAllLines.size) {
val lineViews = mAllLines[i]
val lineHeight = mLineHeight[i]
lineViews.forEach { view ->
val left = curL
val top = curT
val right = left + view.measuredWidth
val bottom = top + view.measuredHeight
//注意要点:在onMeasure之后能够获取measuredWidth或measuredHeight,但获取width/height无效,必须在view.layout之后才生效
view.layout(left, top, right, bottom)
//计算下一个控件的left
curL = right + mHorizontalSpacing
}
//计算下一行控件的Left
curL = paddingLeft
//计算下一行控件的Top
curT += lineHeight + mVerticalSpacing
}
}
private fun dp2px(dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
Resources.getSystem().displayMetrics
).toInt()
}
}