一、初识Canvas:什么是Canvas、Canvas与Widget的关系、CustomPaint组件详解

1.1 到底什么是Canvas?

先问大家一个问题:你平时用Flutter写界面,是不是都在拼Widget?

嗯,这没错。但你想过没有——那些圆角、阴影、渐变,底层是怎么画出来的?

说白了,Canvas就是一张画布。你告诉它「在这里画一条线」「在那里画一个圆」,它就老老实实给你画出来。我刚开始接触Flutter自定义绘制时,总觉得Canvas很神秘,后来发现它其实就是个「绘图指令的集合」。

举个例子:

canvas.drawCircle(Offset(100, 100), 50, paint);

这行代码的意思很简单:在坐标(100, 100)的位置,画一个半径50的圆,用指定的画笔。你看,是不是很直观?

我在项目中遇到过这样一个场景:产品要求做一个波浪进度条,用普通Widget堆叠简直要命。后来用Canvas画了个贝塞尔曲线,十几行代码就搞定了。那一刻我才真正理解——Canvas是Flutter绘制的底层基石

核心理解:Canvas不存储任何图形数据,它只负责「执行绘制命令」。每次重绘,所有命令都会重新执行一遍。

1.2 Canvas与Widget的关系

很多初学者会问:我平时用的Container、Text这些Widget,跟Canvas有什么关系?

答案是:所有Widget最终都要靠Canvas来渲染

你想想看,Flutter的渲染流程是这样的:

  1. Widget树构建
  2. Element树同步
  3. RenderObject布局
  4. 最终通过Canvas绘制到屏幕上

也就是说,你写的每一个Container,底层都会生成对应的绘制指令,交给Canvas去执行。只不过这些细节被Flutter封装好了,你看不到而已。

我个人习惯把Widget和Canvas的关系比作「乐高积木 vs 3D打印」:

  • Widget:像乐高,用现成的积木块拼装,方便但有限制
  • Canvas:像3D打印,你想画什么就画什么,自由度极高

举个例子,你想画一个自定义的仪表盘:

class DashboardWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: DashboardPainter(),
      size: Size(200, 200),
    );
  }
}

class DashboardPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 这里你可以用Canvas画任何东西
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 2.0
      ..style = PaintingStyle.stroke;
    
    canvas.drawCircle(size.center(Offset.zero), size.width / 2, paint);
  }
  
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

你看,通过CustomPaint这个「桥梁」,我们就能直接操作Canvas了。

我的建议:能用Widget搞定的,优先用Widget。只有遇到Widget搞不定的复杂图形,才考虑用Canvas。别为了炫技而滥用自定义绘制。

1.3 CustomPaint组件详解

CustomPaint是Flutter提供的一个「画板组件」。它有三个核心参数:

参数 类型 说明
painter CustomPainter? 前景绘制器,在child之上绘制
foregroundPainter CustomPainter? 背景绘制器,在child之下绘制
child Widget? 子组件,位于两个绘制层之间

这里有个细节要注意:painter和foregroundPainter的区别。我曾经踩过这个坑——想在文字上面画一个遮罩层,结果用了painter,遮罩被文字盖住了。后来才发现,应该用foregroundPainter。

来看一个完整的例子:

class MyCustomPaint extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      // 背景绘制:画一个渐变矩形
      painter: BackgroundPainter(),
      // 中间放一个文本
      child: Center(
        child: Text(
          'Hello Canvas',
          style: TextStyle(fontSize: 24, color: Colors.white),
        ),
      ),
      // 前景绘制:画一个半透明遮罩
      foregroundPainter: ForegroundPainter(),
      size: Size(300, 200),
    );
  }
}

class BackgroundPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final rect = Rect.fromLTWH(0, 0, size.width, size.height);
    final gradient = LinearGradient(
      colors: [Colors.blue, Colors.purple],
    );
    final paint = Paint()..shader = gradient.createShader(rect);
    canvas.drawRect(rect, paint);
  }
  
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

class ForegroundPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.black.withOpacity(0.3);
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      paint,
    );
  }
  
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

注意:CustomPaint的size参数如果不指定,会尽可能填满父容器。但如果父容器没有约束,size会变成(0,0),导致什么都画不出来。我刚开始学的时候就犯过这个错,调试了半天才发现是size没设置。

1.4 关于shouldRepaint

这个方法是CustomPainter里必须实现的。它决定了什么时候需要重绘。

简单来说:

  • 如果绘制内容不变,返回false——避免不必要的重绘
  • 如果绘制内容变了,返回true——触发重绘

举个例子,如果你画的是一个静态图形,直接返回false就行:

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;

但如果你画的是动画,就需要根据新旧数据判断:

class AnimatedPainter extends CustomPainter {
  final double progress;
  
  AnimatedPainter(this.progress);
  
  @override
  void paint(Canvas canvas, Size size) {
    // 根据progress绘制不同状态
  }
  
  @override
  bool shouldRepaint(covariant AnimatedPainter oldDelegate) {
    return oldDelegate.progress != progress;
  }
}

性能小贴士:shouldRepaint返回true时,整个painter的paint方法会重新执行。如果绘制逻辑很复杂,记得做性能优化。我一般会用RepaintBoundary来隔离重绘区域,避免整个页面都跟着重绘。

1.5 小结

好了,这一章的内容就这些。我们来回顾一下:

  • Canvas:一张画布,负责执行绘制命令
  • Widget与Canvas的关系:Widget是封装好的积木,Canvas是底层的画笔
  • CustomPaint:连接Widget和Canvas的桥梁,有三个核心参数

下一章,我们会深入Canvas的绘制API,从最简单的线条开始,一步步画出复杂的图形。到时候我会分享一些我在实际项目中总结的「绘制套路」,保证让你少走弯路。

嗯,今天就到这里。有什么问题,欢迎在评论区交流。