前言
以前总是喜欢用别人写好的东西。但是真的说自己写自定义View还未必能写的出来。这篇文章完全是即时兴起,想看看在没有看其他人的博客的情况下自己是否真的能自定义View,所以就有了这篇文章。
github地址:https://github.com/Amoryan/FlowLayout
为什么要重写onMeasure
我们首先看看View里面这个的默认实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
它调用了getDefaultSize方法,而这个方法里面是这样处理的
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
这里我删除掉了UNSPECIFIED的情况,这个我们暂时用不到。可以看到这个方法中不管是AT_MOST还是EXACTLY都会将result设置为specSize,而specSize是父容器能给予的最大大小。这样就导致一个问题,如果我们用默认的实现,自定义的View就不支持wrap_content。
所以实际上我们重写onMeasure方法一个很重要的目的是为了让它支持wrap_content。
而ViewGroup除了这个目的之外,它还需要测量子控件大小。具体我们可以看看ViewGroup的measureChildren方法
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
而measureChild里面是这样处理的
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这里就会调用子控件的measure方法。
onMeasure
好了,开始自定义我们的流式布局,首先重写onMeasure方法,并逐个测量子控件,于是我们添加一个measureChildBeforeLayout用来测量子控件的大小
private fun measureChildBeforeLayout(widthMeasureSpec: Int, heightMeasureSpec: Int) {
for (i in 0..(childCount - 1)) {
var child = getChildAt(i)
if (View.GONE == child.visibility) continue
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
}
}
测量完子控件后我们开始确定自身的大小,如下所示
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
measureChildBeforeLayout(widthMeasureSpec, heightMeasureSpec)
val wSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val wSpecSize = MeasureSpec.getSize(widthMeasureSpec)
var wResult = wSpecSize
when (wSpecMode) {
MeasureSpec.AT_MOST -> {
wResult = Math.min(wResult, getChildTotalWidth())
}
}
val hSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val hSpecSize = MeasureSpec.getSize(heightMeasureSpec)
var hResult = hSpecSize
when (hSpecMode) {
MeasureSpec.AT_MOST -> {
hResult = Math.min(hResult, getChildTotalHeight(wResult))
}
}
setMeasuredDimension(wResult, hResult)
}
当宽度为wrap_content的时候,所有子控件的宽度和不能大于父控件给予的最大宽度。当高度为wrap_content的时候所有子控件的高度和不能大于父控件给予的最大高度,计算高度的时候得确认排列的行数,然后确定每一行的高度后求和。
private fun getChildTotalHeight(maxWidth: Int): Int {
var totalHeight = paddingTop + paddingBottom
var colIndex = 0
var rowIndex = 0
var rowWidth = paddingLeft + paddingRight
var rowHeight = 0
for (i in 0..(childCount - 1)) {
val child = getChildAt(i)
val lp = child.layoutParams as MarginLayoutParams
if (View.GONE == child.visibility) continue
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
if (colIndex == 0) {
if (rowWidth + childWidth > maxWidth) {
totalHeight += childHeight
if (rowIndex != 0) {
totalHeight += mVerticalSpace.toInt()
}
rowWidth = paddingLeft + paddingRight
rowHeight = 0
rowIndex++
} else {
colIndex++
rowWidth += childWidth
if (rowIndex == 0) {
rowHeight = Math.max(rowHeight, childHeight)
} else {
rowHeight = Math.max(rowHeight, childHeight + mVerticalSpace.toInt())
}
}
} else {
if (rowWidth + childWidth + mHorizontalSpace.toInt() > maxWidth) {
totalHeight += rowHeight
rowWidth = paddingLeft + paddingRight + childWidth
rowHeight = childHeight + mVerticalSpace.toInt()
colIndex = 1
rowIndex++
} else {
colIndex++
rowWidth += childWidth + mHorizontalSpace.toInt()
if (rowIndex == 0) {
rowHeight = Math.max(rowHeight, childHeight)
} else {
rowHeight = Math.max(rowHeight, childHeight + mVerticalSpace.toInt())
}
}
}
}
totalHeight += rowHeight
return totalHeight
}
onLayout
ViewGroup的onLayout主要是为了确定子控件的排放位置。这里添加一个layoutChild方法来确定每一行排列的子控件,然后再对每一行的子控件进行排列
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
mViews.clear()/*多次调用这个方法的情况*/
layoutChild()
var rowTop = paddingTop
for (i in 0..(mViews.size - 1)) {
val lineViews = mViews[i]
var rowLeft = paddingLeft
for (i in 0..(lineViews.size - 1)) {
val view = lineViews[i]
val lp = view.layoutParams as MarginLayoutParams
var left = rowLeft + lp.leftMargin
val top = rowTop + lp.topMargin
var space = view.measuredWidth + lp.leftMargin + lp.rightMargin
if (i != 0) {/*不是第一个控件就加上间距*/
left += mHorizontalSpace.toInt()
space += mHorizontalSpace.toInt()
}
view.layout(left, top, left + view.measuredWidth, top + view.measuredHeight)
rowLeft += space
}
rowTop += getRowMaxHeight(i)
}
}
private fun layoutChild() {
var lineViews = ArrayList()
var colIndex = 0
var rowWidth = paddingLeft + paddingRight
for (i in 0..(childCount - 1)) {
val child = getChildAt(i)
val lp = child.layoutParams as MarginLayoutParams
if (View.GONE == child.visibility) continue
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
if (colIndex == 0) {/*这一行的第一个控件是不用添加间距的*/
if (rowWidth + childWidth > measuredWidth) {
lineViews.add(child)
mViews.add(lineViews)
lineViews = ArrayList()
rowWidth = paddingLeft + paddingRight
} else {
colIndex++
rowWidth += childWidth
lineViews.add(child)
}
} else {/*一行的非第一个控件需要添加间距*/
if (rowWidth + childWidth + mHorizontalSpace > measuredWidth) {
mViews.add(lineViews)
lineViews = ArrayList()
lineViews.add(child)
rowWidth = paddingLeft + paddingRight + childWidth
colIndex = 1
} else {
colIndex++
rowWidth += childWidth + mHorizontalSpace.toInt()
lineViews.add(child)
}
}
}
mViews.add(lineViews)
}