字节笔记本
2026年2月21日
使用 Flutter 构建绘图应用:CustomPaint 与 Canvas 完全指南
本文将介绍如何使用 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
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow[100],
child: CustomPaint(
painter: MyCustomPainter(),
),
);
}
}CustomPainter 需要实现两个方法:
- paint(): 包含所有绘制逻辑,接收 Canvas 和 Size 参数
- shouldRepaint(): 优化方法,决定是否需要重绘
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)
绘制简单线条
@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() 等方法。
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);
}设置颜色和填充
Paint paintMountains = Paint()
..style = PaintingStyle.fill
..color = Colors.brown;
Paint paintSun = Paint()
..style = PaintingStyle.fill
..color = Colors.deepOrangeAccent;实现绘图功能
使用 GestureDetector 检测手势
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]),
),
),
),
);
}处理手势事件
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 对象:
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():
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),
);
},
),
),
);
}添加颜色工具栏
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),
],
),
);
}添加粗细工具栏
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 插件保存图片:
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);
}
}清空画布
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