Flutterで指先アートを実現!「finger_painter」

モバイルアプリでお絵描き機能やサイン入力機能を実装したいと思ったことはありませんか?タッチスクリーンを活用した描画機能は、子供向けアプリ、メモアプリ、電子署名、教育アプリなど様々な用途で重宝されます。しかし、Flutterでゼロから描画機能を実装するのは意外と手間がかかるものです。

  • カスタムペイントの実装が複雑
  • タッチイベントの処理が煩雑
  • 線の滑らかさやパフォーマンスの最適化が難しい

こうした課題を簡単に解決できるのが、今回紹介する「finger_painter」です。


finger_painterとは?

finger_painterは、Flutterアプリにシンプルかつ柔軟なタッチ描画機能を簡単に実装できるパッケージです。

主な特徴:

  • 直感的なAPIで簡単に実装可能
  • カスタマイズ可能な線の太さ、色、スタイル
  • 複数の描画モード(フリーハンド、直線、形状など)
  • 描画内容の保存と復元
  • 消しゴム機能やクリア機能
  • 軽量で高パフォーマンス

このパッケージを使えば、プロフェッショナルな描画機能をわずか数行のコードで実装できます。


インストール方法

まずは pubspec.yaml に依存関係を追加します。

dependencies:
  finger_painter: ^1.2.0  # 最新版は pub.dev を確認

その後、依存関係を取得します:

flutter pub get

基本の使い方

1. シンプルな描画キャンバスの実装

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

class SimpleDrawingPad extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('お絵描きパッド')),
      body: Center(
        child: Container(
          width: 300,
          height: 400,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey),
            borderRadius: BorderRadius.circular(12),
          ),
          child: FingerPainter(
            backgroundColor: Colors.white,
            strokeColor: Colors.blue,
            strokeWidth: 5.0,
          ),
        ),
      ),
    );
  }
}

これだけで、青色のペンで描画できるキャンバスが表示されます。


2. 描画コントローラーを活用する

描画内容を制御したり、画像として保存するには、コントローラーを使用します:

class DrawingPadWithController extends StatefulWidget {
  @override
  _DrawingPadWithControllerState createState() => _DrawingPadWithControllerState();
}

class _DrawingPadWithControllerState extends State<DrawingPadWithController> {
  // コントローラーを作成
  final FingerPainterController _controller = FingerPainterController();
  Color _selectedColor = Colors.black;
  double _selectedWidth = 5.0;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('高機能お絵描きパッド')),
      body: Column(
        children: [
          // ツールバー
          Container(
            padding: EdgeInsets.all(8),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                // 色選択ボタン
                IconButton(
                  icon: Icon(Icons.color_lens, color: _selectedColor),
                  onPressed: _showColorPicker,
                ),
                // 線の太さ調整
                Slider(
                  value: _selectedWidth,
                  min: 1.0,
                  max: 20.0,
                  onChanged: (value) {
                    setState(() {
                      _selectedWidth = value;
                      _controller.setStrokeWidth(value);
                    });
                  },
                ),
                // クリアボタン
                IconButton(
                  icon: Icon(Icons.clear),
                  onPressed: () {
                    _controller.clear();
                  },
                ),
                // 保存ボタン
                IconButton(
                  icon: Icon(Icons.save),
                  onPressed: _saveDrawing,
                ),
              ],
            ),
          ),
          // 描画エリア
          Expanded(
            child: Container(
              margin: EdgeInsets.all(16),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.grey),
                borderRadius: BorderRadius.circular(12),
              ),
              child: FingerPainter(
                controller: _controller,
                backgroundColor: Colors.white,
                strokeColor: _selectedColor,
                strokeWidth: _selectedWidth,
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _showColorPicker() {
    // 色選択ダイアログを表示
    // 実際の実装ではColorPickerパッケージなどを使用
    setState(() {
      _selectedColor = Colors.red; // 例としてred固定
      _controller.setStrokeColor(_selectedColor);
    });
  }

  Future<void> _saveDrawing() async {
    // 描画内容をUint8List(画像データ)として取得
    final imageBytes = await _controller.toImage();
    
    // 実際のアプリでは、ここで画像を保存したり共有したりする処理を実装
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('描画を保存しました!')),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

3. 描画モードの切り替え

finger_painterはさまざまな描画モードをサポートしています:

// フリーハンド描画(デフォルト)
_controller.setDrawMode(DrawMode.freeform);

// 直線描画
_controller.setDrawMode(DrawMode.line);

// 長方形描画
_controller.setDrawMode(DrawMode.rectangle);

// 円描画
_controller.setDrawMode(DrawMode.circle);

// 消しゴムモード
_controller.setDrawMode(DrawMode.eraser);

これらのモードを切り替えるUIを実装してみましょう:

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    IconButton(
      icon: Icon(Icons.edit),
      color: _currentMode == DrawMode.freeform ? Colors.blue : Colors.grey,
      onPressed: () {
        setState(() {
          _currentMode = DrawMode.freeform;
          _controller.setDrawMode(_currentMode);
        });
      },
    ),
    IconButton(
      icon: Icon(Icons.linear_scale),
      color: _currentMode == DrawMode.line ? Colors.blue : Colors.grey,
      onPressed: () {
        setState(() {
          _currentMode = DrawMode.line;
          _controller.setDrawMode(_currentMode);
        });
      },
    ),
    IconButton(
      icon: Icon(Icons.rectangle_outlined),
      color: _currentMode == DrawMode.rectangle ? Colors.blue : Colors.grey,
      onPressed: () {
        setState(() {
          _currentMode = DrawMode.rectangle;
          _controller.setDrawMode(_currentMode);
        });
      },
    ),
    IconButton(
      icon: Icon(Icons.circle_outlined),
      color: _currentMode == DrawMode.circle ? Colors.blue : Colors.grey,
      onPressed: () {
        setState(() {
          _currentMode = DrawMode.circle;
          _controller.setDrawMode(_currentMode);
        });
      },
    ),
    IconButton(
      icon: Icon(Icons.auto_fix_high),
      color: _currentMode == DrawMode.eraser ? Colors.blue : Colors.grey,
      onPressed: () {
        setState(() {
          _currentMode = DrawMode.eraser;
          _controller.setDrawMode(_currentMode);
        });
      },
    ),
  ],
)

高度な機能

背景画像の設定

描画キャンバスに背景画像を設定することもできます:

FingerPainter(
  controller: _controller,
  backgroundColor: Colors.transparent,
  backgroundImage: AssetImage('assets/images/template.png'),
  strokeColor: Colors.red,
  strokeWidth: 3.0,
)

これは写真に注釈を付けたり、テンプレート上に描画したりする場合に便利です。


描画履歴の管理(元に戻す/やり直し)

描画履歴を管理して「元に戻す」「やり直し」機能を実装することもできます:

Row(
  children: [
    IconButton(
      icon: Icon(Icons.undo),
      onPressed: _controller.canUndo() ? () {
        _controller.undo();
      } : null,
    ),
    IconButton(
      icon: Icon(Icons.redo),
      onPressed: _controller.canRedo() ? () {
        _controller.redo();
      } : null,
    ),
  ],
)

描画内容の保存と復元

アプリの状態を保存するため、描画内容をシリアライズ/デシリアライズすることも可能です:

// 描画データを取得
final drawingData = await _controller.toJson();

// データを保存(SharedPreferencesなどを使用)
final prefs = await SharedPreferences.getInstance();
await prefs.setString('saved_drawing', drawingData);

// 後で描画を復元
final savedData = prefs.getString('saved_drawing');
if (savedData != null) {
  await _controller.fromJson(savedData);
}

実装例:シグネチャーパッド

電子署名を収集するためのシンプルなシグネチャーパッドを実装してみましょう:

import 'package:flutter/material.dart';
import 'package:finger_painter/finger_painter.dart';
import 'dart:typed_data';

class SignaturePad extends StatefulWidget {
  @override
  _SignaturePadState createState() => _SignaturePadState();
}

class _SignaturePadState extends State<SignaturePad> {
  final FingerPainterController _controller = FingerPainterController();
  Uint8List? _signatureImage;
  bool _isSigned = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('電子署名'),
        actions: [
          IconButton(
            icon: Icon(Icons.check),
            onPressed: _isSigned ? _saveSignature : null,
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16),
            child: Text(
              '以下のスペースにサインしてください',
              style: TextStyle(fontSize: 18),
            ),
          ),
          Expanded(
            child: Container(
              margin: EdgeInsets.symmetric(horizontal: 16),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.grey),
                borderRadius: BorderRadius.circular(8),
              ),
              child: _signatureImage == null
                  ? FingerPainter(
                      controller: _controller,
                      backgroundColor: Colors.white,
                      strokeColor: Colors.black,
                      strokeWidth: 3.0,
                      onDrawingUpdate: (isDrawing) {
                        if (isDrawing && !_isSigned) {
                          setState(() {
                            _isSigned = true;
                          });
                        }
                      },
                    )
                  : Image.memory(_signatureImage!),
            ),
          ),
          Container(
            padding: EdgeInsets.all(16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: () {
                    _controller.clear();
                    setState(() {
                      _isSigned = false;
                      _signatureImage = null;
                    });
                  },
                  child: Text('クリア'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red,
                  ),
                ),
                ElevatedButton(
                  onPressed: _isSigned ? _saveSignature : null,
                  child: Text('署名を確定'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.green,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _saveSignature() async {
    // 署名を画像として取得
    final signatureBytes = await _controller.toImage();
    
    setState(() {
      _signatureImage = signatureBytes;
    });
    
    // 実際のアプリでは、ここで署名画像をバックエンドに送信するなどの処理を実装
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('署名が保存されました')),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}


パフォーマンスの最適化

finger_painterを使用する際のパフォーマンス最適化のヒント:

  1. キャンバスサイズ: 必要以上に大きなキャンバスを使用しないようにしましょう
  2. ストロークの合理化: 細かすぎるストロークはパフォーマンスに影響します
  3. 背景画像の最適化: 高解像度の背景画像は事前にリサイズしておきましょう
  4. 定期的な保存: 長時間の描画セッションでは定期的に進捗を保存しましょう
  5. コントローラーの適切な破棄: 使用後は必ずdispose()を呼び出しましょう

さまざまな活用シーン

finger_painterはさまざまなアプリケーションで活用できます:

子供向け教育アプリ

  • お絵描き機能
  • 文字の練習
  • パズルやゲーム要素との組み合わせ

ビジネスアプリ

  • 電子署名の収集
  • 文書への注釈付け
  • フィールドワークでのスケッチ

クリエイティブアプリ

  • デザインスケッチ
  • メモやアイデアの視覚化
  • 写真への落書き機能

医療・健康アプリ

  • 患者による症状の視覚的な表現
  • リハビリテーション練習
  • 医療記録への注釈

まとめ

finger_painterは、Flutterアプリにシンプルかつ高機能な描画機能を簡単に実装できるパッケージです。

このパッケージが特に役立つケース:

  • タッチスクリーンを活用した直感的な入力が必要なアプリ
  • サイン収集や手書きメモ機能を実装したい場合
  • 子供向けアプリにお絵描き機能を追加したい場合
  • ゼロから描画機能を実装する時間とリソースを節約したい場合

シンプルなAPIと豊富なカスタマイズオプションで、ユーザーにとって使いやすく、開発者にとっても実装しやすい描画機能を提供します。Flutterアプリに指先で描画する機能を追加したいなら、ぜひfinger_painterを試してみてください。

タイトルとURLをコピーしました