Flutter如何用CustomPaint按绘制速度实现平滑签名绘制

实现方案
你原有代码用固定线宽+drawPoints画折线的方式没法模拟手写笔锋,要实现自然流畅的签名效果,核心要做3件事:用贝塞尔曲线做线段平滑、根据触摸速度/压力动态调整线宽、维护独立的笔触点位数据方便自定义扩展。
核心逻辑说明
- 平滑处理:相邻点位之间用二次贝塞尔曲线连接,完全消除折线棱角
- 动态线宽规则:支持压感的设备优先读取触摸事件的
pressure值映射线宽,无压感设备通过「两点间距/两点时间差」计算移动速度,移动速度越快线条越细,速度越慢线条越粗,完全贴合真实书写的笔锋效果 - 点位过滤:过滤距离过近的重复采样点,减少冗余绘制计算
- 笔触配置:保留圆头线帽、圆角连接配置,避免线段衔接处出现断口
完整实现示例
import 'package:flutter/material.dart'; // 自定义笔触点位,存储坐标、压感、时间戳 class StrokePoint { final Offset position; final double pressure; final int timestamp; StrokePoint({ required this.position, required this.pressure, required this.timestamp, }); } // 单条笔画数据结构 class Stroke { final List<StrokePoint> points = []; final Color color; // 可自定义的线宽范围 final double minWidth; final double maxWidth; Stroke({ required this.color, this.minWidth = 1.0, this.maxWidth = 7.0, }); } class SignaturePainter extends CustomPainter { final List<Stroke> strokes; SignaturePainter(this.strokes); @override void paint(Canvas canvas, Size size) { for (final stroke in strokes) { if (stroke.points.length < 2) continue; final paint = Paint() ..color = stroke.color ..isAntiAlias = true ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round; // 遍历点位逐段绘制,动态设置每段线宽 for (int i = 1; i < stroke.points.length; i++) { final prev = stroke.points[i-1]; final curr = stroke.points[i]; // 计算当前段的线宽 double strokeWidth; // 优先用压感值计算 if (curr.pressure > 0 && curr.pressure <= 1) { strokeWidth = stroke.minWidth + (stroke.maxWidth - stroke.minWidth) * curr.pressure; } else { // 无压感设备用移动速度计算:速度越快线越细 final distance = (curr.position - prev.position).distance; final timeDelta = (curr.timestamp - prev.timestamp).abs(); final speed = timeDelta == 0 ? 0 : distance / timeDelta; // 0.3为灵敏度系数,数值越大笔锋变化越明显 final widthFactor = 1 / (1 + speed * 0.3); strokeWidth = stroke.minWidth + (stroke.maxWidth - stroke.minWidth) * widthFactor; } paint.strokeWidth = strokeWidth; // 取两点中点作为贝塞尔曲线控制点,实现平滑衔接 final midPoint = Offset( (prev.position.dx + curr.position.dx) / 2, (prev.position.dy + curr.position.dy) / 2, ); if (i == 1) { canvas.drawLine(prev.position, midPoint, paint); } else { final prevMid = Offset( (stroke.points[i-2].position.dx + prev.position.dx)/2, (stroke.points[i-2].position.dy + prev.position.dy)/2, ); canvas.drawPath( Path()..quadraticBezierTo(prev.position.dx, prev.position.dy, midPoint.dx, midPoint.dy), paint ); } // 最后一段直接连接到终点 if (i == stroke.points.length - 1) { canvas.drawLine(midPoint, curr.position, paint); } } } } @override bool shouldRepaint(covariant SignaturePainter oldDelegate) => true; } // 可直接复用的签名画布组件 class SignatureCanvas extends StatefulWidget { const SignatureCanvas({super.key}); @override State<SignatureCanvas> createState() => _SignatureCanvasState(); } class _SignatureCanvasState extends State<SignatureCanvas> { final List<Stroke> _strokes = []; Stroke? _currentStroke; final Color _penColor = Colors.black; @override Widget build(BuildContext context) { return GestureDetector( onPanStart: (details) { setState(() { _currentStroke = Stroke(color: _penColor); _currentStroke!.points.add(StrokePoint( position: details.localPosition, pressure: details.pressure, timestamp: DateTime.now().millisecondsSinceEpoch, )); _strokes.add(_currentStroke!); }); }, onPanUpdate: (details) { if (_currentStroke == null) return; setState(() { final newPoint = StrokePoint( position: details.localPosition, pressure: details.pressure, timestamp: DateTime.now().millisecondsSinceEpoch, ); // 过滤距离小于1.5像素的过近点位,减少冗余计算 final lastPoint = _currentStroke!.points.last; if ((newPoint.position - lastPoint.position).distance < 1.5) return; _currentStroke!.points.add(newPoint); }); }, onPanEnd: (details) { _currentStroke = null; }, child: CustomPaint( painter: SignaturePainter(_strokes), size: Size.infinite, ), ); } } // 调用示例 void main() { runApp(MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('手写签名')), body: const SignatureCanvas(), ), )); }
自定义扩展方向
- 调整
minWidth/maxWidth参数、速度映射系数,可以模拟钢笔、毛笔等不同笔触的书写效果 - 给
Paint添加shader、blendMode属性,可以实现马克笔、荧光笔的半透明叠加效果 - 所有笔画点位都独立存储在
Stroke列表中,可以很方便的扩展撤销、重做、橡皮擦、导出为图片等功能 - 带触控笔的设备、支持3D Touch的iOS设备可以直接读取
pressure值,实现更精准的压感笔锋效果
内容的提问来源于stack exchange,提问作者Kevin Yang
相关产品推荐
相关产品推荐

