Article / 2026/06/01

Flutter Canvas

构造

Canvas 绘图

CustomPaint组件

构造

class CustomPaint extends SingleChildRenderObjectWidget {
  /// Creates a widget that delegates its painting.
  const CustomPaint({
    super.key,
    this.painter,
    this.foregroundPainter,
    this.size = Size.zero,
    this.isComplex = false,
    this.willChange = false,
    super.child,
  })
  • painter:绘制的对象,是一个CustomPainter。它的绘制是在child之前。如果设置了child,该painter绘制的内容会被覆盖。
  • foregroundPainter:绘制的对象,是一个CustomPainter。它的绘制是在child之后。如果设置了child,该painter绘制的内容会覆盖child。
  • size: 画板大小,如果定义了child,则会以child的尺寸为准
  • isComplex: 默认值是false,定义绘制内容是否复杂,如果为true,会对canvas的绘制进行一些必要的缓存来优化性能
  • willChange: 默认值是false,配合isComplex使用,控制组件是否在下一帧需要重绘
  • child: 子节点,可以不设置

CustomPainter 组件

CustomPainter是一个抽象类,其构造函数如下

const CustomPainter({ Listenable? repaint })
  • repaint: 是一个Listenable,一般用于动画时,传入一个监听来控制canvas组件的重绘
  • void paint(Canvas canvas, Size size)

这个是我们定义painter时必须实现的方法,其中canvas就是提供出我们绘制的核心,size是告诉我们画板的大小(通过CustomPaint的size或者child确定)

  • bool shouldRepaint(covariant CustomPainter oldDelegate)

返回 true 才会进行重绘,否则就只会绘制一次。你可以通过一些条件判断来决定是否每次绘制,这样能够节约系统资源。(注:有时候不管这里返回的是false还是true,外面的变化也能导致重新绘制,这里为什么是这样,后面的文章会给出解释)

Paint

Paint画笔

  • isAntiAlias: 是否抗锯齿
  • color: 画笔颜色
  • strokeWidth: 画笔宽度
  • style: 样式
    • PaintingStyle.fill 默认 填充
    • PaintingStyle.stroke 线
  • strokeCap: 定义画笔端点形状
    • StrokeCap.butt 无形状(默认)
    • StrokeCap.round 圆形
    • StrokeCap.square 正方形
  • strokeJoin: 定义线段交接时的形状
    • StrokeJoin.miter 默认,当两条线段夹角小于30°时,StrokeJoin.miter将会变成StrokeJoin.bevel 夹角的形状为尖角
    • StrokeJoin.bevel 夹角的形状是平的
    • StrokeJoin.round 夹角的形状为圆的
  • strokeMiterLimit: 当strokeJoin为StrokeJoin.miter时且style为PaintingStyle.stroke有效,用来设置连接线的长度,一般可用strokeJoin来替换
  • imageFilter: 设置模糊度
    • ImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0, TileMode tileMode = TileMode.clamp}): sigmaX与sigmaY在0~10之间,数值越大越模糊
    • ImageFilter.matrix 使用matrix来创建模糊度
    • ImageFilter.compose 组合两个ImageFilter
  • invertColors: 反转画笔颜色(跟设置的color有关)
  • blendMode: 混合模式,两个形状混合时使用的模式,具体可参考blendMode,默认为BlendMode.srcOver
  • shader: 着色器
  • maskFilter: 模糊蒙版滤镜,比如绘制一些阴影效果或者艺术字等
  • filterQuality: 设置滤镜(如maskFilter或者image)的质量
  • colorFilter: 彩色矩阵滤色器,可以通过设置此属性改变画笔颜色如黑白色

Path

方法名作用
moveTo将路径起始点移动到指定的位置
relativeMoveTo相对于当前位置移动到
lineTo从当前位置连接指定点
relativeLineTo相对当前位置连接到
arcTo曲线
conicTo贝塞尔曲线
add**添加其他图形,如addArc,在路径是添加圆弧
contains路径上是否包括某点
transfor给路径做matrix4变换
combine结合两个路径
close关闭路径,连接路径的起始点
reset重置路径,恢复到默认状态

Path路径api 详解

  • void moveTo(double x, double y)

设置画笔开始的位置

  • external void relativeLineTo(double dx, double dy)

与lineTo类似,不过传入的是相对于上一个点为原点的位置,比如上一个点是在(100,100),传入的是(150,150),如果用lineTo要达到同样效果应该传入(250,250)。

  • lineTo

绘制的下一个位置,传入的是相对于坐标系的具体位置,会按照代码顺序进行移动绘制

  • void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)

此方法同drawArc类似,前三个参数都一样,最后一个参数表示是否跟path之前的绘制(如通过lineTo绘制的线段)相连,false表示连接,true表示不相连。

  • void quadraticBezierTo(double x1, double y1, double x2, double y2)
var path = Path();
path.moveTo(50, 500);
path.quadraticBezierTo(100, 300, 350, 300);
canvas.drawPath(path1, paint);
  • void conicTo(double x1, double y1, double x2, double y2, double w)

同样也是绘制二阶贝塞尔曲线,但是同quadraticBezierTo相比,它多了一个参数w,用于控制曲线的弧度。当 w < 1 时,曲线弧度更小;w = 1 时同quadraticBezierTo效果一样;w > 1 时,弧度更大

  • void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)

三阶贝塞尔曲线相比二阶贝塞尔曲线只是多了一个点

  • addRect(Rect.fromLTRB(50, 50, 350, 350));

绘制一个矩形区域

  • addRRect

绘制一个带圆角的矩形

RRect rRect = RRect.fromLTRBR(100, 100, 350, 350, Radius.circular(30));
path.addRRect(rRect);
  • addArc

绘制一个圆弧

Path path = new Path();
// 画一个矩形区域
Rect rect = Rect.fromCircle(
    center: Offset(size.width / 2, size.height / 2), radius: 100);
canvas.drawRect(rect, paint);
// 在矩形区域画圆弧
path.addArc(rect, 90 * (pi / 180), 90 * (pi / 180));
paint.color = Colors.red;
canvas.drawPath(path, paint);
  • addOval

绘制一个椭圆

Rect pRect = Rect.fromLTRB(50, 150, 400, 350);
path.addOval(pRect);
canvas.drawPath(path, paint);
  • void addPolygon(List points, bool close)

通过点绘制线段 point: 传入多个点的位置 close: 为true时最后一个点会和第一个点相连

Path path = new Path();
path.addPolygon([
      Offset(100, 100),
      Offset(250, 180),
      Offset(200, 300),
    ], false);
canvas.drawPath(path, paint);
  • PathMetrics computeMetrics({bool forceClosed = false})

computeMetrics方法用于返回一个之前绘制的路径的一份快照。当我们使用moveTo、lineTo、arcTo、conicTo等绘制路径时,可以使用此来实现只绘制其中一部分。

var path = Path();
path.moveTo(50, 500);
path.cubicTo(50, 200, 300, 400, 350, 150);
// 将完整绘制图形置为红色
paint.color = Colors.red;
canvas.drawPath(path, paint);
ui.PathMetrics pathMetrics = path.computeMetrics();
// 绘制一半
var progress = 0.5;
// 将颜色更改为紫色用于区分
paint.color = Colors.deepPurple;
for (ui.PathMetric pathMetric in pathMetrics) {
  Path extractPath = pathMetric.extractPath(
    0.0,
    pathMetric.length * progress,
  );
  canvas.drawPath(extractPath, paint);
}
  • void extendWithPath(Path path, Offset offset, {Float64List matrix4})

用于复制一份之前绘制的路径并平移offset的位置,原路径会和新路径连接。matrix4是对新路径进行一个4D矩阵处理。

var path = Path();
path.moveTo(50, 500);
// 绘制一个三阶贝塞尔曲线
path.cubicTo(50, 200, 300, 400, 350, 150);
// 处理
path.extendWithPath(path, Offset(50, 30),
    matrix4: Float64List.fromList(
        [1, 0, 0, 0, .1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2]));
canvas.drawPath(path, paint);
  • Path shift(Offset offset)

可用于复制之前绘制的路径并平移offset位置,返回Path。与extendWithPath不同的是,此方法仅仅是复制路径,不会跟原路径相连。

var path = Path();
path.moveTo(50, 500);
path.cubicTo(50, 200, 300, 400, 350, 150);

// 原始图形为红色
paint.color = Colors.red;
canvas.drawPath(path, paint);
// 复制原路径并平移
var path2 = path.shift(Offset(50,20));
paint.color = Colors.yellow;
canvas.drawPath(path2, paint);
  • void close()

用于将路径起点与终点连接起来

  • void drawShadow(Path path, Color color, double elevation, bool transparentOccluder)

绘制阴影 path 绘制阴影的路径, color 绘制阴影的颜色, elevation 绘制阴影的范围, transparentOccluder 是否填充path

var path = Path()
  ..moveTo(50.0, 50.0)
  ..lineTo(300.0, 50.0)
  ..lineTo(300.0, 200.0)
  ..lineTo(50.0, 200.0)
  ..close();
canvas.drawShadow(path, Colors.blue, 3, false);
// 向下平移到200 elevation设置为10
canvas.drawShadow(path.shift(Offset(0, 200)), Colors.blue, 10, false);
// 向下移动到400 transparentOccluder设置为true效果
canvas.drawShadow(path.shift(Offset(0, 400)), Colors.blue, 10, true);
  • void drawCircle(Offset c, double radius, Paint paint)

绘制圆 c 绘制的圆心位置 radius 绘制圆半径

canvas.drawCircle(Offset(200,200), 100, paint);
  • void drawDRRect(RRect outer, RRect inner, Paint paint)

outer 外部形状,由一个RRect构成(为啥是RRect,因为RRect几乎涵盖了所有的闭合形状,如矩形、正方形、圆角矩形、椭圆、圆) inner 内部形状

// 画个圆角矩形
RRect rrect = RRect.fromRectXY(
    Rect.fromCircle(center: Offset(200, 200), radius: 150), 20.0, 40.0);
// 画个圆
RRect rrect1 = new RRect.fromRectXY(
    Rect.fromCircle(center: Offset(200, 200), radius: 80), 100.0, 100.0);
canvas.drawDRRect(rrect, rrect1, paint);

Canvas

Canvas详解

  • save()

操作会保存此前的所有绘制内容和 Canvas 状态。

  • restore()

在调用 save() 或者 saveLayer() 必须调用 restore() 来合成,否则 Flutter 会抛出异常。 值得注意的是,每一个 save() 或者 saveLayer() 都必须有一个对应的 restore()。

  • saveLayer()

saveLayer() 在大多数情况下看起来和 save() 的效果是差不多的。 不同的是 saveLayer() 会创建一个新的图层。 在 saveLayer() 到 restore() 之间的操作,是在新的图层上进行的,虽然最终它们还是会合成到一起。rect Rect,用于设置新图层的范围区域。你的绘制操作只有在这个区域内才会有效,超过这个区域的部分会被忽略

  • translate()

平移

  • scale()

用于将画布进行缩放

  • rotate()

画布的旋转

  • void drawPicture(Picture picture)

将Picture 绘制到画布上 Picture 对象通过 PictureRecorder 来生成

import 'package:flutter/material.dart';
import 'dart:ui';

class CustomImage extends StatefulWidget {
  const CustomImage({Key? key}) : super(key: key);

  @override
  State<CustomImage> createState() => _CustomImageState();
}

class _CustomImageState extends State<CustomImage> {
  Picture? picture;

  Future<Picture> _generatePicture() async {
    var p = PictureRecorder();
    var paint = Paint()
      ..isAntiAlias = true
      ..strokeWidth = 2
      ..style = PaintingStyle.fill
      ..strokeCap = StrokeCap.round
      ..color = Colors.red;

    var paintRect = Paint()
      ..isAntiAlias = true
      ..strokeWidth = 2
      ..style = PaintingStyle.fill
      ..strokeCap = StrokeCap.round
      ..color = Colors.white;

    var rect = const Rect.fromLTRB(10, 20, 190, 300);
    var canvas = Canvas(p, rect);
    canvas.drawRect(rect, paintRect);
    canvas.drawCircle(const Offset(100, 160), 80, paint);

    canvas.save();
    canvas.translate(190, 0);
    canvas.drawRect(rect, paintRect);
    canvas.drawCircle(const Offset(100, 160), 80, paint);

    // canvas.restore();
    // canvas.translate(0, 290);
    // canvas.drawRect(rect, paintRect);
    // canvas.drawCircle(const Offset(100, 160), 80, paint);
    //
    // canvas.save();
    // canvas.translate(190, 0);
    // canvas.drawRect(rect, paintRect);
    // canvas.drawCircle(const Offset(100, 160), 80, paint);

    var endRecording = p.endRecording();
    var f = Future.value(endRecording);
    return f;
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: ImagePainter(picture: picture),
      child: Container(
        color: Colors.yellow,
        child: Align(
          alignment: Alignment.bottomCenter,
          child: ElevatedButton(
            onPressed: () {
              var generatePicture = _generatePicture();
              generatePicture.then((value) => picture = value);

              setState(() {});
            },
            child: const Text("生成图片"),
          ),
        ),
      ),
    );
  }
}

class ImagePainter extends CustomPainter {
  Picture? picture;

  ImagePainter({required this.picture});

  @override
  void paint(Canvas canvas, Size size) {
    if (picture != null) {
      canvas.drawPicture(picture!);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

Giscus 未启用:请在 src/site.config.ts 中配置 repoId 与 categoryId。