• Flutter CustomPainter实现手写签名并保存为图片且去掉多余空白


    1 手写签名效果图

    在这里插入图片描述


    2 实现手写签名

    2.1 使用GestureDetector监听手势并记录

    Flutter提供的GestureDetector可以监听手势变化,我们可以将签名时的手势滑动路径记录下来,然后再绘制。

    • 手从开始移动到离开屏幕为 写了一笔画,由Path来记录笔画的路径。
    • List<Path> 来记录所有的笔画,凑成完整的签名。
    class HandwrittenSignatureWidget extends StatefulWidget {
      const HandwrittenSignatureWidget({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _HandwrittenSignatureWidgetState();
    }
    
    class _HandwrittenSignatureWidgetState
        extends State<HandwrittenSignatureWidget> {
      // 记录一次 不间断的手势路径 即记录一次 笔画
      Path? _path;
    
      // 在一次不间断的移动过程中 记录上一坐标点 用于在前后2点之间绘制贝塞尔曲线
      Offset? _previousOffset;
    
      // 记录所有的笔画 可拼凑成完整字
      final List<Path?> _pathList = [];
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          //手解除到屏幕,在开始移动了
          onPanStart: (details) {
            // 获取当前的坐标
            var position = details.localPosition;
            // 每次重新开始时就是新的Path对象
            _path = Path()..moveTo(position.dx, position.dy);
            _previousOffset = position;
          },
          //持续移动中
          onPanUpdate: (details) {
            // 获取当前的坐标
            var position = details.localPosition;
            var dx = position.dx;
            var dy = position.dy;
    
            final previousOffset = _previousOffset;
            //如果没有上一坐标点
            if (previousOffset == null) {
              _path?.lineTo(dx, dy);
            } else {
              var previousDx = previousOffset.dx;
              var previousDy = previousOffset.dy;
              // 已贝塞尔曲线的方式连线
              _path?.quadraticBezierTo(
                previousDx,
                previousDy,
                (previousDx + dx) / 2,
                (previousDy + dy) / 2,
              );
            }
            _previousOffset = position;
          },
          // 手停止了移动 离开了屏幕
          onPanEnd: (details) {
            // 记录笔画,然后清空临时变量
            _pathList.add(_path);
            _previousOffset = null;
            _path = null;
          },
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    2.2 使用CustomPainter绘制手写签名

    绘制手写签名需要用到 CustomPaint 与 CustomPainter

    • CustomPaint 是一个Widget ,提供了绘制时所需的画布。
    • CustomPainter 一个用于实现CustomPaint绘制的接口,需要实现此接口来进行自定义绘制。

    首先自定义CustomPainter

    class SignaturePainter extends CustomPainter {
      // 绘制签名需要 历史的笔画
      final List<Path?> pathList;
    
      // 当前正在写 的笔画
      final Path? currentPath;
    
      SignaturePainter(this.pathList, this.currentPath);
    
      // 设置画笔的属性
      final _paint = Paint()
        ..color = Colors.black
        ..style = PaintingStyle.stroke
        ..strokeCap = StrokeCap.round // 线条两端的形状
        ..strokeJoin = StrokeJoin.round // 2条线段连接处的形状
        ..strokeWidth = 2 //线宽
        ..isAntiAlias = true; //抗锯齿
    
      @override
      void paint(Canvas canvas, Size size) {
        // 绘制以前写过的笔画
        for (Path? path in pathList) {
          _drawLine(canvas, path);
        }
        // 绘制当前正在写的笔画
        _drawLine(canvas, currentPath);
      }
    
      void _drawLine(Canvas canvas, Path? path) {
        if (path == null) return;
        canvas.drawPath(path, _paint);
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) {
        return true;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    然后使用CustomPaint,并在手势监听回调中使用setState刷新页面

    GestureDetector(
      onPanStart: (details) {
        setState(() {
          ...
        });
      },
      //持续移动中
      onPanUpdate: (details) {
        setState(() {
          ...
        });
      },
      onPanEnd: (details) {
        setState(() {
          ...
        });
      },
      child: CustomPaint(
        size: Size.infinite,
        painter: SignaturePainter(_pathList, _path),
      ),
    ),
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    效果图
    在这里插入图片描述


    3 保存签名为图片并去除多余空白

    • 采用PictureRecorder将绘制的手写签名转换成为图片。
    • 将所有画笔的路径 整体偏移往左上角偏移,保存时指定画布的大小,则可实现去除多余的空白。
      /// 生成Padding为20的 只包含绘制区域的图片
      /// import 'dart:ui' as ui;
      Future<Uint8List?> _generateImage() async {
        var paint = Paint()
          ..color = Colors.black
          ..style = PaintingStyle.stroke
          ..strokeCap = StrokeCap.round
          ..strokeJoin = StrokeJoin.round
          ..strokeWidth = 2
          ..isAntiAlias = true;
    
        // 计算所有笔画 涉及的最大区域
        Rect? bound;
        for (Path? path in _pathList) {
          if (path != null) {
            var rect = path.getBounds();
            if (bound == null) {
              bound = rect;
            } else {
              bound = bound.expandToInclude(rect);
            }
          }
        }
        if (bound == null) {
          return null;
        }
    
        final size = bound.size;
        // PictureRecorder 记录画布上产生的行为
        final recorder = ui.PictureRecorder();
        final canvas = Canvas(recorder, bound);
    
        for (Path? path in _pathList) {
          if (path != null) {
            // padding 是20 ,然后将路径偏移至左上角(20,20)
            var offsetPath = path.shift(Offset(20 - bound.left, 20 - bound.top));
            canvas.drawPath(offsetPath, paint);
          }
        }
    
        // 结束记录
        final picture = recorder.endRecording();
        // 因为padding为20,所以整体宽高要+40
        ui.Image img = await picture.toImage(
            size.width.toInt() + 40, size.height.toInt() + 40);
        // 转换为png格式的图片数据
        var bytes = await img.toByteData(format: ui.ImageByteFormat.png);
        return bytes?.buffer.asUint8List();
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    效果图
    在这里插入图片描述

    4 完整的示例代码

    class DemoPage extends StatefulWidget {
      const DemoPage({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _DemoPageState();
    }
    
    class _DemoPageState extends State<DemoPage> {
      final _controller = HandwrittenSignatureController();
      Uint8List? _savedImage;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Column(
            children: [
              Expanded(
                  child: HandwrittenSignatureWidget(
                controller: _controller,
              )),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  TextButton(
                    onPressed: () {
                      _controller.saveImage().then((value) => setState(() {
                            _savedImage = value;
                          }));
                    },
                    child: const Text(
                      '保存签名',
                      style: TextStyle(color: Colors.black, fontSize: 20),
                    ),
                  ),
                  TextButton(
                    onPressed: () {
                      _controller.reset();
                      setState(() {
                        _savedImage = null;
                      });
                    },
                    child: const Text(
                      '清空签名',
                      style: TextStyle(color: Colors.black, fontSize: 20),
                    ),
                  ),
                ],
              ),
              Expanded(
                child: _savedImage == null
                    ? const SizedBox()
                    : Align(
                        alignment: Alignment.topLeft,
                        child: Container(
                          margin: const EdgeInsets.all(10),
                          decoration: BoxDecoration(
                            border: Border.all(color: Colors.blue),
                          ),
                          child: Image.memory(
                            _savedImage!,
                            filterQuality: FilterQuality.high,
                          ),
                        ),
                      ),
              ),
            ],
          ),
        );
      }
    }
    
    class HandwrittenSignatureController {
      Function? _reset;
      Future<Uint8List?> Function()? _saveImage;
    
      void reset() {
        _reset?.call();
      }
    
      Future<Uint8List?> saveImage() {
        return _saveImage?.call() ?? Future.value(null);
      }
    }
    
    class HandwrittenSignatureWidget extends StatefulWidget {
      final HandwrittenSignatureController? controller;
    
      const HandwrittenSignatureWidget({Key? key, this.controller})
          : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _HandwrittenSignatureWidgetState();
    }
    
    class _HandwrittenSignatureWidgetState
        extends State<HandwrittenSignatureWidget> {
      // 记录一次 不间断的手势路径 即记录一次 笔画
      Path? _path;
    
      // 在一次不间断的移动过程中 记录上一坐标点 用于在前后2点之间绘制贝塞尔曲线
      Offset? _previousOffset;
    
      // 记录所有的笔画 可拼凑成完整字
      final List<Path?> _pathList = [];
    
      @override
      void initState() {
        super.initState();
        widget.controller?._reset = () {
          setState(() {
            _pathList.clear();
          });
        };
        widget.controller?._saveImage = () => _generateImage();
      }
    
      /// 生成Padding为20的 只包含绘制区域的图片
      /// import 'dart:ui' as ui;
      Future<Uint8List?> _generateImage() async {
        var paint = Paint()
          ..color = Colors.black
          ..style = PaintingStyle.stroke
          ..strokeCap = StrokeCap.round
          ..strokeJoin = StrokeJoin.round
          ..strokeWidth = 2
          ..isAntiAlias = true;
    
        // 计算所有笔画 涉及的最大区域
        Rect? bound;
        for (Path? path in _pathList) {
          if (path != null) {
            var rect = path.getBounds();
            if (bound == null) {
              bound = rect;
            } else {
              bound = bound.expandToInclude(rect);
            }
          }
        }
        if (bound == null) {
          return null;
        }
    
        final size = bound.size;
        // PictureRecorder 记录画布上产生的行为
        final recorder = ui.PictureRecorder();
        final canvas = Canvas(recorder, bound);
    
        for (Path? path in _pathList) {
          if (path != null) {
            // padding 是20 ,然后将路径偏移至左上角(20,20)
            var offsetPath = path.shift(Offset(20 - bound.left, 20 - bound.top));
            canvas.drawPath(offsetPath, paint);
          }
        }
    
        // 结束记录
        final picture = recorder.endRecording();
        // 因为padding为20,所以整体宽高要+40
        ui.Image img = await picture.toImage(
            size.width.toInt() + 40, size.height.toInt() + 40);
        // 转换为png格式的图片数据
        var bytes = await img.toByteData(format: ui.ImageByteFormat.png);
        return bytes?.buffer.asUint8List();
      }
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          //手解除到屏幕,在开始移动了
          onPanStart: (details) {
            // 获取当前的坐标
            var position = details.localPosition;
            setState(() {
              // 每次重新开始时就是新的Path对象
              _path = Path()..moveTo(position.dx, position.dy);
              _previousOffset = position;
            });
          },
          //持续移动中
          onPanUpdate: (details) {
            // 获取当前的坐标
            var position = details.localPosition;
            var dx = position.dx;
            var dy = position.dy;
    
            setState(() {
              final previousOffset = _previousOffset;
              //如果没有上一坐标点
              if (previousOffset == null) {
                _path?.lineTo(dx, dy);
              } else {
                var previousDx = previousOffset.dx;
                var previousDy = previousOffset.dy;
                // 已贝塞尔曲线的方式连线
                _path?.quadraticBezierTo(
                  previousDx,
                  previousDy,
                  (previousDx + dx) / 2,
                  (previousDy + dy) / 2,
                );
              }
              _previousOffset = position;
            });
          },
          // 手停止了移动 离开了屏幕
          onPanEnd: (details) {
            // 记录笔画,然后清空临时变量
            setState(() {
              _pathList.add(_path);
              _previousOffset = null;
              _path = null;
            });
          },
          child: CustomPaint(
            size: Size.infinite,
            painter: SignaturePainter(_pathList, _path),
          ),
        );
      }
    }
    
    class SignaturePainter extends CustomPainter {
      // 绘制签名需要 历史的笔画
      final List<Path?> pathList;
    
      // 当前正在写 的笔画
      final Path? currentPath;
    
      SignaturePainter(this.pathList, this.currentPath);
    
      // 设置画笔的属性
      final _paint = Paint()
        ..color = Colors.black
        ..style = PaintingStyle.stroke
        ..strokeCap = StrokeCap.round // 线条两端的形状
        ..strokeJoin = StrokeJoin.round // 2条线段连接处的形状
        ..strokeWidth = 2 //线宽
        ..isAntiAlias = true; //抗锯齿
    
      @override
      void paint(Canvas canvas, Size size) {
        // 绘制以前写过的笔画
        for (Path? path in pathList) {
          _drawLine(canvas, path);
        }
        // 绘制当前正在写的笔画
        _drawLine(canvas, currentPath);
      }
    
      void _drawLine(Canvas canvas, Path? path) {
        if (path == null) return;
        canvas.drawPath(path, _paint);
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) {
        return true;
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
  • 相关阅读:
    【C++】map、set,multiset和multimap的使用及底层原理【完整版】
    探索低代码开发:编程新时代的开启
    生产者消费者问题实践
    卷积计算公式 神经网络,卷积神经网络应用举例
    【pytest官方文档】解读- 如何安装和使用插件
    多态原理、虚表指针与虚基表的内存布局。
    Linux 常用文件和目录操作 (cat less head tail等)
    Mysql忘记登入密码找回 方法(超详细)
    log4j2.xml 获取当前系统属性
    苹果上架Guideline 4.3 - Design
  • 原文地址:https://blog.csdn.net/ww897532167/article/details/125537438