COMPANY SERVICE STAFF BLOG NEWS CONTACT

STAFF BLOG

スタッフブログ

TECHNICAL

テクログ

2024.10.28

Flutterでペイントアプリを作ってみた

テクログ

はじめに

お久しぶりです!
のりさんです。

今回はFlutterのCanvasを使って
ペイントアプリを作ってみようと思います。

環境

  • Windows10 Pro
  • Visual Studio Code
  • Flutter 3.24.3 / Dart 3.5.3

新規プロジェクトの作成

flutterコマンドを実行してFlutterプロジェクトを作成していきます。
 flutter create paint_app

これをベースにアプリを作成していきます。

完成アプリ

完成したアプリは以下のようになりました!


ペイントアプリっぽくなりましたね。
それではソースコードをみていきたいと思います。

ソースコード

今回作成したファイルは、lib/main.dartのみです。

import 'dart:ui' as ui;
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

enum Mode { pen, box, line, circle, text }
enum PaintWidth {normal, wide, full}
extension PaintWidthValue on PaintWidth {
  double get strokeWidth  => switch (this) {
    PaintWidth.normal => 2,
    PaintWidth.wide => 5,
    PaintWidth.full => 10,
  };
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var mode = Mode.pen;
  var paintWidth = PaintWidth.normal;
  var startOffset = const Offset(0, 0);
  var isPreview = false;
  List<void Function(ui.Canvas canvas, Size size)> drawHistory = [];
  List<void Function(ui.Canvas canvas, Size size)> drawPreview = [];
  List<ui.Offset> penPoints = [];
  var imageWidth = 1000;
  var imageHeight = 1000;

  late List<Color> palette = createPalette();
  late Color selectedColor = palette.first;
  late Paint paint = createPaint();

  @override
  void initState() {
    drawHistory.add((canvas, size) {
      canvas.drawRect(
        Rect.fromLTWH(0, 0, size.width, size.height),
        Paint()..color = Colors.white
      );
    });
    super.initState();
  }

  Paint createPaint() {
    return Paint()..color = selectedColor
                  ..strokeCap = StrokeCap.round
                  ..style = PaintingStyle.stroke
                  ..strokeWidth = paintWidth.strokeWidth;
  }

  List<Color> createPalette() {
    return [
        Colors.black,
        Colors.grey,
        Colors.brown,
        Colors.red,
        Colors.orange,
        Colors.yellow,
        Colors.green,
        Colors.blue,
        Colors.purple,
        Colors.purpleAccent,
        Colors.white,
        Colors.grey.shade300,
        Colors.brown.shade300,
        Colors.pink.shade200,
        Colors.orange.shade300,
        Colors.yellow.shade300,
        Colors.lime,
        Colors.lightBlue.shade300,
        Colors.purple.shade200,
        Colors.purpleAccent.shade100,
    ];
  }

  @override
  Widget build(BuildContext context) {   

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            GestureDetector(
                onPanStart: (details) {
                  startOffset = details.localPosition;
                  drawPreview = [...drawHistory];
                  if (mode == Mode.pen) {
                    penPoints = [Offset(startOffset.dx, startOffset.dy)];
                    var drawPaint = Paint.from(paint);
                    drawPreview.add((canvas, size) {
                      canvas.drawPoints(ui.PointMode.polygon, penPoints, drawPaint);
                    });
                  }
                  setState(() {
                    isPreview = true;
                  });
                },
                onPanUpdate: (details) {
                  var prevStartOffset = Offset(startOffset.dx, startOffset.dy);
                  var currentOffset = details.localPosition;
                  var drawPaint = Paint.from(paint);
                  var currentColor = Color(selectedColor.value);

                  // 範囲外判定
                  if (currentOffset.dx < imageWidth && currentOffset.dx >= 0 && currentOffset.dy < imageHeight && currentOffset.dy >= 0 ){
                    if (mode == Mode.pen) {
                      // 鉛筆プレビュー
                      penPoints.add(currentOffset);
                    } else if (mode == Mode.line) {
                      
                      // 直線プレビュー
                      drawPreview = [
                        ...drawHistory,
                        (canvas, size) {
                          canvas.drawLine(prevStartOffset, currentOffset, drawPaint);
                        }
                      ];
                    } else if (mode == Mode.box) {
                      // 四角形プレビュー
                      drawPreview = [
                        ...drawHistory,
                        (canvas, size) {
                          canvas.drawRect(Rect.fromLTRB(prevStartOffset.dx, prevStartOffset.dy, currentOffset.dx, currentOffset.dy) , drawPaint);
                        }
                      ];
                    } else if (mode == Mode.circle) {
                      // 円形プレビュー
                      drawPreview = [
                        ...drawHistory,
                        (canvas, size) {
                          var circlePath = Path()
                                ..addOval(Rect.fromCircle(
                                  center: Offset((prevStartOffset.dx + currentOffset.dx) / 2, (prevStartOffset.dy + currentOffset.dy) /2),
                                  radius: currentOffset.dx - prevStartOffset.dx,
                                ));
                              canvas.drawPath(circlePath, drawPaint);
                        }
                      ];
                    } else if (mode == Mode.text) {
                      // テキストプレビュー
                      drawPreview = [
                        ...drawHistory,
                        (canvas, size) {
                          var tp = TextPainter(
                            text: TextSpan(
                              text: "Flutter",
                              style: TextStyle(color: currentColor, fontSize: 10 + (currentOffset.dy - prevStartOffset.dy)),
                            ),
                            textDirection: TextDirection.ltr,
                          );
                          tp.layout(minWidth: 0);
                          tp.paint(canvas, prevStartOffset);
                        }
                      ];
                    }
                    setState(() {});
                  }
                },
                onPanEnd: (details) {
                  var drawPaint = Paint.from(paint);
                  setState(() {
                    if (mode == Mode.pen) {
                      var points = [...penPoints];
                      drawHistory = [...drawHistory, (canvas, size) {
                          canvas.drawPoints(ui.PointMode.polygon, points, drawPaint);
                      }];
                    } else {
                      drawHistory = [...drawPreview];
                    }
                    isPreview = false;
                  });
                },
                child: SizedBox(
                    width: imageWidth.toDouble(),
                    height: imageHeight.toDouble(),
                    child: CustomPaint(
                        painter: DrawHistoryPainter(
                            drawHistory:
                                isPreview ? drawPreview : drawHistory)))),
            Container(
              padding: const EdgeInsets.all(10),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  IconButton(
                    onPressed: ()=>setState((){
                      mode = Mode.pen;
                    }),
                    icon: const Icon(Icons.edit_outlined, color: Colors.black87),
                    style: IconButton.styleFrom(backgroundColor: mode == Mode.pen ? Colors.blue[100] : Colors.transparent),
                  ),
                  const SizedBox(height: 10),
                  IconButton(
                    onPressed: ()=>setState((){
                      mode = Mode.line;
                    }),
                    icon: const Icon(Icons.border_color_outlined, color: Colors.black87),
                    style: IconButton.styleFrom(backgroundColor: mode == Mode.line ? Colors.blue[100] : Colors.transparent),
                  ),
                  const SizedBox(height: 10),
                  IconButton(
                    onPressed: ()=>setState((){
                      mode = Mode.box;
                    }),
                    icon: const Icon(Icons.rectangle_outlined, color: Colors.black87),
                    style: IconButton.styleFrom(backgroundColor: mode == Mode.box ? Colors.blue[100] : Colors.transparent),
                  ),
                  const SizedBox(height: 10),
                  IconButton(
                    onPressed: ()=>setState((){
                      mode = Mode.circle;
                    }),
                    icon: const Icon(Icons.circle_outlined, color: Colors.black87),
                    style: IconButton.styleFrom(backgroundColor: mode == Mode.circle ? Colors.blue[100] : Colors.transparent),
                  ),
                  const SizedBox(height: 10),
                  IconButton(
                    onPressed: ()=>setState((){
                      mode = Mode.text;
                    }),
                    icon: const Icon(Icons.font_download_outlined, color: Colors.black87),
                    style: IconButton.styleFrom(backgroundColor: mode == Mode.text ? Colors.blue[100] : Colors.transparent),
                  )
                ],
              ),
            ),
            Container(
              padding: const EdgeInsets.all(10),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  IconButton(
                    onPressed: ()=>setState((){
                      paintWidth = PaintWidth.normal;
                      paint = createPaint();
                    }),
                    icon: const Icon(Icons.width_normal, color: Colors.black87),
                    style: IconButton.styleFrom(backgroundColor: paintWidth == PaintWidth.normal ? Colors.green[100] : Colors.transparent),
                  ),
                  const SizedBox(height: 10),
                  IconButton(
                    onPressed: ()=>setState((){
                      paintWidth = PaintWidth.wide;
                      paint = createPaint();
                    }),
                    icon: const Icon(Icons.width_wide, color: Colors.black87),
                    style: IconButton.styleFrom(backgroundColor: paintWidth == PaintWidth.wide ? Colors.green[100] : Colors.transparent),
                  ),
                  const SizedBox(height: 10),
                  IconButton(
                    onPressed: ()=>setState((){
                      paintWidth = PaintWidth.full;
                      paint = createPaint();
                    }),
                    icon: const Icon(Icons.width_full, color: Colors.black87),
                    style: IconButton.styleFrom(backgroundColor: paintWidth == PaintWidth.full ? Colors.green[100] : Colors.transparent),
                  ),
                ],
              ),
            ),
            Container(
              width: 300,
              padding: const EdgeInsets.all(10),
              child: GridView.builder(
                itemCount: palette.length,
                itemBuilder: (context, index) {
                  return GestureDetector(
                    child: Container(
                      color: selectedColor == palette[index] ? Colors.blue.shade200 : Colors.transparent,
                      padding: const EdgeInsets.all(3),
                      child: ColoredBox(
                        color: palette[index],
                      ),
                    ),
                    onTap: () {
                      setState(() {
                        selectedColor = palette[index];
                        paint = createPaint();
                      });
                    },
                  );
                },
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 10,
                  mainAxisSpacing: 0,
                  crossAxisSpacing: 2,
                ),
              ),
            ),
          ],
        ),
        
    );
  }
}

class DrawHistoryPainter extends CustomPainter {
  final List<void Function(ui.Canvas canvas, Size size)> drawHistory;
  DrawHistoryPainter({required this.drawHistory});
  @override
  void paint(Canvas canvas, Size size) {
    for (var draw in drawHistory) {
      draw(canvas, size);
    }
  }

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

おわりに

今回はCanvas操作の履歴と、CustomPaint/CustomPainterを使って実装してみました。

今回のソースコードにはありませんが
PictureRecorderを使えばイメージに変換できるので、そこから画像の保存もできそうです。
また、drawHistoryはリストになっているため、元に戻すなどの操作はリスト編集すれば簡単に実装できそうですね。

それではまた!

この記事を書いた人

のりさん

入社年2014年

出身地東京都

業務内容開発

特技・趣味水泳・筋トレ・旅行

雑記に関する記事一覧

TOP