もくじ
こんにちは。
スマホアプリをメインに開発しているロッキーカナイです。
今回は、Flutterでお絵かき機能を実装してみましたので、その紹介をいたします。
ざっくり仕様
- 画面をなぞると線が描写されるようにする
- undo(元に戻す)機能の実装
- redo(やり直す)機能の実装
- clear(削除)機能の実装
- undoとredo、clearボタンはフローティングボタンに配置する
以上!
コーディング
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はペイントのページを呼び出すだけなので、以後変更はありません。
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
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
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
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なるものがあるので、今後触る機会があったら記事にしたいと思います。
それでは。