こんにちは。
スマホアプリをメインに開発しているロッキーカナイです。
今回は、Flutterでお絵かき機能を実装してみましたので、その紹介をいたします。
ざっくり仕様
- 画面をなぞると線が描写されるようにする
- undo(元に戻す)機能の実装
- redo(やり直す)機能の実装
- clear(削除)機能の実装
- undoとredo、clearボタンはフローティングボタンに配置する
以上!
コーディング
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import 'package:flutter/material.dart'; import 'package:flutter_test_app/PaintPage.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: PaintPage(), ); } } |
main.dartはペイントのページを呼び出すだけなので、以後変更はありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
import 'package:flutter/material.dart'; /* * ペイントページ */ class PaintPage extends StatefulWidget { @override _PaintPageState createState() => _PaintPageState(); } /* * ペイント ステート */ class _PaintPageState extends State<PaintPage> { @override Widget build(BuildContext context) { return Scaffold( /* * AppBar */ appBar: AppBar( title: Text('ペイント'), centerTitle: true, ), /* * body */ body: Container( color: Colors.blue, ), /* * floatingActionButton */ floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ // undoボタン FloatingActionButton( heroTag: "undo", onPressed: () { }, child: Text("undo"), ), SizedBox( height: 20.0, ), // redoボタン FloatingActionButton( heroTag: "redo", onPressed: () { }, child: Text("redo"), ), SizedBox( height: 20.0, ), // クリアボタン FloatingActionButton( heroTag: "clear", onPressed: () { }, child: Text("clear"), ), ], ), ); } } |
PaintPage.dartではフローティングボタンの用意をしただけのクラスになります。
こちらをベースに変更を加えていきお絵かき機能の実装をしていきます。
Flutterでペイント機能で全体の制御をまとめると、
新規でPainterクラス、PaintControllerクラス、_CustomPainterクラス、PaintHistoryクラスをつくります。
Painterクラスでは以下の機能や制御を行います。
- ペイントをコントロールするクラス(PaintController)を保持。
- 指のジェスチャーの取得
- 線を描写するクラス(_CustomPainter)を保持
PaintControllerクラスでは以下の機能や制御を行います。
- ペイントの履歴(PaintHistory)を保持
- 線の太さなどの設定データを保持(または変更できる)
- undoやredo、clearの制御の受け取り口を持っていて実行をPaintHistoryへ指示する
PaintHistoryクラスでは以下の機能をもっています。
- ジェクチャーを受け取り線のデータを管理
- undoやredo、clear機能
_CustomPainterクラスは、CustomPainterを継承しており、描写のカスタマイズをするためのものです。
全体コード
PaintPage.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
import 'package:flutter/material.dart'; import 'package:flutter_test_app/Painter.dart'; /* * ペイントページ */ class PaintPage extends StatefulWidget { @override _PaintPageState createState() => _PaintPageState(); } /* * ペイント ステート */ class _PaintPageState extends State<PaintPage> { // コントローラ PaintController _controller = PaintController(); @override Widget build(BuildContext context) { return Scaffold( /* * AppBar */ appBar: AppBar( title: Text('ペイント'), centerTitle: true, ), /* * body */ body: Container( child: Painter( paintController: _controller, ), ), /* * floatingActionButton */ floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ // undoボタン FloatingActionButton( heroTag: "undo", onPressed: () { if (_controller.canUndo) _controller.undo(); }, child: Text("undo"), ), SizedBox( height: 20.0, ), // redoボタン FloatingActionButton( heroTag: "redo", onPressed: () { if (_controller.canRedo) _controller.redo(); }, child: Text("redo"), ), SizedBox( height: 20.0, ), // クリアボタン FloatingActionButton( heroTag: "clear", onPressed: () => _controller.clear(), child: Text("clear"), ), ], ), ); } } |
Painter.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test_app/PaintHistory.dart'; /* * ペイント */ class Painter extends StatefulWidget { // ペイントコントローラ final PaintController paintController; Painter({ @required this.paintController }) : super(key: ValueKey<PaintController>(paintController)) { assert(this.paintController != null); } @override _PainterState createState() => _PainterState(); } /* * ペイント ステート */ class _PainterState extends State<Painter> { @override Widget build(BuildContext context) { return Container( // イベント監視 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 PaintHistory _paintHistory; _CustomPainter( this._paintHistory, { Listenable repaint }) : super(repaint: repaint); @override void paint(Canvas canvas, Size size) { _paintHistory.draw(canvas, size); } @override bool shouldRepaint(_CustomPainter oldDelegate) => true; } /* * ペイントコントローラ */ class PaintController extends ChangeNotifier { // ペイント履歴 PaintHistory _paintHistory = PaintHistory(); // 線の色 Color _drawColor = Color.fromARGB(255, 0, 0, 0); // 線幅 double _thickness = 5.0; // 背景色 Color _backgroundColor = Color.fromARGB(255, 255, 255, 255); /* * コンストラクタ */ PaintController() : super() { // ペイント設定 Paint paint = Paint(); paint.color = _drawColor; paint.style = PaintingStyle.stroke; paint.strokeWidth = _thickness; _paintHistory.currentPaint = paint; _paintHistory.backgroundColor = _backgroundColor; } /* * undo実行 */ void undo() { _paintHistory.undo(); notifyListeners(); } /* * redo実行 */ void redo() { _paintHistory.redo(); notifyListeners(); } /* * undo可能か */ bool get canUndo => _paintHistory.canUndo(); /* * redo可能か */ bool get canRedo => _paintHistory.canRedo(); /* * リスナー実行 */ void _notifyListeners() { notifyListeners(); } /* * クリア */ void clear() { _paintHistory.clear(); notifyListeners(); } } |
PaintHistory.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; /* * ペイントデータ */ class _PaintData { _PaintData({ this.path, }) : super(); Path path; // パス } /* * ペイントの履歴を管理するクラス */ class PaintHistory { // ペイントの履歴リスト List<MapEntry<_PaintData, Paint>> _paintList = List<MapEntry<_PaintData, Paint>>(); // ペイントundoリスト List<MapEntry<_PaintData, Paint>> _undoneList = List<MapEntry<_PaintData, Paint>>(); // 背景ペイント Paint _backgroundPaint = Paint(); // ドラッグ中フラグ bool _inDrag = false; // カレントペイント Paint currentPaint; /* * undo可能か */ bool canUndo() => _paintList.length > 0; /* * redo可能か */ bool canRedo() => _undoneList.length > 0; /* * undo */ void undo() { if (!_inDrag && canUndo()) { _undoneList.add(_paintList.removeLast()); } } /* * redo */ void redo() { if (!_inDrag && canRedo()) { _paintList.add(_undoneList.removeLast()); } } /* * クリア */ void clear() { if (!_inDrag) { _paintList.clear(); _undoneList.clear(); } } /* * 背景色セッター */ set backgroundColor(color) => _backgroundPaint.color = color; /* * 線ペイント開始 */ 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) { if (data.key.path != null) { canvas.drawPath(data.key.path, data.value); } } } } |
iOSシュミレータで確認

我ながら画伯っぷりを炸裂しましたな。
ちなみにundoやredo、clearも正常に動作しました。
PaintControllerを改変することで線の幅や色なども変更できます。
今回_PaintDataクラスには触れませんでしたが、ここに図形データや画像を入れられるようにすることで、線以外のお絵かきも可能です。それはまた機会があれば、紹介したいと思います。
エンドロール
エンドロールって書きたかっただけです。特になにもありません。
Flutter Webなるものがあるので、今後触る機会があったら記事にしたいと思います。
それでは。
