【Flutter】GestureDetectorとCustomPaintを使い、タッチした場所に図形を描画する

By | 2019年12月4日

環境

  • Flutter 1.12.16
  • AndroidStudio 3.5

概要

タッチした場所に図形を描画するアプリをFlutterで作成する。完成イメージは以下のようになる。

このアプリの作成を通してGestureDetectorとCustomPaintの基本的な使い方を紹介する。

準備

アプリの作成はAndroidStudioで行った。まずは新規のFlutterプロジェクトを作成し、main.dartを次のように編集してアプリの土台を作る。

main.dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pointer drawing lesson',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(),
    );
  }
}

描画ウィジェットの作成

それでは描画ウィジェットを作成しよう。このウィジェットが、最終的には先に上記に掲載したコードのMaterialAppのhomeで指定されているScaffoldと置き換わる。描画ウィジェットはタッチした点を覚えておく必要があるため、StatefulWidgetとして作成する。main.dartに以下のコードを追加する。

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

  @override
  _PointerDrawingWidgetState createState() => _PointerDrawingWidgetState();
}

このように、状態を持つようなウィジェットを作る場合はStatefulWidgetを継承する。その実装はcreateState()をオーバーライドしてStateクラスを返すことのみを行う。
_PointerDrawingWidgetState()が、このWidgetのためのStateを表すクラスである。dart.mainに、続けて次のコードを実装しよう。

// 描画ウィジェットの状態クラス
class _PointerDrawingWidgetState extends State<PointerDrawingWidget> {
  // タッチした点を覚えておく
  final _points = List<Offset>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: GestureDetector(
        // TapDownイベントを検知
        onTapDown: _addPoint,
        // カスタムペイント
        child: CustomPaint(
          painter: MyPainter(_points),
          // タッチを有効にするため、childが必要
          child: Center(),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // 点のクリアボタン
        onPressed: _clearPoints,
        tooltip: 'Clear',
        child: Icon(Icons.clear),
      ),
    );
  }

  // タッチした点をクリアする
  void _clearPoints(){
    setState((){
      _points.clear();
    });
  }

  // 点を追加
  void _addPoint(TapDownDetails details) {
    // setState()にリストを更新する関数を渡して状態を更新
    setState(() {
      _points.add(details.localPosition);
    });
  }
}

ややコードが長くなったが、主要なポイントを説明する。

  • 4行目でタッチした点を記録しておくためOffsetのListを宣言している。これがこのクラスの状態となる。
  • 12行目、ScaffoldのbodyにはGestureDetectorを指定している。このウィジェットを使うことで、ユーザーのインタラクションを検出できる。ここでは、ユーザーのタッチに反応するため、onTapDownイベントを使った。
  • 12行目、GestureDetectorのchildに指定しているのはCustomPaintである。このウィジェットはCustomPainterを実装することで、背景に自在に描画することができる。実装の詳細は後述する。
  • 19行目ではCustomPainterのchildにCenterウィジェットを置いている。このようにしないと、GestureDetectorがタッチイベントに反応しないため、あえてこのようにしている。領域全体を占めるウィジェットであれば、Centerでなくても良いと思う。
  • 39行目の_addPaint()はonTapDownイベント発生時に呼ばれるメソッドで、タッチされた座標をリストに追加している。Flutterでの決まりごととして、状態を更新するにはStateに用意されているsetState()メソッドに、更新のための関数を渡すことで行う必要がある。

次に、カスタム描画のコードを示す。


class MyPainter extends CustomPainter{
  final List<Offset> _points;
  final _rectPaint = Paint()..color = Colors.blue;

  MyPainter(this._points);

  @override
  void paint(Canvas canvas, Size size) {
    // 記憶している点を描画する
    _points.forEach((offset) =>
        canvas.drawRect(Rect.fromCenter(center: offset, width: 20.0, height: 20.0), _rectPaint));
  }

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

カスタム描画を行うときはCustomPainterを継承したクラスを作り、paint()とshouldRepaint()をオーバーライドする。paint()には実際の描画コードを書く。引数で渡ってくるCanvasには色々な描画メソッドが用意されている。ここでは、タッチした点を中心として大きさ20の四角形を描画している。この辺りの使い方はAndroidネイティブの描画方法とほぼ同じなため、すぐに馴染めると思う。shouldRepaint()はboolを返すメソッドで、再描画すべき時にはtrueを返す必要がある。このように簡単なサンプルでは常にtrueを返しても良いだろう。

これで、実装は完了した。最後に冒頭のMaterialAppのhomeを描画ウィジェットに置き換えるとアプリの完成である。


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pointer drawing lesson',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // MaterialAppのhomeに作成した描画ウィジェットを指定する
      home: PointerDrawingWidget(title:'Pointer drawing lesson'),
    );
  }
}

このアプリの完全なコードはgithubにあるので参考にして欲しい。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です