10、Canvas与Drawable:Canvas的常用API、Drawable的绘制、自定义Drawable
说到自定义View,Canvas和Drawable是绕不开的两个核心角色。我刚开始接触这部分时,总觉得Canvas就是个画板,Drawable就是个图片,后来踩了不少坑才明白——它们之间的关系远比想象中微妙。
今天我们就来彻底捋清楚。我会从Canvas的常用API讲起,再深入到Drawable的绘制机制,最后带你手写一个自定义Drawable。嗯,都是实战中摸爬滚打出来的经验。
核心要点:Canvas是“怎么画”,Drawable是“画什么”。两者配合,才能构建出高效的UI渲染体系。
10.1 Canvas的常用API——画布上的基本功
Canvas就像一张无限大的画布。你拿着画笔(Paint),在上面画点、线、面。说白了,View的onDraw方法里拿到的Canvas,就是系统给你准备好的画布。
我个人习惯把Canvas的API分成三类:几何图形、位图、变换与裁剪。咱们一个一个来看。
10.1.1 画线:drawLine
这个API很简单,但用好了能画出很复杂的东西。比如折线图、网格线、甚至波形。
// 画一条直线
canvas.drawLine(startX, startY, stopX, stopY, paint);
// 画多条线(性能更好)
canvas.drawLines(float[] pts, paint);
// pts 数组格式:[x1,y1,x2,y2, x3,y3,x4,y4, ...]
小技巧:如果你要画很多条线,用drawLines比循环调用drawLine性能好得多。我在做股票K线图时,一次要画上千条线,用drawLines帧率能提升30%以上。
10.1.2 画矩形:drawRect
矩形是UI里最常用的形状。按钮、卡片、进度条,本质上都是矩形。
// 方式一:四个坐标
canvas.drawRect(left, top, right, bottom, paint);
// 方式二:Rect对象
Rect rect = new Rect(left, top, right, bottom);
canvas.drawRect(rect, paint);
// 方式三:RectF(带浮点精度)
RectF rectF = new RectF(left, top, right, bottom);
canvas.drawRect(rectF, paint);
你可能会问:这三种方式有什么区别?嗯,Rect存的是int,RectF存的是float。如果你做动画或者精细布局,RectF更合适。Rect适合静态场景。
10.1.3 画圆角矩形:drawRoundRect
现在App里到处都是圆角。drawRoundRect就是干这个的。
canvas.drawRoundRect(left, top, right, bottom, rx, ry, paint);
// rx、ry 分别是x轴和y轴的圆角半径
注意:rx和ry不要超过矩形宽高的一半,否则圆角会变形。我曾经在适配平板时踩过这个坑,圆角直接变成了椭圆。
10.1.4 画圆和椭圆:drawCircle / drawOval
// 画圆
canvas.drawCircle(cx, cy, radius, paint);
// 画椭圆(外接矩形)
canvas.drawOval(left, top, right, bottom, paint);
drawOval其实画的是椭圆,它的形状由外接矩形决定。如果你传一个正方形,画出来的就是正圆。
10.1.5 画弧线:drawArc
这个API做饼图、环形进度条时特别有用。
canvas.drawArc(rectF, startAngle, sweepAngle, useCenter, paint);
// startAngle: 起始角度(3点钟方向为0度)
// sweepAngle: 扫过的角度
// useCenter: 是否连接到圆心
useCenter这个参数很有意思。设为true时画的是扇形,设为false时画的是弧线。做环形进度条时记得设为false。
10.1.6 画位图:drawBitmap
这是性能最敏感的一个API。我见过太多人因为Bitmap用不好导致OOM。
// 最简单的方式
canvas.drawBitmap(bitmap, left, top, paint);
// 带矩阵变换
canvas.drawBitmap(bitmap, matrix, paint);
// 只画一部分(源区域映射到目标区域)
canvas.drawBitmap(bitmap, srcRect, dstRect, paint);
性能建议:drawBitmap时尽量复用Bitmap对象,不要频繁创建。另外,srcRect和dstRect的尺寸比例最好一致,否则会触发缩放,影响性能。
10.1.7 画文字:drawText
文字绘制比想象中复杂。基线对齐、字体度量、换行处理,每个都是坑。
canvas.drawText(text, x, y, paint);
// 注意:x和y是文字基线的起点,不是左上角
为什么是基线?因为不同语言的文字高度不一样,基线能保证对齐。我记得第一次做多语言适配时,中文和英文混排总是对不齐,后来才搞明白基线的作用。
10.2 Drawable的绘制——不只是图片
很多人以为Drawable就是图片。其实它是个抽象类,任何可以绘制的东西都能封装成Drawable。比如颜色、渐变、形状、甚至是一段动画。
10.2.1 Drawable的核心方法
| 方法 | 作用 | 说明 |
|---|---|---|
| draw(Canvas) | 绘制内容 | 核心方法,所有子类必须实现 |
| setBounds(Rect) | 设置绘制区域 | 不设置的话,draw可能不显示 |
| getIntrinsicWidth/Height | 获取固有尺寸 | 比如图片的原始宽高 |
| setAlpha / setColorFilter | 设置透明度/颜色滤镜 | 用于状态变化 |
你想想看,Drawable和View的区别是什么?View有完整的生命周期和事件处理,Drawable只是一个绘制单元。它轻量、可复用,适合做背景、图标、状态指示器。
10.2.2 Drawable的绘制流程
当一个Drawable被设置到View上时,流程是这样的:
- View调用Drawable的setBounds,确定绘制区域
- View在onDraw中调用Drawable的draw方法
- Drawable内部使用Canvas进行绘制
说白了,Drawable就是帮你封装了一套绘制逻辑。你不需要关心它怎么画,只需要告诉它画在哪里。
10.3 自定义Drawable——从入门到实战
系统提供的Drawable有时候不够用。比如你想做一个带圆环的圆形头像,或者一个渐变的进度条。这时候就需要自定义Drawable。
10.3.1 自定义Drawable的基本步骤
public class CircleDrawable extends Drawable {
private Paint paint;
private int color;
public CircleDrawable(int color) {
this.color = color;
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setStyle(Paint.Style.FILL);
}
@Override
public void draw(Canvas canvas) {
// 获取绘制区域
Rect bounds = getBounds();
float cx = bounds.centerX();
float cy = bounds.centerY();
float radius = Math.min(cx, cy);
// 画圆
canvas.drawCircle(cx, cy, radius, paint);
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
invalidateSelf();
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
invalidateSelf();
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
看到了吗?核心就三步:实现draw方法、处理setAlpha和setColorFilter、返回透明度。其他都是辅助。
经验之谈:自定义Drawable时,记得调用invalidateSelf()来触发重绘。我刚开始写的时候经常忘记,结果改了颜色界面没反应,排查了半天。
10.3.2 实战:带边框的圆形Drawable
这个需求很常见——用户头像,圆形,带一圈边框。
public class BorderCircleDrawable extends Drawable {
private Paint fillPaint;
private Paint borderPaint;
private float borderWidth;
public BorderCircleDrawable(int fillColor, int borderColor, float borderWidth) {
this.borderWidth = borderWidth;
fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
fillPaint.setColor(fillColor);
fillPaint.setStyle(Paint.Style.FILL);
borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
borderPaint.setColor(borderColor);
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(borderWidth);
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
float cx = bounds.centerX();
float cy = bounds.centerY();
float radius = Math.min(cx, cy) - borderWidth / 2;
// 先画填充
canvas.drawCircle(cx, cy, radius, fillPaint);
// 再画边框
canvas.drawCircle(cx, cy, radius, borderPaint);
}
// setAlpha、setColorFilter、getOpacity 省略...
}
这里有个细节:边框的宽度要算在半径里,否则边框会超出边界。我刚开始没注意,结果边框被裁掉了一半。
10.3.3 Drawable vs 自定义View
什么时候用Drawable,什么时候用自定义View?我个人的经验是:
- 用Drawable:只需要绘制,不需要交互。比如背景、图标、进度条样式
- 用自定义View:需要触摸事件、动画、复杂的布局逻辑
Drawable更轻量,可以复用在多个View上。一个Drawable实例可以同时被多个View使用,而自定义View不行。
总结一下:Canvas是画笔和画布,Drawable是画好的图案。掌握Canvas的API让你能画出任何东西,掌握Drawable让你能封装和复用这些绘制逻辑。两者结合,才能写出高效、可维护的自定义UI。
嗯,这一章的内容就到这里。Canvas的API还有很多,比如clipRect、save/restore、矩阵变换等,这些我们后面会专门讲。Drawable的自定义也远不止这些,状态选择器、动画Drawable都是进阶内容。
记住一句话:画布在手,天下我有。多动手写,多踩坑,自然就熟练了。