概述
View的绘制流程
一个View从创建到最终绘制出来,有三个方法是不得不提到的,那就是onMeasure测量,onLayout定位,onDraw绘制
onMeasure
对于一个View绘制前,首先需要测量出来这个View的宽高,而这步工作就是由onMeasure完成的了。
//view测量宽高的方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//将计算出来的宽高传入setMeasureDimension方法中完成测量
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasureDimension方法通过传入的值计算出绝对宽高,并最终调用setMeasureDimensionRaw方法进行赋值
所以View的宽高测量关键就在于onMeasure方法中,我们只需要在该方法中计算出宽高,并调用setMeasureDimension方法即可完成测量步骤。
而对于宽高的计算,Android提供了一个非常精简有效的类MeasureSpec用于传递宽高信息的
MeasureSpec主要是三个方法
//用于父布局计算好子布局的宽高之后,用这个方法生成对应的int值传给子布局的onMeasure方法
//这个int值实际上是一个32位的二进制,前两位用于储存MODE,后30位用于储存SIZE
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//用于解析宽高的MODE
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
//用于解析宽高的SIZE
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec类中给出了三种MODE,分别是
- EXACTLY 精确值模式,对于match_parent(父控件的尺寸值)或者是具体的尺寸值
- AL_MOST 最大值模式,对应wrap_content,即View宽高随着子控件或者内容的变化而变化,只要不超出父控件给出的最大宽高即可
- UNSPECIFIED 无限制模式, 对应XML设置宽高为0或者未设置宽高时,一般自定义View用不到
而View的onMeasure方法默认只支持EXACTLY模式,即当你的自定义view不需要使用wrap_content来设定宽高时,可以选择不重写onMeasure方法,否则必须重写该方法进行宽高设置
这里可以这么理解,如果宽高从一开始(即xml定义时)就是确定好并不会改变,那么就不需要去自己重写测量了,如果你的宽高是动态改变的,是根据你的绘制内容而变化的,那么你就需要通过重写onMeasure方法去告诉程序你所需要的宽高值。
下面上一段重写onMeasure方法的代码
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var widthResult = 0
var heightResult = 0
//获取宽高对应的mode
var widthMode = MeasureSpec.getMode(widthMeasureSpec)
var heightMode = MeasureSpec.getMode(widthMeasureSpec)
//获取宽高对应的size
var widthSize = MeasureSpec.getSize(widthMeasureSpec)
var heightSize = MeasureSpec.getSize(heightMeasureSpec)
when(widthMode){
MeasureSpec.EXACTLY -> {
//此时的widthSize是XML设定的精确值
widthResult = widthSize
}
MeasureSpec.AT_MOST -> {
//模拟一个当前绘制内容总宽度
var currentWidth = 200
//此时widthSize是父控件允许该控件的最大宽度
//取二者最小值,即可保证控件跟随内容变化却又不超出父控件
widthResult = min(widthSize, currentWidth)
}
MeasureSpec.UNSPECIFIED -> {
//模拟一个当前绘制内容总宽度
var currentWidth = 200
//直接根据内容设定宽度
heightResult = currentWidth
}
}
when(heightMode){
MeasureSpec.EXACTLY -> {
//此时的heightSize是XML设定的精确值
heightResult = heightSize
}
MeasureSpec.AT_MOST -> {
//模拟一个当前绘制内容总高度
var currentWidth = 200
//此时heightSize是父控件允许该控件的最大高度
//取二者最小值,即可保证控件跟随内容变化却又不超出父控件
heightResult = min(heightSize, currentWidth)
}
MeasureSpec.UNSPECIFIED -> {
//模拟一个当前绘制内容总高度
var currentHeight = 200
//直接根据内容设定高度
heightResult = currentHeight
}
}
setMeasuredDimension(widthResult, heightResult)
}
上面便是自定义view的onMeasure重写代码了,而如果用户需要自定义的是ViewGroup,那便不一样了,我们先看一下参考LinearLayout类的onMeasure方法。
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
......
//遍历子View
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
......
//当高度是精确值模式时,直接设置高度
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
......
} else {
//当宽高为wrap_content时
......
//测量每一个子View的宽高
//该方法最终调用child.measure
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
......
final int childHeight = child.getMeasuredHeight();
......
final int totalLength = mTotalLength;
//计算每一个子View的高度以及margin之和
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
......
}
......
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
......
//设置自身
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
......
}
LinearLayout有两个测量方法,分别对应纵向和横向两种情况,这里只贴出measureVertical的部分代码,感兴趣的可以去详细研究一下。可以看到
- 当高度为精确值或者match_parent时,LinearLayout可以直接设置高度。
- 当高度为wrap_content时,首先需要遍历所有子控件,通过measure依次测量他们的高度,再根据他们的高度计算出自身的高度。
总结:
- 当测量模式为EXACTLY(即精确值或match_parent)时,直接设置宽高(此时可以不重写onMeasure方法)
- 当测量模式为AT_MOST(即wrap_content)或UNSPECIFIED(即0或未设置宽高)时,需要重写方法onMeasure方法并调用setMeasuredDimension方法手动设置宽高属性。
- 若含有子View时,且需要根据子View内容动态改变宽高时,需要先调用子View的measure方法测量后,再去获取其宽高并计算自身宽高。
onLayout
相比onMeasure方法而言,onLayout方法就显得简单多了,他的作用是告诉程序View的位置,但是这个位置默认不是由View本身决定的,而是由父控件调用其layout方法实现,在这我们可以看一下View的onLayout方法和layout方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
public void layout(int l, int t, int r, int b) {
......
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b)
}
......
}
View本身的onLayout方法是一个空方法,真正确定布局位置的是layout方法。实际上,onLayout方法真正作用是用于确定子控件的位置信息,而其本身的位置是由父布局确定并调用layout进行设置。
所以如果是viewGroup时,便需要遍历所有的子View并调用其layout方法,从而来确定自己的布局位置。这里也参考LinearLayout的onLayout方法
void layoutVertical(int left, int top, int right, int bottom) {
......
//遍历所有的子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
......
//最终是调用child.layout方法去指定子View的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
......
}
值得一提的时,用户可以通过调用layout的方式去调整本身的位置,这个技巧可以用于实现跟随手指滑动
onDraw()
最后就是View的内容绘制了,而实际上View的真正绘制是由draw方法控制的,在这个方法中依次调用了以下方法
- drawBackground(canvas) 绘制背景
- onDraw() 绘制该View内容,View的该方法为空方法,即提供给开发者重写完成绘制的
- dispatchDraw(canvas)这个方法可以阅读ViewGroup的源码,适用于绘制子布局
- onDrawForeground(canvas) 用于绘制装饰器,例如滚动条
以上方法中一般的自定义View只需要重写onDraw方法即可。
最后
以上就是健壮钢笔为你收集整理的Android自定义View——onMeasure,onLayout,onDraw的作用 View的绘制流程的全部内容,希望文章能够帮你解决Android自定义View——onMeasure,onLayout,onDraw的作用 View的绘制流程所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复