前言
最近,專案中有這樣一個需求,需要得到使用者的手寫簽名資訊。於是整理了一下關於Flutter簽字畫板和Widget截圖的相關知識。
如圖:
實現方案
第一步:簽字畫板
通過CustomPaint實現,自定義CustomPainter,把使用者觸控的點用drawLine連線起來。
class BoardPainter extends CustomPainter {
BoardPainter(this.points);
final List<Offset> points;
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
bool shouldRepaint(BoardPainter other) => other.points != points;
}
複製程式碼
第二步:截圖生成 Uint8List
- RepaintBoundary包裹Widget
- 生成 Image
- 生成 Uint8List
// 1. 獲取 RenderRepaintBoundary
RenderRepaintBoundary boundary =
_globalKey.currentContext.findRenderObject();
// 2. 生成 Image
ui.Image image = await boundary.toImage();
// 3. 生成 Uint8List
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
複製程式碼
第三步:本地儲存 Image
通過File.writeAsBytes(List bytes())方法實現。
toFile.writeAsBytes(pngBytes);
複製程式碼
完整程式碼
import 'dart:io';
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:ui' as ui show ImageByteFormat, Image;
/// Created On 2019/12/7
/// Description: 簽名畫板並截圖
///
class DrawingBoardPage extends StatefulWidget {
@override
_DrawingBoardPageState createState() => _DrawingBoardPageState();
}
class _DrawingBoardPageState extends State<DrawingBoardPage> {
/// 標記簽名畫板的Key,用於截圖
GlobalKey _globalKey;
/// 已描繪的點
List<Offset> _points = <Offset>[];
/// 記錄截圖的本地儲存路徑
String _imageLocalPath;
@override
void initState() {
super.initState();
// Init
_globalKey = GlobalKey();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Drawing Board')),
body: Container(
margin: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
child: Column(
children: <Widget>[
Container(
height: 180.0,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 0.5),
),
child: RepaintBoundary(
key: _globalKey,
child: Stack(
children: [
GestureDetector(
onPanUpdate: (details) => _addPoint(details),
onPanEnd: (details) => _points.add(null),
),
CustomPaint(painter: BoardPainter(_points)),
],
),
),
),
Row(
children: <Widget>[
RaisedButton(
color: Theme.of(context).primaryColor,
onPressed: () async {
setState(() {
_points?.clear();
_points = [];
_imageLocalPath = null;
});
},
child: Text(
'CLEAR',
style: TextStyle(color: Colors.white),
),
),
Expanded(child: Container()),
RaisedButton(
color: Theme.of(context).primaryColor,
onPressed: () async {
File toFile = await _saveImageToFile();
String toPath = await _capturePng(toFile);
print('Signature Image Path: $toPath');
setState(() {
_imageLocalPath = toPath;
});
},
child: Text(
'SCREEN SHOT',
style: TextStyle(
fontSize: 14.0,
color: Colors.white,
fontWeight: FontWeight.normal,
),
),
),
],
),
Container(
alignment: Alignment.centerLeft,
margin: EdgeInsets.only(top: 4.0),
child: Text('Image local path:'),
),
Container(
alignment: Alignment.centerLeft,
margin: EdgeInsets.only(top: 4.0),
child: Text(
_imageLocalPath ?? '',
style: TextStyle(color: Colors.blue),
),
),
Container(
alignment: Alignment.centerLeft,
margin: EdgeInsets.only(top: 4.0),
child: Text('Show Image: '),
),
Container(
height: 180.0,
margin: EdgeInsets.only(top: 4.0),
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 0.5),
),
child: Image.file(File(_imageLocalPath ?? '')),
)
],
),
),
);
}
/// 新增點,注意不要超過Widget範圍
_addPoint(DragUpdateDetails details) {
RenderBox referenceBox = _globalKey.currentContext.findRenderObject();
Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
double maxW = referenceBox.size.width;
double maxH = referenceBox.size.height;
// 校驗範圍
if (localPosition.dx <= 0 || localPosition.dy <= 0) return;
if (localPosition.dx > maxW || localPosition.dy > maxH) return;
setState(() {
_points = List.from(_points)..add(localPosition);
});
}
/// 選取儲存檔案的路徑
Future<File> _saveImageToFile() async {
Directory tempDir = await getTemporaryDirectory();
int curT = DateTime.now().millisecondsSinceEpoch;
String toFilePath = '${tempDir.path}/$curT.png';
File toFile = File(toFilePath);
bool exists = await toFile.exists();
if (!exists) {
await toFile.create(recursive: true);
}
return toFile;
}
/// 截圖,並且返回圖片的快取地址
Future<String> _capturePng(File toFile) async {
// 1. 獲取 RenderRepaintBoundary
RenderRepaintBoundary boundary =
_globalKey.currentContext.findRenderObject();
// 2. 生成 Image
ui.Image image = await boundary.toImage();
// 3. 生成 Uint8List
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
// 4. 本地儲存Image
toFile.writeAsBytes(pngBytes);
return toFile.path;
}
}
class BoardPainter extends CustomPainter {
BoardPainter(this.points);
final List<Offset> points;
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
bool shouldRepaint(BoardPainter other) => other.points != points;
}
複製程式碼