ByteNoteByteNote

字节笔记本

2026年2月21日

使用 Flutter 构建绘图应用:CustomPaint 与 Canvas 完全指南

API中转
¥120

本文将介绍如何使用 Flutter 的 CustomPaint 和 Canvas 创建一个功能完整的绘图应用。你将学习如何检测用户手势、绘制路径、管理画笔颜色和粗细,以及保存作品到相册。

准备工作

首先下载起始项目,使用 Android Studio 4.1+ 或 VS Code 打开。运行 flutter create . 生成平台文件夹,然后执行 pub get 获取依赖。

项目结构包含以下关键文件:

  • main.dart: 应用入口,包含 MaterialApp 和 DrawingPage
  • drawing_page.dart: 主页面,包含绘图区域、工具栏和按钮
  • drawn_line.dart: 路径数据模型,包含点列表、颜色和粗细
  • sketcher.dart: 自定义绘制器,继承 CustomPainter 实现绘制逻辑

Flutter Canvas 和 CustomPaint 简介

Flutter 的 Canvas 是一个用于记录图形操作的接口,可用于绘制形状、图像、文本等。要创建和访问 Canvas,需要使用 CustomPaint 组件。

使用 CustomPaint Widget

dart
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow[100],
      child: CustomPaint(
        painter: MyCustomPainter(),
      ),
    );
  }
}

CustomPainter 需要实现两个方法:

  • paint(): 包含所有绘制逻辑,接收 Canvas 和 Size 参数
  • shouldRepaint(): 优化方法,决定是否需要重绘
dart
class MyCustomPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 绘制逻辑
  }

  @override
  bool shouldRepaint(MyCustomPainter delegate) {
    return true;
  }
}

Canvas 基础

Canvas 使用坐标系统定位屏幕上的点。以 iPhone X(375x812)为例:

  • 左上角: (0, 0)
  • 右上角: (375, 0)
  • 左下角: (0, 812)
  • 右下角: (375, 812)
  • 中心点: (187.5, 406)

绘制简单线条

dart
@override
void paint(Canvas canvas, Size size) {
  // 起点和终点
  Offset startPoint = Offset(0, 0);
  Offset endPoint = Offset(size.width, size.height);

  // 创建画笔
  Paint paint = Paint();

  // 绘制线条
  canvas.drawLine(startPoint, endPoint, paint);
}

绘制路径

Path 可以绘制任意形状,支持 lineTo()、moveTo()、addOval()、addArc() 等方法。

dart
void paint(Canvas canvas, Size size) {
  Paint paint = Paint()..style = PaintingStyle.stroke;

  Path path = Path();
  path.moveTo(0, 250);
  path.lineTo(100, 200);
  path.lineTo(150, 150);
  path.lineTo(200, 50);
  path.lineTo(250, 150);
  path.lineTo(300, 200);
  path.lineTo(size.width, 250);
  path.lineTo(0, 250);

  // 绘制圆形(太阳)
  path.moveTo(100, 100);
  path.addOval(Rect.fromCircle(center: Offset(100, 100), radius: 25));

  canvas.drawPath(path, paint);
}

设置颜色和填充

dart
Paint paintMountains = Paint()
  ..style = PaintingStyle.fill
  ..color = Colors.brown;

Paint paintSun = Paint()
  ..style = PaintingStyle.fill
  ..color = Colors.deepOrangeAccent;

实现绘图功能

使用 GestureDetector 检测手势

dart
GestureDetector buildCurrentPath(BuildContext context) {
  return GestureDetector(
    onPanStart: onPanStart,
    onPanUpdate: onPanUpdate,
    onPanEnd: onPanEnd,
    child: RepaintBoundary(
      child: Container(
        color: Colors.transparent,
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: CustomPaint(
          painter: Sketcher(lines: [line]),
        ),
      ),
    ),
  );
}

处理手势事件

dart
void onPanStart(DragStartDetails details) {
  final box = context.findRenderObject() as RenderBox;
  final point = box.globalToLocal(details.globalPosition);

  setState((){
    line = DrawnLine([point], selectedColor, selectedWidth);
  });
}

void onPanUpdate(DragUpdateDetails details) {
  final box = context.findRenderObject() as RenderBox;
  final point = box.globalToLocal(details.globalPosition);
  final path = List.from(line.path)..add(point);

  setState((){
    line = DrawnLine(path, selectedColor, selectedWidth);
  });
}

void onPanEnd(DragEndDetails details) {
  setState((){
    lines.add(line);
  });
}

绘制多条路径

为了支持多条独立的路径(每条有自己的颜色和粗细),需要将每条路径保存为单独的 DrawnLine 对象:

dart
void onPanStart(DragStartDetails details) {
  final box = context.findRenderObject() as RenderBox;
  final point = box.globalToLocal(details.globalPosition);

  setState(() {
    line = DrawnLine([point], selectedColor, selectedWidth);
  });
}

void onPanUpdate(DragUpdateDetails details) {
  final box = context.findRenderObject() as RenderBox;
  final point = box.globalToLocal(details.globalPosition);
  final path = List.from(line.path)..add(point);
  line = DrawnLine(path, selectedColor, selectedWidth);

  setState(() {
    if (lines.length == 0) {
      lines.add(line);
    } else {
      lines[lines.length - 1] = line;
    }
  });
}

void onPanEnd(DragEndDetails details) {
  setState(() {
    lines.add(line);
  });
}

优化性能

使用两个 CustomPaint 分别绘制当前路径和历史路径,配合 StreamBuilder 避免频繁调用 setState():

dart
final linesStreamController = StreamController<List<DrawnLine>>.broadcast();
final currentLineStreamController = StreamController<DrawnLine>.broadcast();

Widget buildAllPaths(BuildContext context) {
  return RepaintBoundary(
    child: Container(
      width: MediaQuery.of(context).size.width,
      height: MediaQuery.of(context).size.height,
      child: StreamBuilder<List<DrawnLine>>(
        stream: linesStreamController.stream,
        builder: (context, snapshot) {
          return CustomPaint(
            painter: Sketcher(lines: lines),
          );
        },
      ),
    ),
  );
}

添加颜色工具栏

dart
Widget buildColorToolbar() {
  return Positioned(
    top: 40.0,
    right: 10.0,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        buildColorButton(Colors.red),
        buildColorButton(Colors.blueAccent),
        buildColorButton(Colors.deepOrange),
        buildColorButton(Colors.green),
        buildColorButton(Colors.lightBlue),
        buildColorButton(Colors.black),
        buildColorButton(Colors.white),
      ],
    ),
  );
}

添加粗细工具栏

dart
Widget buildStrokeToolbar() {
  return Positioned(
    bottom: 100.0,
    right: 10.0,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        buildStrokeButton(5.0),
        buildStrokeButton(10.0),
        buildStrokeButton(15.0),
      ],
    ),
  );
}

保存绘图

使用 RepaintBoundary 和 image_gallery_saver 插件保存图片:

dart
Future<void> save() async {
  try {
    final boundary = _globalKey.currentContext.findRenderObject()
        as RenderRepaintBoundary;
    final image = await boundary.toImage();
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    final pngBytes = byteData.buffer.asUint8List();

    final saved = await ImageGallerySaver.saveImage(
      pngBytes,
      quality: 100,
      name: DateTime.now().toIso8601String() + ".png",
      isReturnImagePathOfIOS: true,
    );
  } catch (e) {
    print(e);
  }
}

清空画布

dart
Future<void> clear() async {
  setState(() {
    lines = [];
    line = null;
  });
}

扩展功能建议

完成基础绘图应用后,可以尝试添加以下功能:

  • 撤销/重做: 使用命令模式管理绘图操作历史
  • 颜色选择器: 使用 flutter_colorpicker 插件提供完整色盘
  • 自定义粗细: 添加滑块控件精确控制画笔粗细
  • 橡皮擦: 通过混合模式或路径裁剪实现擦除效果
  • 图层系统: 支持多图层管理和混合

总结

通过本教程,你学习了:

  • 使用 CustomPaint 和 CustomPainter 进行自定义绘制
  • 理解 Canvas 坐标系统和基本绘制操作
  • 使用 GestureDetector 捕获用户手势
  • 管理多条路径和独立样式
  • 使用 StreamBuilder 优化性能
  • 保存绘制内容为图片

Flutter 的绘制 API 非常强大,配合 CustomPaint 可以实现任何复杂的自定义 UI 效果。


原文链接: https://www.kodeco.com/25237210-building-a-drawing-app-in-flutter 作者: Samarth Agarwal 发布时间: Aug 30, 2021

分享: