1.自定义View
自定义View可以分为三个流程:测量、布局、绘制 分别对应着onMeasure、onLayout、onDraw方法。
自定义View可以分为两种类型:
1.自定义ViewGroup :主要是onMeasure()、onLayout()测量和布局方法。
2.自定义View:主要是onMeasure()、onDarw()测量和绘制方法。
- 在ViewGroup和View中都使用了测量onMeasure()方法,接下来通过一个写一个自定义一个瀑布流ViewGroup学习onMeasure()是怎么测量的。
2.onMeasure()测量
2.1 MeasureSpec
-
首先要搞清楚,我们为什么要测量?
因为在我们的xml布局文件中,我们设置width和height时会使用match_parent或warp_content来设置,测量的方法就是要将之变成具体的值,如250dp等。 -
MeasureSpec的基本知识
每一个ViewGroup和View都会有MeasureSpec。MeasureSpec有32位字节组成,前两位:放三个模式,后30位:放控件的大小。
每个子View的MeasureSpec:由父ViewGroup的MeasureSpec和子view的LayoutParams确定。在getChildMeasureSpec()方法中可以查看。
MeasureSpec:exactly、at_most、unspecified
LayoutParams: 100dp、match_parent、warp_content
以下是为每个子View确定MeasureSpec的方法代码:
如果父View的MeasureSpec模式是exactly,那么子View的是如精确值100dp,则子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式exactly,大小为父view的默认大小;子View是warp_content时,模式是at_most,大小为父view的默认大小。
如果父View的MeasureSpec模式是at_most,那么子View的是如精确值100dp,则子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式at_most,大小为父view的默认大小;子View是warp_content时,模式是at_most,大小为父view的默认大小。
如果父View的MeasureSpec模式是upspecified,那么子View的是如精确值100dp,则子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式upspecified,大小为父view的默认大小;子View是warp_content时,模式是upspecified,大小为父view的默认大小。
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);
}
2.2 onMeasure()测量的通用流程:
不同ViewGroup由于子View的显示布局样式不同,所以代码都可能不一样。但是它们在测量时会有一个基本的通用流程:
- 遍历子View为其设置MeasureSpec并调用子view的measure()方法:这一步通过getChildMeasureSpec()方法,也可以使用measureChildMargins()方法完成。
- 确定自定义view的大小:通过自身的measureSpec的模式和子view的所需大小,确定自定义view的大小。如viewGroup本身的模式是exactly则不需要理会子view所需的大小。这种模式关系来确定大小的关系可以自己定义,也可以有resolveSizeAndState()方法来确定。最后调用setMeasureDimension()来确定自定义view的大小。
onMeasure()通用流程的代码:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//怎么测孩子呢?
for (i in 0 until childCount) {
val childView = getChildAt(i)
//获得子View的LayoutParams来确定子View的measureSpec
//可用替代measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0)
var childLP = childView.layoutParams
//让ViewGroup的MeasureSpec和子View的LayoutParams,来确定子view的MeasureSpec
val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width)
val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height)
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}
//自己定义不同模式的大小,也可以调用resolveSizeAndState()方法定义大小。
val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth
val realHeight = if (heightMode == MeasureSpec.EXACTLY) selHeight else parentNeedHeight
setMeasuredDimension(realWidth, realHeight)
}
2.3 举例瀑布流onMeasure()的测量:
布局分析:子View得换行:我们要先根据ViewGroup里MeasureSpec拿到默认的大小(一般都是上一个父View的最大值),然后跟子View所使用的宽度,如果比ViewGroup的默认大小要大,则换行。每一行的view都需要记录下来,以便在布局中确定位置。同时每一行的最大高度也需要记录下来,以便在布局中确定位置。同时也把每一行的最大宽度记录下来。下面是瀑布流布局的OnMeasure()方法代码
private val mVerticalSpacing = 0
private val mHorizontalSpacing = 0
private val allLines: MutableList<List<View?>> = ArrayList()
private val lineHeights: MutableList<Int> = ArrayList()
private var lineViews: MutableList<View> = ArrayList()
fun clearList(){
allLines.clear()
lineHeights.clear()
lineViews.clear()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
clearList()
val childCount = childCount
//拿到ViewGroup的默认的宽高
val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
val selHeight = MeasureSpec.getSize(heightMeasureSpec)
var parentNeedHeight = 0
var parentNeedWidth = 0
var lineWidthUsed = 0
var lineHeight = 0 //一行的高度
//怎么测孩子呢?
for (i in 0 until childCount) {
val childView = getChildAt(i)
//获得子View的LayoutParams来确定子View的measureSpec
//measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0)
var childLP = childView.layoutParams
//让ViewGroup的MeasureSpec和子View的LayoutParams,来确定子view的MeasureSpec
val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width)
val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height)
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
//获取子view的测量宽高
val childMeasureWidth = childView.measuredWidth
val childMeasureHeight = childView.measuredHeight
//这个时候需要换行
if (lineWidthUsed + childMeasureWidth > selfWidth) {
allLines.add(lineViews)
lineHeights.add(lineHeight)
parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed) + mHorizontalSpacing
parentNeedHeight = parentNeedHeight + lineHeight
lineViews = ArrayList()
lineWidthUsed = 0
lineHeight = 0
}
//记录每一行的view
lineViews.add(childView)
lineWidthUsed = lineWidthUsed + childMeasureWidth //每行已经添加的宽度值
lineHeight = Math.max(lineHeight, childMeasureHeight)
if (i == childCount - 1) {
allLines.add(lineViews)
lineHeights.add(lineHeight)
parentNeedWidth = Math.max(parentNeedHeight, lineWidthUsed)
parentNeedHeight = parentNeedHeight + lineHeight
}
}
//再测量自己,确定自己得大小
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth
val realHeight = if (heightMode == MeasureSpec.EXACTLY) selHeight else parentNeedHeight
setMeasuredDimension(realWidth, realHeight)
}
3. onLayout()布局方法:
通过上面的onMeasure()方法,已经确定了ViewGroup的大小。这时通过调用子view的.layout(l,t,r,b)来确定每个子view的位置。
3.1 getLeft()、getX()、getRawX()的区别:
getLeft():是控件左边到手机屏幕坐标系的左边的位置。
getX():是手势点击的点,到所属控件的里面左边位置。
getRawX():是手势点击的点,到手机屏幕的左边位置。
3.2 getWidth()和getMeasureWidth()的区别
getWidth()和getHeight()是在onLayout()方法执行完才有效。
getMeasureWidth()和getMeasureHeight()在onMeasure()就有效。
3.3 例子瀑布流的onLayout()分析:
布局分析:在上面的onMeasure()方法中,我们已经记录了每行都有哪些view,因此我们只要计算每个子view的左上坐标,然后通过view.getMeasureWidth和view.getMeasureHeight的被测量过子view的宽高以此来确定四个点的位置。以下是代码。
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//分为几行
val lineCount = allLines.size
var curL = paddingLeft
var curT = paddingTop
for (i in 0 until lineCount) {
val lineView = allLines[i]
val lineHeight = lineHeights[i]
for (j in lineView.indices) {
val view = lineView[j]
val left = curL
val top = curT
val right = left + view!!.measuredWidth
val bottom = top + view!!.measuredHeight
view.layout(left, top, right, bottom)
curL = right + mHorizontalSpacing
}
curT = curT + lineHeight + mVerticalSpacing
curL = paddingLeft
}
}
写在最后:
这里写的一个FlowLayout只是用作学习自定义ViewGroup如何测量和布局的一个简单例子,由于时间关系只写出一个大概。不过现在我们要用到流布局一般会使用recyclerView+FlexboxLayoutManager来写既方便又快速,这里有篇文章可以参考:RecyclerView之使用FlexboxLayoutManager
网友评论