Flutterで手書き署名機能(ペイント)を作ってみた

#はじめに
こちらの記事は[ユアマイスターアドベントカレンダー2021](https://adventar.org/calendars/6956 "ユアマイスター Advent Calendar 2021
")、8日目の記事です
(投稿が遅れ、申し訳ありません!!)。

業務で手書き署名機能を実装する可能性がでたので、実際にどのように作るのか試してみました。
手書き署名で調べてましたが、実際にはペイント機能を作ればよさそうでした。

#参考
下記を参考にさせてもらいました。
https://www.egao-inc.co.jp/programming/flutter-paint/

#実装

後に説明するSignerとクリア用ボタンを設置しておきます。

```
import 'package:flutter/material.dart';
import 'package:test/signer.dart';

class SignPage extends StatefulWidget {
  const SignPage({Key? key}) : super(key: key);

  @override
  _SignPageState createState() => _SignPageState();
}


class _SignPageState extends State<SignPage> {
  final SignController _controller = SignController();

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: const Text('署名'),
        centerTitle: true,
      ),
      body: Signer(
        paintController: _controller,
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            heroTag: "clear",
            onPressed: () => _controller.clear(),
            child: const Text('クリア'),
          ),
        ],
      ),
    );
  }
}
```

GestureDetectorでタッチイベントを検出します。
https://api.flutter.dev/flutter/widgets/GestureDetector-class.html

```
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:test/sign_history.dart';

class Signer extends StatefulWidget {
  final SignController paintController;

  Signer({required this.paintController})
      : super(key: ValueKey<SignController>(paintController));

  @override
  _SignerState createState() => _SignerState();
}

class _SignerState extends State<Signer> {
  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: GestureDetector(
        child: CustomPaint(
          willChange: true,
          painter: _CustomPainter(
            widget.paintController._paintHistory,
            repaint: widget.paintController,
          ),
        ),
        onPanStart: _onPaintStart,
        onPanUpdate: _onPaintUpdate,
        onPanEnd: _onPaintEnd,
      ),
      width: double.infinity,
      height: double.infinity,
    );
  }

  void _onPaintStart(DragStartDetails start) {
    widget.paintController._paintHistory
        .addPaint(_getGlobalToLocalPosition(start.globalPosition));
    widget.paintController._notifyListeners();
  }

  void _onPaintUpdate(DragUpdateDetails update) {
    widget.paintController._paintHistory
        .updatePaint(_getGlobalToLocalPosition(update.globalPosition));
    widget.paintController._notifyListeners();
  }

  void _onPaintEnd(DragEndDetails end) {
    widget.paintController._paintHistory.endPaint();
    widget.paintController._notifyListeners();
  }

  Offset _getGlobalToLocalPosition(Offset global) {
    return (context.findRenderObject() as RenderBox).globalToLocal(global);
  }
}

class _CustomPainter extends CustomPainter {
  final SignHistory _paintHistory;

  _CustomPainter(this._paintHistory, {required Listenable repaint})
      : super(repaint: repaint);

  @override
  void paint(Canvas canvas, Size size) {
    _paintHistory.draw(canvas, size);
  }

  @override
  bool shouldRepaint(_CustomPainter oldDelegate) => true;
}

class SignController extends ChangeNotifier {
  final SignHistory _paintHistory = SignHistory();

  final Color _drawColor = Colors.black;

  final double _thickness = 2.0;

  final Color _backgroundColor = Colors.white;

  SignController() : super() {
    Paint paint = Paint();
    paint.color = _drawColor;
    paint.style = PaintingStyle.stroke;
    paint.strokeWidth = _thickness;
    _paintHistory.currentPaint = paint;
    _paintHistory.backgroundColor = _backgroundColor;
  }

  void _notifyListeners() {
    notifyListeners();
  }

  void clear() {
    _paintHistory.clear();
    notifyListeners();
  }
}
```

描画の履歴を管理するwidgetです。

```
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class _PaintData {
  _PaintData({
    required this.path,
  }) : super();

  Path path;
}

class SignHistory {
  final List<MapEntry<_PaintData, Paint>> _paintList =
      <MapEntry<_PaintData, Paint>>[];

  final Paint _backgroundPaint = Paint();

  bool _inDrag = false;

  late Paint currentPaint;

  set backgroundColor(color) => _backgroundPaint.color = color;

  void clear() {

    if (!_inDrag) {
      _paintList.clear();
    }
  }

  void addPaint(Offset startPoint) {
    if (!_inDrag) {
      _inDrag = true;
      Path path = Path();
      path.moveTo(startPoint.dx, startPoint.dy);
      _PaintData data = _PaintData(path: path);
      _paintList.add(MapEntry<_PaintData, Paint>(data, currentPaint));
    }
  }

  void updatePaint(Offset nextPoint) {
    if (_inDrag) {
      _PaintData data = _paintList.last.key;
      Path path = data.path;
      path.lineTo(nextPoint.dx, nextPoint.dy);
    }
  }

  void endPaint() {
    _inDrag = false;
  }

  void draw(Canvas canvas, Size size) {
    canvas.drawRect(
      Rect.fromLTWH(
        0.0,
        0.0,
        size.width,
        size.height,
      ),
      _backgroundPaint,
    );

    for (MapEntry<_PaintData, Paint> data in _paintList) {
      canvas.drawPath(data.key.path, data.value);
    }
  }
}
```