一、View的绘制流程:理解onMeasure、onLayout、onDraw三大核心方法
说实话,刚接触Android自定义View的时候,我也有点懵。三个方法,十几个参数,各种模式——到底谁先谁后?每个方法该干什么?
别急,咱们一步步拆解。View的绘制流程,说白了就是三个步骤:量一量、摆一摆、画一画。对应到代码里,就是onMeasure、onLayout、onDraw。
1.1 绘制流程的总览
先记住这个顺序:onMeasure → onLayout → onDraw。这个顺序是固定的,系统从根View开始,一层层往下调用。
我个人习惯把这三个方法比作装修房子:
- onMeasure:量尺寸。量一下客厅多宽、卧室多长,决定家具该买多大的。
- onLayout:摆位置。沙发放哪、桌子放哪,确定每个家具的位置。
- onDraw:刷墙贴砖。真正把颜色、图案画上去。
嗯,这个比喻虽然简单,但很实用。你想想看,如果尺寸没量对,后面摆位置和画画都是白搭。
1.2 onMeasure:测量模式与尺寸计算
onMeasure是绘制流程的第一步,也是坑最多的一步。我在项目中遇到过好几次,自定义View显示不全或者撑爆屏幕,最后发现都是测量模式没处理好。
系统会传进来两个参数:widthMeasureSpec和heightMeasureSpec。这两个参数里,其实藏着两个信息:测量模式和父容器建议的大小。
测量模式有三种:
| 模式 | 值 | 含义 |
|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | 精确模式。父容器说了,你就这么大。比如match_parent或固定dp值。 |
| AT_MOST | MeasureSpec.AT_MOST | 最大模式。你最大不能超过这个值,具体多大你自己看着办。比如wrap_content。 |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | 未指定模式。你想多大就多大,一般用在ScrollView或ListView里。 |
重点来了:自定义View时,如果你不重写onMeasure,默认行为是直接使用父容器给的大小。这会导致wrap_content失效——明明设置了wrap_content,结果却撑满了全屏。
为什么会这样?因为View的默认onMeasure实现,直接调用了setMeasuredDimension,传进去的是MeasureSpec里解析出来的大小。对于wrap_content,父容器给的是AT_MOST模式,但默认实现直接用了这个最大值。
正确的做法是:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 默认宽高
int defaultWidth = 200;
int defaultHeight = 200;
if (widthMode == MeasureSpec.AT_MOST) {
// wrap_content时,使用我们自己的默认大小
widthSize = Math.min(defaultWidth, widthSize);
}
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = Math.min(defaultHeight, heightSize);
}
setMeasuredDimension(widthSize, heightSize);
}
小技巧:我个人习惯把默认大小定义成常量,比如DEFAULT_WIDTH = 200。这样在多个自定义View里复用,也方便后期调整。
1.3 onLayout:子View的摆放逻辑
onLayout是ViewGroup特有的方法。说白了,就是决定每个子View放在哪。
我曾经踩过一个坑:自定义了一个流式布局,结果子View全叠在一起了。排查了半天,发现是onLayout里忘了计算每个子View的left和top。
onLayout的签名是这样的:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// changed:布局是否发生变化
// l, t, r, b:当前ViewGroup的边界
}
对于ViewGroup,你需要遍历所有子View,调用每个子View的layout()方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int currentTop = t;
int currentLeft = l;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 摆放子View
child.layout(currentLeft, currentTop,
currentLeft + childWidth, currentTop + childHeight);
// 更新下一个子View的位置
currentTop += childHeight;
}
}
}
注意:onLayout里一定要用getMeasuredWidth()和getMeasuredHeight(),而不是getWidth()和getHeight()。因为此时View还没有完成布局,getWidth()可能返回0。
1.4 onDraw:真正的绘制工作
onDraw是最后一步,也是最有意思的一步。在这里,你可以用Canvas画任何东西——圆形、矩形、文字、甚至自定义的路径。
我记得第一次写自定义View时,在onDraw里画了一个渐变圆环,效果出来那一刻,真的很有成就感。
onDraw的基本结构:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 1. 绘制背景
// 2. 绘制内容
// 3. 绘制子View(如果是ViewGroup)
// 4. 绘制装饰(如前景、滚动条等)
}
一个简单的例子:画一个带边框的圆形
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width, height) / 2;
// 画填充圆
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.BLUE);
canvas.drawCircle(width / 2f, height / 2f, radius, paint);
// 画边框
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
paint.setColor(Color.WHITE);
canvas.drawCircle(width / 2f, height / 2f, radius, paint);
}
性能提醒:onDraw方法会被频繁调用(比如滑动、动画)。不要在onDraw里创建对象,否则会频繁触发GC,导致掉帧。把Paint、Path这些对象在构造方法里创建好。
1.5 三个方法的调用时机与关系
这三个方法不是独立运行的。它们之间有明确的依赖关系:
- onMeasure先执行,确定宽高。
- onLayout接着执行,确定位置。
- onDraw最后执行,进行绘制。
如果onMeasure里设置的尺寸不对,onLayout里摆放的位置就会错位,onDraw画出来的东西自然也不对。
另外,这三个方法不一定每次都会全部执行。比如:
- 如果只是改变位置(调用
layout()),只会触发onLayout和onDraw。 - 如果只是改变背景色(调用
invalidate()),只会触发onDraw。 - 如果改变了尺寸(调用
requestLayout()),三个方法都会重新执行。
避坑指南:我曾经在自定义View里调用了invalidate(),以为只会重绘,结果发现onMeasure也被调用了。后来才意识到,是因为我在onDraw里修改了某个会影响尺寸的属性。所以,不要在onDraw里修改会影响布局的属性。
1.6 实战:一个完整的自定义View示例
说了这么多,咱们来个完整的例子。一个带进度显示的圆形进度条:
public class CircleProgressView extends View {
private Paint paint;
private int progress = 0;
private static final int DEFAULT_SIZE = 300;
public CircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST) {
widthSize = Math.min(DEFAULT_SIZE, widthSize);
}
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = Math.min(DEFAULT_SIZE, heightSize);
}
// 保持正方形
int size = Math.min(widthSize, heightSize);
setMeasuredDimension(size, size);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width, height) / 2 - 20;
// 画背景圆环
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(30);
paint.setColor(Color.LTGRAY);
canvas.drawCircle(width / 2f, height / 2f, radius, paint);
// 画进度弧
paint.setColor(Color.BLUE);
float sweepAngle = progress / 100f * 360;
canvas.drawArc(20, 20, width - 20, height - 20,
-90, sweepAngle, false, paint);
// 画进度文字
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(50);
paint.setColor(Color.BLACK);
String text = progress + "%";
float textWidth = paint.measureText(text);
canvas.drawText(text, (width - textWidth) / 2f,
height / 2f + 15, paint);
}
public void setProgress(int progress) {
this.progress = Math.max(0, Math.min(100, progress));
invalidate(); // 只触发onDraw
}
}
这个例子涵盖了三个核心方法:onMeasure处理了wrap_content的情况,onDraw画了圆环和文字。你想想看,如果onMeasure里没有处理AT_MOST模式,这个View在wrap_content时就会撑满屏幕,效果就完全不对了。
1.7 总结
View的绘制流程,说白了就是三个步骤:量、摆、画。每个步骤都有它的职责和坑点:
- onMeasure:别忘了处理wrap_content,否则默认会撑满父容器。
- onLayout:用getMeasuredWidth/Height,别用getWidth/Height。
- onDraw:别创建对象,别修改布局属性。
嗯,掌握了这三个方法,自定义View就算入门了。下一章我们会深入讲Canvas和Paint的高级用法,到时候就能画出更炫酷的效果了。
一句话记住:onMeasure定大小,onLayout定位置,onDraw定长相。三者缺一不可,顺序也不能乱。