3. 绘制基础图形:点、线、矩形、圆角矩形、圆形、椭圆

好,咱们进入真正的绘制环节了。前面讲了画布和画笔,现在该让它们干活了。

基础图形是自定义绘制的基石。说白了,你以后画的任何复杂界面,拆开来看,无非就是这些基本元素的组合。我个人习惯先把这些基础图形练熟,后面画图表、画动画时才不会手忙脚乱。

3.1 点:最小的绘制单元

在Flutter的Canvas里,点其实不是「画一个像素点」,而是画一个很小的圆。为什么?因为Canvas没有专门的「画点」方法,我们通常用drawCircle来实现。

void paint(Canvas canvas, Size size) {
  final paint = Paint()
    ..color = Colors.black
    ..strokeWidth = 2.0;

  // 画一个点:在 (100, 100) 位置,半径为 3
  canvas.drawCircle(Offset(100, 100), 3.0, paint);
}

嗯,这里要注意:半径设得越小,点就越细。但别设成0,那样就看不见了。我一般设2到4之间,看起来像个正常的点。

小技巧:如果你需要画很多点(比如散点图),别用循环一个个画,性能会很差。改用canvas.drawPoints(),一次调用搞定。

3.2 线:直线与路径

画线用drawLine,需要两个点:起点和终点。

canvas.drawLine(
  Offset(50, 50),   // 起点
  Offset(200, 50),  // 终点
  paint,
);

我曾经在项目里画折线图时,一开始用drawLine一条条画,代码又长又乱。后来改用Path,清爽多了:

final path = Path();
path.moveTo(50, 100);   // 起点
path.lineTo(100, 150);  // 画到第二个点
path.lineTo(150, 80);   // 画到第三个点
path.lineTo(200, 120);  // 画到第四个点

canvas.drawPath(path, paint);

你想想看,如果数据点有几十个,用drawLine得写多少行?用Path一个循环就搞定了。

注意:画线时,paint.style要设为PaintingStyle.stroke,否则画出来的是实心形状,不是线。我刚开始学时就踩过这个坑。

3.3 矩形:最常用的形状

矩形有两种画法:drawRectdrawRRect。前者画直角矩形,后者画圆角矩形。

// 直角矩形
final rect = Rect.fromLTWH(50, 50, 150, 100);
canvas.drawRect(rect, paint);

// 圆角矩形
final rrect = RRect.fromRectAndRadius(
  Rect.fromLTWH(50, 200, 150, 100),
  Radius.circular(12),
);
canvas.drawRRect(rrect, paint);

这里有个细节:Rect.fromLTWH的参数分别是 left、top、width、height。别搞混了顺序,我见过有人把width和height写反,结果矩形变形了。

方法 说明 适用场景
fromLTWH 左上角坐标 + 宽高 最常用
fromCircle 圆心 + 半径 画正方形外接矩形
fromPoints 两个对角点 动态计算矩形

3.4 圆形与椭圆

圆形用drawCircle,椭圆用drawOval。其实椭圆就是「被拉伸的圆」。

// 圆形:圆心 (100, 100),半径 50
canvas.drawCircle(Offset(100, 100), 50, paint);

// 椭圆:在矩形区域内绘制
final ovalRect = Rect.fromLTWH(50, 200, 150, 100);
canvas.drawOval(ovalRect, paint);

为什么椭圆要用矩形来定义?因为椭圆本质上就是矩形的内切图形。你给一个正方形,它就画正圆;给一个长方形,它就画椭圆。

核心理解:在Flutter Canvas中,所有「不规则」的图形,都可以用矩形来约束。圆角矩形、椭圆、甚至弧线,都是基于矩形区域计算的。

3.5 实战:画一个简单的仪表盘

光说不练假把式。咱们用刚学的知识,画一个简单的仪表盘背景:

void paint(Canvas canvas, Size size) {
  final bgPaint = Paint()
    ..color = Colors.grey[200]!
    ..style = PaintingStyle.fill;

  final borderPaint = Paint()
    ..color = Colors.blue
    ..style = PaintingStyle.stroke
    ..strokeWidth = 3.0;

  // 画背景圆
  canvas.drawCircle(
    Offset(size.width / 2, size.height / 2),
    100,
    bgPaint,
  );

  // 画边框圆
  canvas.drawCircle(
    Offset(size.width / 2, size.height / 2),
    100,
    borderPaint,
  );

  // 画刻度线(用矩形模拟)
  for (int i = 0; i < 12; i++) {
    double angle = i * 30 * 3.14159 / 180;
    double x1 = size.width / 2 + 80 * cos(angle);
    double y1 = size.height / 2 + 80 * sin(angle);
    double x2 = size.width / 2 + 95 * cos(angle);
    double y2 = size.height / 2 + 95 * sin(angle);

    canvas.drawLine(
      Offset(x1, y1),
      Offset(x2, y2),
      borderPaint,
    );
  }
}

这段代码里,圆形是仪表盘的外壳,线条是刻度。你看,基础图形组合起来,就能做出像模像样的东西了。

我的经验:刚开始练习时,别急着画复杂图形。先把点、线、矩形、圆这四种图形练熟,每个都画上几十遍。等你闭着眼睛都能写出drawCircle的参数顺序时,再去看那些炫酷的自定义绘制,会发现——哦,原来都是这些基础图形的排列组合。

好了,基础图形就讲到这里。下一节咱们聊聊路径的高级玩法,比如贝塞尔曲线和弧线。那些才是真正让绘制「活起来」的东西。