Edited at

Flutterでお絵かきしてみない?


アプリ開発の基本と言えば

そう、「お絵かきアプリ」ですよね。(え、ちがう?)

というわけで、お勉強として作ってみました。

iine.mov.gif

適当に作った割には、ヌルヌル動いている気がします。


ポイント


CustomPaint

Flutterには、CustomPaintと呼ばれるWidgetがあり、setState()すると、コンストラクタに渡したCustomPainterpaint()が呼ばれる。

paint()には、引数としてCanvasが付いているので、こいつに対してdrawLineなどを呼ぶ。


main.dart

  Expanded(

child: AspectRatio(
aspectRatio: 1.0,
child: GestureDetector(
child: CustomPaint(
painter: PaintCanvas(lines,nowPoints,nowColor),
),
onHorizontalDragUpdate: moveGestureDetector,
onVerticalDragUpdate: moveGestureDetector,
onHorizontalDragStart: newGestureDetector,
onVerticalDragStart: newGestureDetector,
),
),



AspectRatio

GestureDetectorをAspectRatioで囲うことで、画面全体に描画エリアを引き延ばしています。

(多分、横にはみ出しています。。)


Bezier

やってないけど、ベジエ曲線も描けそうです


CustomPainter.dart

    canvas.drawPath(path, paint)


という感じで、pathを渡せるので。


不明点


Widgetの座標

それぞれのWidgetの座標が取得できず(方法が分からず)、GrobalPostionに無理やり、上部のパレットの高さ分を足し算して、描画しています。

ステータスバーの高さ下げるために、SafeAreaという便利なWidgetがあったのですが、何ポイント下にずれているのか分からず、諦めました。

rect_getterを使えば取得できそうな気がしますが、試していません。。


ソース

main.dartをコピペすれば動きますので、まんま置いておきます。

色々とアレなところはありますが、何かの参考になれば幸いです。

// 2018/09/18 追記ここから

一応Githubにも置いておきました。

// 2018/09/18 追記ここまで


main.dart

import 'package:flutter/material.dart';

import 'package:flutter/rendering.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.dark
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

List<LinePoints> lines = <LinePoints>[];
List<Offset> nowPoints = <Offset>[];
Color nowColor = Colors.redAccent;

void moveGestureDetector(DragUpdateDetails detail){
Offset p = Offset(detail.globalPosition.dx, detail.globalPosition.dy - 60);
setState(() {
nowPoints.add(p);
});
}

void newGestureDetector(DragStartDetails detail) {
if (nowPoints.length != 0) {
LinePoints l = LinePoints(new List<Offset>.from(nowPoints), nowColor);
lines.add(l);
nowPoints.clear();
}
Offset p = Offset(detail.globalPosition.dx, detail.globalPosition.dy - 60);
setState(() {
nowPoints.add(p);
});
}

void changeColor (Color c){
if (nowPoints.length != 0) {
LinePoints l = LinePoints(new List<Offset>.from(nowPoints), nowColor);
lines.add(l);
}
setState(() {
nowPoints.clear();
nowColor = c;
});
}

List<Color> colors = <Color>[
Colors.redAccent,
Colors.pink,
Colors.greenAccent,
Colors.blueAccent,
Colors.amber,
Colors.purpleAccent,
Colors.deepPurpleAccent,
Colors.lightBlueAccent,
Colors.lightGreenAccent,
Colors.cyanAccent,];

void _tapClear(){
setState(() {
lines.clear();
nowPoints.clear();
});
}

@override
Widget build(BuildContext context) {
List<Widget> pallet = <Widget>[];
for (int i = 0; i < colors.length; i++) {
Color c = colors[i];
pallet.add(ColorPallet(color: c,changeColor: changeColor,isSelect: c==nowColor,));
}

return new Scaffold(
primary: false,
body: new Container(
decoration: BoxDecoration(
color: Colors.white
),
child:new Flex(
direction: Axis.vertical,
children: <Widget>[
Container(
decoration:BoxDecoration(
color: Colors.black12
),
child: ListView(
scrollDirection: Axis.horizontal,
children: pallet
,
),
height: 60.0,
),
Expanded(
child: AspectRatio(
aspectRatio: 1.0,
child: GestureDetector(
child: CustomPaint(
painter: PaintCanvas(lines,nowPoints,nowColor),
),
onHorizontalDragUpdate: moveGestureDetector,
onVerticalDragUpdate: moveGestureDetector,
onHorizontalDragStart: newGestureDetector,
onVerticalDragStart: newGestureDetector,
),
),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _tapClear,
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
child: Icon(Icons.delete),
),
);
}
}

// 実際に描画するキャンバス
class PaintCanvas extends CustomPainter{

final List<LinePoints> lines;
final List<Offset> nowPoints;
final Color nowColor;

PaintCanvas(this.lines, this.nowPoints, this.nowColor);

@override
void paint(Canvas canvas, Size size) {
Paint p = new Paint()
..color = Colors.redAccent
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
canvas.save();
for (int i = 0; i < lines.length; i++) {
LinePoints l = lines[i];
for (int j = 1; j < l.points.length; j++){
Offset p1 = l.points[j - 1];
Offset p2 = l.points[j];
p.color = l.lineColor;
canvas.drawLine(p1, p2, p);
}
}
for (int i = 1; i < nowPoints.length; i++){
Offset p1 = nowPoints[i - 1];
Offset p2 = nowPoints[i];
p.color = nowColor;
canvas.drawLine(p1, p2, p);
}

canvas.restore();
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

// 一筆書き分の座標を持つClass
class LinePoints{
final List<Offset> points;
final Color lineColor;
LinePoints(this.points, this.lineColor);
}

// 色を変えるボタンClass
class ColorPallet extends StatelessWidget {
final Color color;
final Function changeColor;
const ColorPallet({Key key, this.color, this.changeColor, this.isSelect}) : super(key: key);
final bool isSelect;

void onPressed(){
changeColor(color);
}

@override
Widget build(BuildContext context) {
return new RawMaterialButton(
onPressed: onPressed,
constraints: BoxConstraints(minWidth: 60.0,minHeight: 50.0),
child: new Container(
margin: EdgeInsets.only(top: 5.0,bottom: 5.0),
width: 50.0,
height: 50.0,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.all(Radius.circular(25.0)),
border: Border.all(color: Colors.white,width: isSelect?3.0:0.0)
),
)) ;
}
}