Flutter 框架跨平台鸿蒙开发 - 手写签名生成器:打造专业电子签名工具
手写签名生成器是一款支持手写绘制、保存和管理电子签名的应用。通过触摸屏幕绘制签名,支持多种画笔颜色和粗细调节,可以保存签名图片并随时查看和管理,适用于电子文档签署、合同签名等场景。运行效果图手写签名生成器签名绘制页已保存签名签名详情绘制区域工具栏设置手写输入网格辅助颜色选择粗细调节撤销清除保存网格开关历史记录网格展示签名预览时间信息大图查看删除签名使用CustomPainter实现流畅的手写效果。
Flutter手写签名生成器:打造专业电子签名工具
项目简介
手写签名生成器是一款支持手写绘制、保存和管理电子签名的应用。通过触摸屏幕绘制签名,支持多种画笔颜色和粗细调节,可以保存签名图片并随时查看和管理,适用于电子文档签署、合同签名等场景。
运行效果图



核心功能
- 手写绘制:流畅的手写签名体验
- 画笔设置:多种颜色和粗细选择
- 撤销功能:支持撤销上一笔
- 网格辅助:可选网格线辅助对齐
- 签名保存:保存为PNG图片
- 签名管理:查看和删除已保存签名
应用特色
| 特色 | 说明 |
|---|---|
| 流畅绘制 | 基于CustomPainter实现流畅手写 |
| 高清导出 | 3倍像素比例高清图片 |
| 多样化设置 | 8种颜色、10级粗细 |
| 网格辅助 | 可选网格线辅助书写 |
| 便捷管理 | 网格展示、快速查看 |
功能架构
核心功能详解
1. 手写绘制实现
使用CustomPainter实现流畅的手写效果。
绘制控制器:
class SignaturePainterController extends ChangeNotifier {
final List<List<DrawPoint>> _strokes = [];
final List<List<DrawPoint>> _undoneStrokes = [];
bool get isEmpty => _strokes.isEmpty;
bool get canUndo => _strokes.isNotEmpty;
void addPoint(Offset point, Color color, double width) {
if (_strokes.isEmpty || _strokes.last.isEmpty) {
_strokes.add([]);
}
_strokes.last.add(DrawPoint(point, color, width));
_undoneStrokes.clear();
notifyListeners();
}
void endStroke() {
if (_strokes.isNotEmpty && _strokes.last.isNotEmpty) {
_strokes.add([]);
}
}
void undo() {
if (_strokes.isNotEmpty) {
final lastStroke = _strokes.removeLast();
if (lastStroke.isNotEmpty) {
_undoneStrokes.add(lastStroke);
}
notifyListeners();
}
}
void clear() {
_strokes.clear();
_undoneStrokes.clear();
notifyListeners();
}
List<List<DrawPoint>> get strokes => _strokes;
}
数据结构:
_strokes:所有笔画的列表- 每个笔画是一个
DrawPoint列表 DrawPoint包含位置、颜色、粗细信息
手势处理:
GestureDetector(
onPanStart: (details) {
_controller.addPoint(
details.localPosition,
_penColor,
_penWidth,
);
},
onPanUpdate: (details) {
_controller.addPoint(
details.localPosition,
_penColor,
_penWidth,
);
},
onPanEnd: (details) {
_controller.endStroke();
},
child: CustomPaint(
size: Size.infinite,
painter: SignaturePainter(_controller),
),
)
2. 签名绘制
使用CustomPainter绘制签名路径。
绘制实现:
class SignaturePainter extends CustomPainter {
final SignaturePainterController controller;
SignaturePainter(this.controller) : super(repaint: controller);
void paint(Canvas canvas, Size size) {
for (final stroke in controller.strokes) {
if (stroke.isEmpty) continue;
// 绘制连续的线段
for (int i = 0; i < stroke.length - 1; i++) {
final p1 = stroke[i];
final p2 = stroke[i + 1];
final paint = Paint()
..color = p1.color
..strokeWidth = p1.width
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
canvas.drawLine(p1.offset, p2.offset, paint);
}
// 单点绘制为圆点
if (stroke.length == 1) {
final p = stroke[0];
final paint = Paint()
..color = p.color
..strokeWidth = p.width
..strokeCap = StrokeCap.round;
canvas.drawCircle(p.offset, p.width / 2, paint);
}
}
}
bool shouldRepaint(SignaturePainter oldDelegate) => true;
}
绘制特点:
- 使用
drawLine连接相邻点 - 圆角端点和连接点
- 单点绘制为圆形
- 支持不同颜色和粗细
3. 画笔设置
支持颜色和粗细调节。
颜色选择器:
void _showColorPicker() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('选择画笔颜色'),
content: Wrap(
spacing: 12,
runSpacing: 12,
children: [
Colors.black,
Colors.blue,
Colors.red,
Colors.green,
Colors.purple,
Colors.orange,
Colors.brown,
Colors.pink,
].map((color) {
return GestureDetector(
onTap: () {
setState(() => _penColor = color);
Navigator.pop(context);
},
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: _penColor == color
? Colors.white
: Colors.grey,
width: _penColor == color ? 3 : 1,
),
),
),
);
}).toList(),
),
),
);
}
粗细调节器:
void _showPenWidthDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('画笔粗细'),
content: StatefulBuilder(
builder: (context, setState) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('${_penWidth.toStringAsFixed(1)} px'),
Slider(
value: _penWidth,
min: 1.0,
max: 10.0,
divisions: 18,
onChanged: (value) {
setState(() => _penWidth = value);
this.setState(() {});
},
),
const SizedBox(height: 16),
CustomPaint(
size: const Size(200, 50),
painter: PreviewPainter(_penColor, _penWidth),
okeCap.round;
canvas.drawLine(
Offset(0, size.height / 2),
Offset(size.width, size.height / 2),
paint,
);
}
bool shouldRepaint(PreviewPainter oldDelegate) =>
color != oldDelegate.color || width != oldDelegate.width;
}
4. 网格辅助
可选的网格线辅助对齐。
网格绘制:
class GridPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.grey.withValues(alpha: 0.2)
..strokeWidth = 1;
const spacing = 30.0;
// 绘制垂直线
for (double x = 0; x < size.width; x += spacing) {
canvas.drawLine(
Offset(x, 0),
Offset(x, size.height),
paint
);
}
// 绘制水平线
for (double y = 0; y < size.height; y += spacing) {
canvas.drawLine(
Offset(0, y),
Offset(size.width, y),
paint
);
}
}
bool shouldRepaint(GridPainter oldDelegate) => false;
}
网格使用:
Stack(
children: [
if (_showGrid)
CustomPaint(
size: Size.infinite,
painter: GridPainter(),
),
GestureDetector(
// 手写输入
child: CustomPaint(
size: Size.infinite,
painter: SignaturePainter(_controller),
),
),
],
)
5. 签名保存
将签名转换为PNG图片保存。
截图实现:
Future<Uint8List?> _captureSignature() async {
try {
final boundary = _signatureKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary == null) return null;
// 生成高清图片(3倍像素比例)
final image = await boundary.toImage(pixelRatio: 3.0);
final byteData = await image.toByteData(
format: ui.ImageByteFormat.png
);
return byteData?.buffer.asUint8List();
} catch (e) {
return null;
}
}
保存流程:
Future<void> _saveSignature() async {
if (_controller.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请先绘制签名')),
);
}
return;
}
final imageData = await _captureSignature();
if (imageData != null && mounted) {
final name = await _showNameDialog();
if (name != null && name.isNotEmpty && mounted) {
setState(() {
_savedSignatures.insert(
0,
SignatureData(
name: name,
imageData: imageData,
createTime: DateTime.now(),
),
);
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('签名已保存')),
);
}
}
}
}
关键技术:
RepaintBoundary:标记需要截图的区域RenderRepaintBoundary.toImage():生成图片pixelRatio: 3.0:3倍像素比例,确保高清ImageByteFormat.png:PNG格式
6. 签名管理
网格展示和详情查看。
网格展示:
GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.2,
),
itemCount: signatures.length,
itemBuilder: (context, index) {
final signature = signatures[index];
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SignatureDetailPage(
signature: signature,
onDelete: () {
onDelete(index);
Navigator.pop(context);
},
),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Container(
color: Colors.white,
padding: const EdgeInsets.all(8),
child: Image.memory(
signature.imageData,
fit: BoxFit.contain,
),
),
),
Container(
padding: const EdgeInsets.all(8),
color: Colors.grey[100],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
signature.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
_formatTime(signature.createTime),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
],
),
),
);
},
)
界面设计要点
1. 绘制区域
白色背景、圆角边框、阴影效果:
Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: RepaintBoundary(
key: _signatureKey,
child: // 绘制内容
),
),
)
2. 工具按钮
图标+文字的垂直布局:
Widget _buildToolButton({
required IconData icon,
required String label,
VoidCallback? onPressed,
Color? color,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(icon, color: color),
onPressed: onPressed,
style: IconButton.styleFrom(
backgroundColor: onPressed == null
? Colors.grey[200]
: Colors.blue.withValues(alpha: 0.1),
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: onPressed == null ? Colors.grey : Colors.black87,
),
),
],
);
}
3. 颜色方案
| 用途 | 颜色 | 说明 |
|---|---|---|
| 主色调 | Blue | 清新、专业 |
| 背景 | White | 纯净、清晰 |
| 边框 | Grey[300] | 柔和分隔 |
| 禁用 | Grey[200] | 不可用状态 |
| 阴影 | Black(0.1) | 轻微立体感 |
数据模型设计
签名数据模型
class SignatureData {
final String name; // 签名名称
final Uint8List imageData; // PNG图片数据
final DateTime createTime; // 创建时间
SignatureData({
required this.name,
required this.imageData,
required this.createTime,
});
}
绘制点模型
class DrawPoint {
final Offset offset; // 位置
final Color color; // 颜色
final double width; // 粗细
DrawPoint(this.offset, this.color, this.width);
}
核心技术要点
1. RepaintBoundary
用于标记需要截图的区域:
RepaintBoundary(
key: _signatureKey,
child: // 需要截图的内容
)
2. CustomPainter
自定义绘制:
class SignaturePainter extends CustomPainter {
final SignaturePainterController controller;
SignaturePainter(this.controller) : super(repaint: controller);
void paint(Canvas canvas, Size size) {
// 绘制逻辑
}
bool shouldRepaint(SignaturePainter oldDelegate) => true;
}
3. ChangeNotifier
状态管理:
class SignaturePainterController extends ChangeNotifier {
void addPoint(Offset point, Color color, double width) {
// 添加点
notifyListeners(); // 通知更新
}
}
4. GestureDetector
手势识别:
GestureDetector(
onPanStart: (details) {
// 开始绘制
},
onPanUpdate: (details) {
// 持续绘制
},
onPanEnd: (details) {
// 结束绘制
},
child: // 绘制区域
)
功能扩展建议
1. 数据持久化
使用SharedPreferences保存签名:
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
class SignatureStorage {
Future<void> saveSignatures(List<SignatureData> signatures) async {
final prefs = await SharedPreferences.getInstance();
final jsonList = signatures.map((s) => {
'name': s.name,
'imageData': base64Encode(s.imageData),
'createTime': s.createTime.toIso8601String(),
}).toList();
await prefs.setString('signatures', jsonEncode(jsonList));
}
Future<List<SignatureData>> loadSignatures() async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString('signatures');
if (jsonStr == null) return [];
final jsonList = jsonDecode(jsonStr) as List;
return jsonList.map((json) => SignatureData(
name: json['name'],
imageData: base64Decode(json['imageData']),
createTime: DateTime.parse(json['createTime']),
)).toList();
}
}
2. 分享功能
使用share_plus分享签名:
import 'package:share_plus/share_plus.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
Future<void> shareSignature(SignatureData signature) async {
final directory = await getTemporaryDirectory();
final file = File('${directory.path}/${signature.name}.png');
await file.writeAsBytes(signature.imageData);
await Share.shareXFiles(
[XFile(file.path)],
text: '我的签名:${signature.name}',
);
}
3. 导出PDF
使用pdf包生成PDF文档:
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
Future<void> exportToPDF(SignatureData signature) async {
final pdf = pw.Document();
final image = pw.MemoryImage(signature.imageData);
pdf.addPage(
pw.Page(
build: (context) => pw.Center(
child: pw.Column(
mainAxisAlignment: pw.MainAxisAlignment.center,
children: [
pw.Text(
signature.name,
style: pw.TextStyle(fontSize: 24),
),
pw.SizedBox(height: 20),
pw.Image(image, width: 300),
pw.SizedBox(height: 20),
pw.Text(
'签署时间:${signature.createTime}',
style: pw.TextStyle(fontSize: 12),
),
],
),
),
),
);
await Printing.layoutPdf(
onLayout: (format) async => pdf.save(),
);
}
4. 背景图片
支持在背景图片上签名:
class SignatureWithBackground extends StatefulWidget {
final Uint8List? backgroundImage;
Widget build(BuildContext context) {
return Stack(
children: [
if (backgroundImage != null)
Image.memory(
backgroundImage,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
),
CustomPaint(
size: Size.infinite,
painter: SignaturePainter(_controller),
),
],
);
}
}
5. 签名模板
预设签名样式:
class SignatureTemplate {
final String name;
final TextStyle textStyle;
final Color backgroundColor;
SignatureTemplate({
required this.name,
required this.textStyle,
required this.backgroundColor,
});
static final templates = [
SignatureTemplate(
name: '正式',
textStyle: TextStyle(
fontFamily: 'Serif',
fontSize: 32,
fontWeight: FontWeight.bold,
),
backgroundColor: Colors.white,
),
SignatureTemplate(
name: '艺术',
textStyle: TextStyle(
fontFamily: 'Cursive',
fontSize: 36,
fontStyle: FontStyle.italic,
),
backgroundColor: Colors.grey[100]!,
),
];
}
6. 签名识别
使用ML Kit识别手写文字:
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
class SignatureRecognition {
final textRecognizer = TextRecognizer();
Future<String?> recognizeText(Uint8List imageData) async {
final inputImage = InputImage.fromBytes(
bytes: imageData,
metadata: InputImageMetadata(
size: Size(800, 600),
rotation: InputImageRotation.rotation0deg,
format: InputImageFormat.nv21,
bytesPerRow: 800,
),
);
final recognizedText = await textRecognizer.processImage(inputImage);
return recognizedText.text;
}
void dispose() {
textRecognizer.close();
}
}
7. 签名水印
添加时间戳水印:
class WatermarkPainter extends CustomPainter {
final String text;
WatermarkPainter(this.text);
void paint(Canvas canvas, Size size) {
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
color: Colors.grey.withValues(alpha: 0.3),
fontSize: 12,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
size.width - textPainter.width - 10,
size.height - textPainter.height - 10,
),
);
}
bool shouldRepaint(WatermarkPainter oldDelegate) => false;
}
8. 签名加密
使用加密保护签名数据:
import 'package:encrypt/encrypt.dart';
class SignatureEncryption {
final key = Key.fromLength(32);
final iv = IV.fromLength(16);
String encryptSignature(Uint8List imageData) {
final encrypter = Encrypter(AES(key));
final encrypted = encrypter.encryptBytes(
imageData,
iv: iv,
);
return encrypted.base64;
}
Uint8List decryptSignature(String encryptedData) {
final encrypter = Encrypter(AES(key));
final decrypted = encrypter.decryptBytes(
Encrypted.fromBase64(encryptedData),
iv: iv,
);
return Uint8List.fromList(decrypted);
}
}
项目结构
lib/
├── main.dart # 应用入口
├── models/ # 数据模型
│ ├── signature_data.dart # 签名数据
│ ├── draw_point.dart # 绘制点
│ └── signature_template.dart # 签名模板
├── pages/ # 页面
│ ├── signature_page.dart # 主绘制页面
│ ├── saved_signatures_page.dart # 已保存签名
│ └── signature_detail_page.dart # 签名详情
├── widgets/ # 组件
│ ├── signature_painter.dart # 签名画笔
│ ├── grid_painter.dart # 网格绘制
│ ├── preview_painter.dart # 预览画笔
│ └── tool_button.dart # 工具按钮
├── controllers/ # 控制器
│ └── signature_controller.dart # 签名控制器
└── services/ # 服务
├── signature_storage.dart # 数据存储
├── signature_export.dart # 导出服务
└── signature_encryption.dart # 加密服务
使用指南
基本操作
-
绘制签名
- 在白色区域用手指绘制
- 支持多笔画连续绘制
-
调整画笔
- 点击"颜色"选择画笔颜色
- 点击"粗细"调节画笔粗细
-
撤销操作
- 点击"撤销"删除上一笔
- 点击"清除"清空所有内容
-
保存签名
- 点击"保存签名"按钮
- 输入签名名称
- 确认保存
-
查看签名
- 点击右上角历史图标
- 网格展示所有签名
- 点击查看详情
高级技巧
-
使用网格
- 开启网格辅助对齐
- 适合规整的签名
-
多次尝试
- 使用撤销功能修正
- 清除后重新绘制
-
选择合适粗细
- 细笔适合详细签名
- 粗笔适合简洁签名
常见问题
Q1: 如何提高签名清晰度?
签名以3倍像素比例保存,已经很清晰。如需更高清晰度:
final image = await boundary.toImage(pixelRatio: 5.0);
Q2: 如何导出签名图片?
使用share_plus或image_gallery_saver:
import 'package:image_gallery_saver/image_gallery_saver.dart';
Future<void> saveToGallery(Uint8List imageData) async {
await ImageGallerySaver.saveImage(
imageData,
quality: 100,
name: 'signature_${DateTime.now().millisecondsSinceEpoch}',
);
}
Q3: 如何实现签名透明背景?
修改截图时的背景:
// 在RepaintBoundary内不设置背景色
Container(
color: Colors.transparent, // 透明背景
child: CustomPaint(
painter: SignaturePainter(_controller),
),
)
Q4: 如何优化绘制性能?
- 使用RepaintBoundary
RepaintBoundary(
child: CustomPaint(
painter: SignaturePainter(_controller),
),
)
- 减少重绘范围
bool shouldRepaint(SignaturePainter oldDelegate) {
return controller.strokes.length !=
oldDelegate.controller.strokes.length;
}
- 使用Path优化
final path = Path();
for (int i = 0; i < stroke.length - 1; i++) {
if (i == 0) {
path.moveTo(stroke[i].offset.dx, stroke[i].offset.dy);
} else {
path.lineTo(stroke[i].offset.dx, stroke[i].offset.dy);
}
}
canvas.drawPath(path, paint);
Q5: 如何添加签名日期?
在保存时添加日期水印:
Future<Uint8List?> _captureWithDate() async {
final boundary = _signatureKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary == null) return null;
final image = await boundary.toImage(pixelRatio: 3.0);
// 创建新的画布添加日期
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
// 绘制原图
canvas.drawImage(image, Offset.zero, Paint());
// 绘制日期
final textPainter = TextPainter(
text: TextSpan(
text: DateFormat('yyyy-MM-dd').format(DateTime.now()),
style: TextStyle(color: Colors.grey, fontSize: 24),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, Offset(10, image.height - 40));
final picture = recorder.endRecording();
final finalImage = await picture.toImage(
image.width.toInt(),
image.height.toInt(),
);
final byteData = await finalImage.toByteData(
format: ui.ImageByteFormat.png
);
return byteData?.buffer.asUint8List();
}
性能优化
1. 绘制优化
// 使用Path代替多个drawLine
final path = Path();
for (int i = 0; i < stroke.length; i++) {
if (i == 0) {
path.moveTo(stroke[i].offset.dx, stroke[i].offset.dy);
} else {
path.lineTo(stroke[i].offset.dx, stroke[i].offset.dy);
}
}
canvas.drawPath(path, paint);
2. 内存优化
// 限制历史记录数量
if (_savedSignatures.length > 50) {
_savedSignatures.removeLast();
}
// 及时释放资源
void dispose() {
_controller.dispose();
super.dispose();
}
3. 截图优化
// 使用合适的像素比例
final image = await boundary.toImage(
pixelRatio: MediaQuery.of(context).devicePixelRatio,
);
总结
手写签名生成器是一款实用的电子签名工具,具有以下特点:
核心优势
- 流畅体验:基于CustomPainter实现流畅手写
- 高清输出:3倍像素比例高清图片
- 功能完善:颜色、粗细、撤销、网格
- 便捷管理:保存、查看、删除
技术亮点
- CustomPainter:自定义绘制实现
- RepaintBoundary:高清截图技术
- ChangeNotifier:状态管理
- GestureDetector:手势识别
应用价值
- 电子文档签署
- 合同签名
- 快递签收
- 会议签到
- 艺术创作
通过扩展功能如数据持久化、PDF导出、签名识别等,这款应用可以成为专业的电子签名解决方案,满足各种商务和个人需求。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)