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;
Offset? _previousOffset;
final List<Path?> _pathList = [];
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: (details) {
var position = details.localPosition;
_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;
},
);
}
}
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
..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;
}
}
然后使用CustomPaint,并在手势监听回调中使用setState刷新页面
GestureDetector(
onPanStart: (details) {
setState(() {
...
});
},
onPanUpdate: (details) {
setState(() {
...
});
},
onPanEnd: (details) {
setState(() {
...
});
},
child: CustomPaint(
size: Size.infinite,
painter: SignaturePainter(_pathList, _path),
),
),
效果图
3 保存签名为图片并去除多余空白
- 采用
PictureRecorder 将绘制的手写签名转换成为图片。 - 将所有画笔的路径 整体偏移往左上角偏移,保存时指定画布的大小,则可实现去除多余的空白。
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;
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder, bound);
for (Path? path in _pathList) {
if (path != null) {
var offsetPath = path.shift(Offset(20 - bound.left, 20 - bound.top));
canvas.drawPath(offsetPath, paint);
}
}
final picture = recorder.endRecording();
ui.Image img = await picture.toImage(
size.width.toInt() + 40, size.height.toInt() + 40);
var bytes = await img.toByteData(format: ui.ImageByteFormat.png);
return bytes?.buffer.asUint8List();
}
效果图
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;
Offset? _previousOffset;
final List<Path?> _pathList = [];
@override
void initState() {
super.initState();
widget.controller?._reset = () {
setState(() {
_pathList.clear();
});
};
widget.controller?._saveImage = () => _generateImage();
}
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;
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder, bound);
for (Path? path in _pathList) {
if (path != null) {
var offsetPath = path.shift(Offset(20 - bound.left, 20 - bound.top));
canvas.drawPath(offsetPath, paint);
}
}
final picture = recorder.endRecording();
ui.Image img = await picture.toImage(
size.width.toInt() + 40, size.height.toInt() + 40);
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()..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
..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;
}
}
|