第2章:Kotlin绘图基础:Canvas与Paint对象详解、坐标系与屏幕坐标、绘制基本图形

各位同学,欢迎来到第二章。上一章我们搭好了开发环境,也跑通了第一个Demo。今天咱们来点真家伙——Canvas和Paint,这两个家伙是Android绘图的左膀右臂。我刚开始学的时候,总觉得它们像黑魔法,后来才发现,说白了就是「画布」和「画笔」的关系。

2.1 Canvas与Paint:绘图界的「纸」和「笔」

先问大家一个问题:你小时候在纸上画画,需要什么?一张纸,一支笔,对吧?Canvas就是那张纸,Paint就是那支笔。只不过在Android里,这张纸是虚拟的,这支笔可以调粗细、颜色、样式。

Canvas 负责「怎么画」——画线、画圆、画矩形,都是它的方法。
Paint 负责「画成什么样」——红色还是蓝色?实线还是虚线?要不要抗锯齿?

我个人习惯把Paint比作「瑞士军刀」,因为它能调的东西太多了。咱们先看一个最简单的例子:

// 创建一个画笔
val paint = Paint().apply {
    color = Color.RED          // 红色
    strokeWidth = 5f           // 线宽5像素
    style = Paint.Style.STROKE // 只画轮廓,不填充
    isAntiAlias = true         // 开启抗锯齿,边缘更平滑
}

// 在Canvas上画一条线
canvas.drawLine(100f, 100f, 500f, 100f, paint)

嗯,这里要注意:isAntiAlias 这个属性,我建议你永远设为 true。为什么?因为不开抗锯齿,斜线会有明显的锯齿感,像狗啃的一样。我在项目中遇到过,用户反馈说「这个圆怎么有毛刺」,其实就是忘了开抗锯齿。

小技巧:Paint的style有三种:FILL(填充)、STROKE(描边)、FILL_AND_STROKE(既填充又描边)。画空心图形用STROKE,画实心图形用FILL,这个很直观。

2.2 坐标系与屏幕坐标:别搞反了Y轴

Android的坐标系,跟数学课上学的有点不一样。数学课上,Y轴向上是正方向。但在Android里,Y轴向下是正方向。你想想看,手机屏幕左上角是(0,0),往右X增加,往下Y增加。

我第一次写自定义View时,想画一个从左上角到右下角的对角线,结果画反了。后来才意识到,Y轴方向搞反了。

来看一张坐标示意图(用文字描述):

(0,0) ——————→ X 正方向
  |
  |
  ↓
Y 正方向

屏幕坐标有几个关键点:

  • 原点:屏幕左上角 (0, 0)
  • X轴:向右为正
  • Y轴:向下为正
  • 宽高:通过 canvas.widthcanvas.height 获取
避坑指南:我曾经在做一个图表控件时,把Y轴坐标写成了数学坐标系,结果所有柱状图都倒着长了。后来我加了一个辅助函数:fun Float.toScreenY(canvasHeight: Float) = canvasHeight - this,把数学Y转换成屏幕Y。这个习惯我一直保留到现在。

2.3 绘制基本图形:点、线、矩形、圆、椭圆

好了,理论说完了,咱们动手画点东西。我习惯先画一个「Hello World」级别的图形集合,这样能快速验证Canvas和Paint是否配合正常。

2.3.1 画点

画点用 drawPoint,但要注意,点其实是一个小圆。如果你把线宽设大一点,点看起来就是个实心圆。

val pointPaint = Paint().apply {
    color = Color.BLUE
    strokeWidth = 10f   // 点的大小由线宽决定
    style = Paint.Style.FILL
    isAntiAlias = true
}
canvas.drawPoint(200f, 300f, pointPaint)

2.3.2 画线

画线用 drawLine,四个参数分别是:起点X、起点Y、终点X、终点Y。

val linePaint = Paint().apply {
    color = Color.GREEN
    strokeWidth = 3f
    style = Paint.Style.STROKE
}
canvas.drawLine(50f, 50f, 400f, 50f, linePaint)  // 水平线
canvas.drawLine(50f, 50f, 50f, 400f, linePaint)  // 垂直线

如果想画多条连续的线,可以用 drawLines,传入一个float数组,两两一组:

val points = floatArrayOf(
    100f, 100f,  // 点1
    200f, 200f,  // 点2
    300f, 150f,  // 点3
    400f, 250f   // 点4
)
canvas.drawLines(points, linePaint)

2.3.3 画矩形

矩形用 drawRect,参数是左上角和右下角的坐标。也可以传一个 RectF 对象。

val rectPaint = Paint().apply {
    color = Color.MAGENTA
    style = Paint.Style.FILL
}
canvas.drawRect(100f, 100f, 300f, 200f, rectPaint)  // 实心矩形

// 空心矩形
rectPaint.style = Paint.Style.STROKE
rectPaint.strokeWidth = 4f
canvas.drawRect(350f, 100f, 550f, 200f, rectPaint)
重点:RectF的四个参数是 left, top, right, bottom。left必须小于right,top必须小于bottom,否则画不出来。我刚开始写的时候,把left和right写反了,结果矩形消失了,排查了半天。

2.3.4 画圆

画圆用 drawCircle,三个参数:圆心X、圆心Y、半径。

val circlePaint = Paint().apply {
    color = Color.CYAN
    style = Paint.Style.FILL
    isAntiAlias = true
}
canvas.drawCircle(200f, 300f, 100f, circlePaint)  // 圆心(200,300),半径100

画空心圆也很简单,把style改成STROKE就行:

circlePaint.style = Paint.Style.STROKE
circlePaint.strokeWidth = 8f
canvas.drawCircle(450f, 300f, 100f, circlePaint)

2.3.5 画椭圆

椭圆用 drawOval,参数是一个 RectF,椭圆会被限制在这个矩形框内。

val ovalPaint = Paint().apply {
    color = Color.YELLOW
    style = Paint.Style.FILL
    isAntiAlias = true
}
val ovalRect = RectF(100f, 400f, 300f, 500f)  // 宽200,高100
canvas.drawOval(ovalRect, ovalPaint)

你想想看,如果这个矩形是正方形,画出来的椭圆就是圆。所以圆其实是椭圆的特例。

2.4 综合示例:画一个小房子

咱们把上面学的综合一下,画一个简单的小房子。这个例子我在教新同事时经常用,能快速检验基本功。

// 在自定义View的onDraw中
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    // 1. 画房子主体(矩形)
    val housePaint = Paint().apply {
        color = Color.rgb(200, 150, 100)  // 棕色
        style = Paint.Style.FILL
    }
    canvas.drawRect(100f, 200f, 400f, 450f, housePaint)

    // 2. 画屋顶(三角形,用三条线)
    val roofPaint = Paint().apply {
        color = Color.RED
        strokeWidth = 5f
        style = Paint.Style.STROKE
    }
    canvas.drawLine(100f, 200f, 250f, 100f, roofPaint)  // 左斜边
    canvas.drawLine(250f, 100f, 400f, 200f, roofPaint)  // 右斜边
    canvas.drawLine(100f, 200f, 400f, 200f, roofPaint)  // 底边

    // 3. 画门(矩形)
    val doorPaint = Paint().apply {
        color = Color.rgb(100, 50, 0)  // 深棕色
        style = Paint.Style.FILL
    }
    canvas.drawRect(220f, 320f, 280f, 450f, doorPaint)

    // 4. 画窗户(圆)
    val windowPaint = Paint().apply {
        color = Color.CYAN
        style = Paint.Style.FILL
    }
    canvas.drawCircle(150f, 280f, 30f, windowPaint)
    canvas.drawCircle(350f, 280f, 30f, windowPaint)
}

运行一下,你应该能看到一个棕色的小房子,红色屋顶,一扇门,两扇圆窗户。虽然简陋,但五脏俱全。

我的经验:刚开始练习时,别急着画复杂图形。把点、线、矩形、圆、椭圆这五种基本图形练熟,每个都试一遍FILL和STROKE两种样式。我当年花了整整一个下午,就画这五个图形,换不同的颜色、线宽、位置。后来画任何复杂图形,心里都有底。

2.5 本章小结

这一章我们干了三件事:

  • 认识了Canvas(画布)和Paint(画笔)这对黄金搭档
  • 搞清楚了Android坐标系——Y轴向下为正,别搞反了
  • 亲手画了点、线、矩形、圆、椭圆,还搭了个小房子

下一章,咱们要玩点更高级的——路径(Path)和贝塞尔曲线。到时候你会发现,原来那些圆润的图标、流畅的动画,背后都是这些基础知识的组合。嗯,先消化今天的内容,动手敲一遍代码,比看十遍都管用。

记住:绘图没有捷径,多画就对了。