概述
本文主要分享Android流式布局實(shí)現(xiàn)把沼,實(shí)現(xiàn)效果如下:
在實(shí)現(xiàn)之前先來(lái)看一下View的生命周期,如下圖:
流式布局屬于自定義ViewGroup秩伞,重點(diǎn)關(guān)注onMeasure與onLayout方法
onMeasure完成子控件以及自身寬高測(cè)量
onMeasure方法中的主要工作:
- 確定子控件的widthMeasureSpec與heightMeasureSpec(重點(diǎn))
- 根據(jù)childWidthMeasureSpec與childHeightMeasureSpec測(cè)量子控件
- 根據(jù)流式布局的算法計(jì)算出最大行寬和行高
- 獲取自身的測(cè)量模式以及測(cè)量寬高逞带,再根據(jù)子View的測(cè)量結(jié)果來(lái)確定最終的寬高
確定子控件的widthMeasureSpec與heightMeasureSpec
子控件對(duì)應(yīng)寬高的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對(duì)應(yīng)的寬高共同確定的。
MeasureSpec是View中的內(nèi)部類脸爱,基本都是二進(jìn)制運(yùn)算遇汞。由于int是32位的,用高兩位表示mode簿废,低30位表示size空入,MODE_SHIFT = 30的作用是移位,而mode包含三種模式:
- UNSPECIFIED:不對(duì)View大小做限制族檬,系統(tǒng)使用
- EXACTLY:確切的大小歪赢,如:100dp
- AT_MOST:大小不可超過(guò)某數(shù)值,如:matchParent, 最大不能超過(guò)父控件
由上述的源碼可知单料,普通View的創(chuàng)建規(guī)則如下:
明白了ViewMeasureSpec的創(chuàng)建規(guī)則后埋凯,那確認(rèn)子控件MeasureSpec就非常簡(jiǎn)單了,核心代碼如下:
val childView = getChildAt(i)
//對(duì)應(yīng)xml布局參數(shù)
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
)
根據(jù)widthMeasureSpec與heightMeasureSpec測(cè)量子控件
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
根據(jù)流式布局的算法計(jì)算出最大行寬和行高
//測(cè)量完成后可獲取測(cè)量的寬高
val measuredWidth = childView.measuredWidth
val measuredHeight = childView.measuredHeight
//判斷是否需要換行
if (lineWidthUsed + measuredWidth + mHorizontalSpacing > selfWidth) {
//記錄行數(shù)
mAllLines.add(lineViews)
//記錄行高
mLineHeight.add(lineHeight)
//在每次換行時(shí)計(jì)算自身所需的寬高
parentNeedWidth = parentNeedWidth.coerceAtLeast(lineWidthUsed + mHorizontalSpacing)
parentNeedHeight += lineHeight + mVerticalSpacing
//重置參數(shù)
lineViews = mutableListOf()
lineWidthUsed = 0
lineHeight = 0
}
//記錄每一行存放控件
lineViews.add(childView)
//記錄每一行已使用的高度
lineWidthUsed += measuredWidth + mHorizontalSpacing
//記錄每一行的最大高度
lineHeight = lineHeight.coerceAtLeast(measuredHeight)
獲取自身的測(cè)量模式以及測(cè)量寬高,再根據(jù)子View的測(cè)量結(jié)果來(lái)確定最終的寬高
//獲取自身的測(cè)量模式
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
//逐個(gè)將每一行的控件進(jìn)行擺放
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
//注意要點(diǎn):在onMeasure之后能夠獲取measuredWidth或measuredHeight藏斩,但獲取width/height無(wú)效躏结,必須在view.layout之后才生效
view.layout(left, top, right, bottom)
//計(jì)算下一個(gè)控件的left
curL = right + mHorizontalSpacing
}
//計(jì)算下一行控件的Left
curL = paddingLeft
//計(jì)算下一行控件的Top
curT += lineHeight + mVerticalSpacing
}
需要注意在onMeasure之后能夠獲取控件的measuredWidth或measuredHeight,但獲取width/height無(wú)效狰域,必須在view.layout之后獲取才生效媳拴,所以在控件擺放之前如果需要獲取控件的寬高需要使用getMeasureWidth/getMeasureHeight
完整代碼實(shí)現(xiàn)
class FlowLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
//記錄總共多少行
private val mAllLines = mutableListOf<List<View>>()
//記錄每一行的最大高度,用于Layout階段使用
private val mLineHeight = mutableListOf<Int>()
//水平間距
private val mHorizontalSpacing = dp2px(16)
//垂直間距
private val mVerticalSpacing = dp2px(8)
//自定義ViewGroup一般重新onMeasure onLayout
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//注意要點(diǎn):由于測(cè)量過(guò)程是從父控件到子控件遞歸調(diào)用的兆览,所以onMeasure可能被調(diào)用多次屈溉,這里參考FrameLayout源碼進(jìn)行清除工作
mAllLines.clear()
mLineHeight.clear()
/**
* 思路:1.先確定子控件的childWidthMeasureSpec與childHeightMeasureSpec(重點(diǎn))
* 2.在根據(jù)childWidthMeasureSpec與childHeightMeasureSpec測(cè)量子控件
* 3.根據(jù)流式布局的算法計(jì)算出最大行寬和行高
* 4.獲取自身的測(cè)量模式,再根據(jù)子View的測(cè)量結(jié)果來(lái)確定自身的最終寬高
*/
//獲取父控件給我的寬度
val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
//獲取父控件給我的寬度
val selfHeight = MeasureSpec.getSize(heightMeasureSpec)
//每一行已使用的高度
var lineWidthUsed = 0
//每一行的最大高度
var lineHeight = 0
//自身所需的寬度
var parentNeedWidth = 0
//自身所需的高度
var parentNeedHeight = 0
//記錄每行控件的個(gè)數(shù)
var lineViews = mutableListOf<View>()
for (i in 0 until childCount) {
val childView = getChildAt(i)
//對(duì)應(yīng)xml布局參數(shù)
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
)
//測(cè)量子控件
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
//測(cè)量完成后可獲取測(cè)量的寬高
val measuredWidth = childView.measuredWidth
val measuredHeight = childView.measuredHeight
//判斷是否需要換行
if (lineWidthUsed + measuredWidth + mHorizontalSpacing > selfWidth) {
//記錄行數(shù)
mAllLines.add(lineViews)
//記錄行高
mLineHeight.add(lineHeight)
//在每次換行時(shí)計(jì)算自身所需的寬高
parentNeedWidth = parentNeedWidth.coerceAtLeast(lineWidthUsed + mHorizontalSpacing)
parentNeedHeight += lineHeight + mVerticalSpacing
//重置參數(shù)
lineViews = mutableListOf()
lineWidthUsed = 0
lineHeight = 0
}
//記錄每一行存放控件
lineViews.add(childView)
//記錄每一行已使用的高度
lineWidthUsed += measuredWidth + mHorizontalSpacing
//記錄每一行的最大高度
lineHeight = lineHeight.coerceAtLeast(measuredHeight)
}
//先獲取自身的測(cè)量模式以及大小帆赢,再根據(jù)子View的測(cè)量結(jié)果來(lái)確定自身的寬高
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
//逐個(gè)將每一行的控件進(jìn)行擺放
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
//注意要點(diǎn):在onMeasure之后能夠獲取measuredWidth或measuredHeight,但獲取width/height無(wú)效线梗,必須在view.layout之后才生效
view.layout(left, top, right, bottom)
//計(jì)算下一個(gè)控件的left
curL = right + mHorizontalSpacing
}
//計(jì)算下一行控件的Left
curL = paddingLeft
//計(jì)算下一行控件的Top
curT += lineHeight + mVerticalSpacing
}
}
private fun dp2px(dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
Resources.getSystem().displayMetrics
).toInt()
}
}