Flutter 簽字畫板及螢幕或Widget截圖

藍色微笑ing 發表於 2019-12-07

前言

最近,專案中有這樣一個需求,需要得到使用者的手寫簽名資訊。於是整理了一下關於Flutter簽字畫板和Widget截圖的相關知識。

如圖:

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;
}
複製程式碼