Android
iOS
Dart
Flutter

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)
          ),
        )) ;
  }
}