3. Canvas详解:Canvas的常用API与变换操作

Canvas,说白了就是你的画板。在Android自定义View里,所有你能看到的东西,最终都是通过Canvas画出来的。我刚开始学自定义View时,总觉得Canvas很神秘,后来才发现——它就是个带了一堆画笔工具的画布而已。

今天咱们就把Canvas的常用API和变换操作捋一遍。这些东西,你以后写自定义View几乎天天都要用。

3.1 基础绘制API

先说说最常用的几个绘制方法。嗯,这些是基本功,就像练武要先扎马步一样。

3.1.1 drawLine — 画线

canvas.drawLine(startX, startY, stopX, stopY, paint);

这个方法很简单,就是从起点到终点画一条直线。我在项目中遇到过一个问题:画网格线时,如果线条太多,性能会明显下降。后来我改用drawLines(带s的那个),一次性传入所有坐标数组,性能提升了不少。

小技巧:画多条线时,优先用 drawLines(float[] pts, Paint paint),而不是循环调用 drawLine。一次绘制调用比多次调用效率高得多。

3.1.2 drawRect — 画矩形

canvas.drawRect(left, top, right, bottom, paint);
// 或者用 RectF
canvas.drawRect(new RectF(left, top, right, bottom), paint);

画矩形有两种方式:直接传四个float,或者传一个RectF对象。我个人习惯用RectF,因为后续做动画或计算时,RectF的封装方法更方便。

你想想看,如果只是画一个静态矩形,用四个float就够了。但如果你要判断点击是否落在矩形内,或者要缩放矩形,RectF的contains()inset()方法就派上用场了。

3.1.3 drawCircle — 画圆

canvas.drawCircle(cx, cy, radius, paint);

画圆需要三个参数:圆心坐标和半径。这里有个坑——我曾经在画头像时,把半径传成了直径,结果头像被裁掉了一半。嗯,这种低级错误,犯过一次就记住了。

注意:Paint的样式会影响绘制效果。如果Paint是FILL模式,画出来的是实心圆;如果是STROKE模式,画出来的是空心圆。两者都想要?用FILL_AND_STROKE。

3.1.4 drawPath — 画路径

Path是Canvas里最强大的工具。它能画出任意形状——直线、曲线、弧线、贝塞尔曲线,组合起来就是各种复杂图形。

Path path = new Path();
path.moveTo(100, 100);      // 起点
path.lineTo(200, 200);      // 画直线
path.quadTo(300, 100, 400, 200);  // 画贝塞尔曲线
path.close();               // 闭合路径
canvas.drawPath(path, paint);

Path的API很多,但常用的就几个:moveTo(移动起点)、lineTo(画直线)、quadTo(二阶贝塞尔)、cubicTo(三阶贝塞尔)、addCircle(添加圆形子路径)、addRect(添加矩形子路径)。

我记得有一次做水波纹效果,就是用Path配合贝塞尔曲线实现的。说白了,Path就是你的「万能画笔」。

3.2 Canvas的变换操作

变换操作是Canvas的精髓。你想想看,如果每次画图都要手动计算坐标,那得多累?Canvas提供了平移、旋转、缩放这些操作,让你可以「换个姿势」画画。

3.2.1 translate — 平移

canvas.translate(dx, dy);
// 之后的所有绘制,坐标都会偏移 (dx, dy)

平移是最常用的变换。我习惯用它来做「坐标系搬家」——比如画一个仪表盘,先把原点移到View中心,然后所有刻度都基于中心点计算,省事多了。

核心理解:translate不是移动画布上的内容,而是移动了坐标系的原点。之后画的任何东西,坐标都是相对于新原点计算的。

3.2.2 rotate — 旋转

canvas.rotate(degrees, pivotX, pivotY);
// 绕 (pivotX, pivotY) 旋转 degrees 度

旋转操作,说白了就是让画布转个角度。我做过一个时钟控件,秒针的旋转就是靠canvas.rotate()实现的——每秒钟旋转6度,简单粗暴。

这里有个细节:rotate可以传两个参数(只传角度,默认绕原点旋转),也可以传三个参数(指定旋转中心)。我个人建议总是传三个参数,因为绕原点旋转的结果往往不是你想要的。

3.2.3 scale — 缩放

canvas.scale(sx, sy, pivotX, pivotY);
// 以 (pivotX, pivotY) 为中心,x方向缩放sx倍,y方向缩放sy倍

缩放操作要注意一点:缩放不仅会改变图形大小,还会改变画笔的粗细。我曾经在做一个缩放动画时,发现图形放大了,边框也变粗了——这就是因为缩放影响了画笔的strokeWidth。

避坑指南:如果你只想缩放图形,不想让边框跟着变粗,可以在缩放前先保存Canvas状态,缩放绘制完再恢复。或者,用Matrix来做变换,对画笔的控制更精细。

3.3 Canvas的状态保存与恢复

这个知识点,我觉得是Canvas里最重要的。没有之一。

canvas.save();      // 保存当前状态(平移、旋转、缩放等)
// ... 做一些变换和绘制 ...
canvas.restore();   // 恢复到之前保存的状态

save和restore要成对出现。它们就像括号一样,把变换操作「包裹」起来。我在项目中见过不少bug,都是因为忘了restore,导致后续绘制受到了前面变换的影响。

举个例子:

canvas.save();
canvas.translate(100, 100);
canvas.rotate(45);
canvas.drawRect(0, 0, 50, 50, paint);  // 这个矩形被平移并旋转了
canvas.restore();

canvas.drawRect(0, 0, 50, 50, paint);  // 这个矩形不受影响,在原位置

你想想看,如果没有save/restore,第二个矩形也会被平移和旋转,那画面就乱套了。

3.4 实战小例子:画一个带刻度的圆环

说了这么多,咱们来个小例子练练手。画一个圆环,上面有12个刻度——就像表盘那样。

// 假设View宽高都是400,中心在(200, 200)
int cx = 200, cy = 200, radius = 150;

Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(4);

// 画外圆
canvas.drawCircle(cx, cy, radius, paint);

// 画12个刻度
canvas.save();
canvas.translate(cx, cy);  // 把原点移到中心

for (int i = 0; i < 12; i++) {
    canvas.rotate(30);     // 每次旋转30度
    canvas.drawLine(0, -radius + 20, 0, -radius + 40, paint);
}

canvas.restore();

这段代码的核心思路是:每次旋转30度,然后在固定位置画一条线。因为坐标系一直在旋转,所以画出来的线就均匀分布在圆环上了。是不是很巧妙?

我的习惯:做这种循环绘制时,我通常会在循环外面save一次,循环结束后restore一次。不要在循环里反复save/restore,那样性能会差一些。

3.5 总结

Canvas的API其实不多,但组合起来能玩出花来。我个人觉得,掌握Canvas的关键就两点:

  • 理解坐标系——每次变换都在改变坐标系,你画的东西是相对于当前坐标系的
  • 用好save/restore——它们是变换操作的「结界」,保护你的绘制不受干扰

嗯,今天就先聊到这儿。下一章咱们会深入聊聊Paint——那个决定你画出来东西长什么样的「画笔」。到时候我会分享一些调色和特效的技巧,都是我踩过坑之后总结出来的。