第2章:自定义View基础:从零开始创建一个自定义View
说实话,很多初学者一听到「自定义View」就觉得头大。我当年也是这样,总觉得那是大神才碰的东西。但后来我发现,只要你理解了几个核心概念,这事儿其实没那么玄乎。
今天我们就从零开始,手把手创建一个自定义View。我会带着你一步步走,把构造方法、onDraw、Canvas和Paint这几个关键点讲透。
2.1 自定义View的三种构造方法
先说说构造方法。你可能会问:「为啥一个View要写好几个构造方法?」
嗯,这其实是为了适配不同的创建场景。Android系统在XML布局里加载View时,和你在代码里new一个View时,调用的构造方法是不一样的。
public class MyCustomView extends View {
// 在代码中直接new时调用
public MyCustomView(Context context) {
super(context);
init();
}
// 在XML布局中使用时调用
public MyCustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
// 带style属性时调用
public MyCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
}
我个人习惯把初始化逻辑抽到一个单独的 init() 方法里。这样不管哪个构造方法进来,都能走同一套初始化流程。省得你写三遍重复代码,对吧?
2.2 onDraw方法:View的「画布时刻」
onDraw是自定义View的灵魂。说白了,你所有想要显示的内容,都得在这里画出来。
系统会在需要绘制View的时候自动调用onDraw。你不需要手动去调它,只需要在需要刷新时调用 invalidate() 就行。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 你的绘制代码写在这里
}
我记得刚入行那会儿,有个同事在onDraw里做了大量对象创建的操作,结果界面卡得不行。后来我才明白——onDraw会被频繁调用,千万别在这里new对象。
2.3 Canvas:你的画板
Canvas就像一块画板。你可以在上面画圆、画线、画文字、画图片。它提供了一系列的drawXXX方法,基本上你能想到的图形都能画。
| 方法 | 作用 |
|---|---|
| drawCircle() | 画圆形 |
| drawRect() | 画矩形 |
| drawLine() | 画直线 |
| drawText() | 画文字 |
| drawBitmap() | 画图片 |
| drawPath() | 画任意路径 |
你想想看,Canvas其实就是一个「状态机」。你调用drawCircle时,它就在当前位置画个圆。你调用translate时,它就把坐标系移动一下。理解了这个,后面学动画就轻松多了。
2.4 Paint:你的画笔
光有画板还不够,你还得有一支画笔。Paint就是干这个的。
你可以设置画笔的颜色、粗细、样式、抗锯齿等等。没有Paint,Canvas画出来的东西就是默认的黑色细线,丑得没法看。
private Paint mPaint;
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.RED); // 设置颜色
mPaint.setStrokeWidth(5f); // 设置线宽
mPaint.setStyle(Paint.Style.FILL); // 设置填充样式
mPaint.setAntiAlias(true); // 开启抗锯齿
}
2.5 实战:画一个带边框的圆形
光说不练假把式。我们来写一个完整的自定义View,画一个红色填充、蓝色边框的圆形。
public class CircleView extends View {
private Paint mFillPaint;
private Paint mStrokePaint;
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// 填充画笔
mFillPaint = new Paint();
mFillPaint.setColor(Color.RED);
mFillPaint.setStyle(Paint.Style.FILL);
mFillPaint.setAntiAlias(true);
// 边框画笔
mStrokePaint = new Paint();
mStrokePaint.setColor(Color.BLUE);
mStrokePaint.setStyle(Paint.Style.STROKE);
mStrokePaint.setStrokeWidth(8f);
mStrokePaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
float radius = Math.min(width, height) / 2f;
// 画填充圆
canvas.drawCircle(width / 2f, height / 2f, radius, mFillPaint);
// 画边框圆
canvas.drawCircle(width / 2f, height / 2f, radius, mStrokePaint);
}
}
这段代码里,我用了两支画笔。一支画填充,一支画边框。你可能会问:「为啥不用一支画笔切换样式?」
嗯,理论上可以。但实际项目中,我建议你每种样式单独建一个Paint对象。这样代码更清晰,也方便后续修改。我在项目里见过有人用一个Paint来回切换,结果改了一个地方,其他地方也跟着变了,调试起来特别头疼。
2.6 理解View的尺寸
在onDraw里,getWidth() 和 getHeight() 拿到的是View的实际宽高。但要注意,这两个值在onDraw被调用时才是有效的。如果你在构造方法里调用,拿到的就是0。
为什么会这样?因为View的测量和布局是在onDraw之前完成的。构造方法执行时,View还没被测量呢。
2.7 总结一下
今天的内容其实就三个核心点:
- 构造方法:负责初始化,把Paint等对象创建好
- onDraw:负责绘制,用Canvas和Paint画出你想要的内容
- Canvas + Paint:一个提供画板,一个提供画笔,缺一不可
我个人觉得,自定义View最难的不是语法,而是「思维转换」。你习惯了写布局文件里那些现成的控件,突然要自己控制每一个像素的绘制,确实需要适应一下。
但别急,后面几章我们会一步步深入。从简单的图形,到复杂的动画,再到性能优化。等你学完这30章,你会发现——自定义View其实挺有意思的。
下一章,我们来聊聊「测量与布局:onMeasure和onLayout的深入理解」。到时候我会分享一个我在实际项目中踩过的坑,保证让你印象深刻。