RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

Flutterでカメラを作成してみた

f:id:tech-rakus:20200928174834p:plain

こんにちは。技術推進課のt_okkanです。

最近業務でFlutterを使用することになり、少しずつですがFlutterを学習しています。 そこで今回はFlutterで、実機(iPhone)のカメラを利用してみました。カメラを使用するためのプラグインの追加から、カメラでの画像の撮影と保存、保存した画像の表示までを行います。

環境

使用した環境は以下になります。

Flutterの環境構築は、公式HPに詳しく記載されています。

https://flutter.dev/docs/get-started/install

上記を参考に、macOS + VSCodeの環境を構築してください。

Flutterについて

Flutterについて少しだけ紹介します。Googleが提供しているUIツールキットで、主にiOSAndroidアプリのクロスプラットフォームツールとして利用されています。最近はWeb対応が進み、モバイル・ウェブ・デスクトップと真のクロスプラットフォームを目指している注目のツールです。

FlutterはDartという言語で実装されています。カメラなどのデバイス固有の機能にアクセスするには、各プラットフォームのネイティブの実装を必要とします。

Flutterではそのような各ネイティブの実装を必要とするプラグインをPluginパッケージとして提供しています。

プラグインpub.devで公開されています。

アプリ作成〜プラグインのインストール

Flutterのプロジェクトを作成して、cameraプラグインと、カメラとマイクへのアクセス許可の確認までを設定します。また、画像を保存するために各プラットフォームのパスを取得する必要があるので、path_providerpathプラグインもインストールします。

まずはコマンドでFlutterのプロジェクトを作成します。

$ flutter create camera_app

その後プラグインを追加します。Flutterではpubspec.yamlファイルに使用するプラグインを記載します。今回は以下のようにします。

dependencies:
  camera: ^0.5.8+5
  path_provider: ^1.6.14
  path: ^1.7.0
  flutter:
    sdk: flutter

次に、カメラを使用の許可を促すメッセージを表示するための設定を行います。

ios/Runner/Info.plistファイルに以下を追加します。

  <key>NSCameraUsageDescription</key>
  <string>Can I use the camera please?</string>
  <key>NSMicrophoneUsageDescription</key>
  <string>Can I use the mic please?</string>

またAndroidで使用する場合は、SDKの最低バージョンを21を指定する必要があります。

android/app/build.gradleに以下を追加します。

defaultConfig {
    // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
    applicationId "com.example.camera_app"
    minSdkVersion 21
    targetSdkVersion 29
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
}

準備は以上です。では実際にコードを書いていきます。

実装

まずはcameraプラグインで使用するクラスを紹介します。

  • CameraController

    端末のカメラを制御するクラス。このクラスに制御したいカメラを渡すことで、プレビューの表示や、写真・動画の撮影を行うことができます。

  • CameraDescription

    カメラの情報(フロント・バック)を管理しているクラス。CameraControllerクラスにこのクラスを渡すことで、カメラの制御を行うことができます。

  • CameraPreview

    カメラが取得している映像(静止画の連続)を画面に表示するウィジェット

今回はカメラで画像を撮影・保存し表示するまでの手順を順番に説明します。最後に必要なコードを全て載せますので、先に見たい方はそちらからどうぞ。

では、以下の手順で実装していきます。

  • 使用できるカメラの取得
  • カメラ制御の初期化
  • カメラのプレビューの実装
  • 画像の撮影と保存の実装
  • 撮影した画像を表示する

使用できるカメラの取得

まずは端末から使用できるカメラを取得します。

今回は取得したカメラから、背面のカメラを使用します。

  // runAppが実行される前に、cameraプラグインを初期化
  WidgetsFlutterBinding.ensureInitialized();

  // デバイスで使用可能なカメラの一覧を取得する
  final cameras = await availableCameras();

  // 利用可能なカメラの一覧から、指定のカメラを取得する
  final firstCamera = cameras.first;

プラグインavailableCamerasメソッドで、端末で使用可能なカメラを配列で取得できます。 ここで取得したカメラを、表示するウィジェットに渡すことで、カメラを使用できます。 では次に、取得したカメラを制御するための準備をします。

カメラ制御の初期化

取得したカメラの制御を行うために、CameraControllerクラスを初期化する必要があります。 ここではカメラ用の画面CameraHomeの構築から、CameraControllerクラスの初期化までを実装します。

// ①
class CameraHome extends StatefulWidget {
  final CameraDescription camera;

  const CameraHome({Key key, @required this.camera}) : super(key: key);

  @override
  State<StatefulWidget> createState() => CameraHomeState();
}

class CameraHomeState extends State<CameraHome> {
  // デバイスのカメラを制御するコントローラ
  CameraController _cameraController;
  // コントローラーに設定されたカメラを初期化する関数
  Future<void> _initializeCameraController;

  @override
  void initState() {
    super.initState();

    // ②
    // コントローラを初期化
    _cameraController = CameraController(
        // 使用するカメラをコントローラに設定
        widget.camera,
        // 使用する解像度を設定
        // low : 352x288 on iOS, 240p (320x240) on Android
        // medium : 480p (640x480 on iOS, 720x480 on Android)
        // high : 720p (1280x720)
        // veryHigh : 1080p (1920x1080)
        // ultraHigh : 2160p (3840x2160)
        // max : 利用可能な最大の解像度
        ResolutionPreset.max);
    // ③
    // コントローラーに設定されたカメラを初期化
    _initializeCameraController = _cameraController.initialize();
  }

  // ④
  @override
  void dispose() {
    // ウィジェットが破棄されたタイミングで、カメラのコントローラを破棄する
    _cameraController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {}
}
  1. カメラを起動する画面を構築する

    カメラを起動する画面としてStatefulWidgetを使用し、画面を構築します。コンストラクターCameraDescriptionクラスを受け取り、使用するカメラを設定します。

  2. カメラコントローラーを初期化します

    CameraControllerコンストラクターの第一引数にCameraDescriptionを指定し、制御対象のカメラを設定します。第二引数に、列挙型ResolutionPresetからカメラの解像度を指定します。今回は使用できる最大の解像度を指定します。

  3. カメラコントローラーに設定されたカメラを初期化します

    CameraControllerクラスに設定された端末のカメラを、initializeメソッドを実行して初期化します。initializeメソッドは非同期処理を行うメソッドのため、Featureオブジェクトを返します。

  4. ウィジェット破棄のメソッドの追加

    StatefulWidgetdisposeメソッド内で、CameraControllerクラスのdisposeメソッドを呼び出します。ウィジェットが破棄されたタイミングで、CameraControllerクラスも破棄する用になります。

以上がカメラの初期設定になります。次は、カメラが取得した画像を画面に表示するプレビュー機能を実装します。

カメラのプレビューの実装

cameraプラグインCameraPreviewクラスを利用して、カメラが取得した画像をプレビューとして画面に表示します。先ほどのCameraHomeStateクラスの、buildメソッドに以下のコードを追加します。

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(''),
      ),
      // FutureBuilderを実装
      body: FutureBuilder<void>(
        future: _initializeCameraController,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            // カメラの初期化が完了したら、プレビューを表示
            return CameraPreview(_cameraController);
          } else {
            // カメラの初期化中はインジケーターを表示
            return const Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }

FutureBuilderクラスは、非同期で画面を構築できます。引数のfutureで非同期処理を設定し、builderで非同期処理が完了した場合と完了していない場合の処理を実装します。今回は、非同期処理としてカメラの初期化を行うinitializeメソッドを実行し、カメラの初期化が完了するとプレビュー画面を、完了していない場合はインジケーターを表示するようにします。

以上がカメラのプレビューを表示するための実装です。

画像の撮影と保存の実装

次は、カメラのシャッターボタンを実装し、画像の撮影と保存を行います。カメラのシャッターボタンはFloatingActionButtonで実装します。 FloatingActionButtononPressed引数でボタンが押下された場合の実装をします。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(''),
      ),
      body: FutureBuilder<void>(
        // 省略
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.camera_alt),
        // ボタンが押下された際の処理
        onPressed: () async {
          try {
            // ①画像を保存するパスを作成する
            final path = join(
              (await getApplicationDocumentsDirectory()).path,
              '${DateTime.now()}.png',
            );

            // ②カメラで画像を撮影する
            await _cameraController.takePicture(path);

            // ③画像を表示する画面に遷移
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => CameraDisplay(imgPath: path),
              ),
            );
          } catch (e) {
            print(e);
          }
        },
      ),
  }
  1. 画像を保存するパスを作成

    pathプラグインjoinメソッドを使用して、画像を保存するパスを作成します。path_providerプラグインgetApplicationDocumentsDirectoryメソッドを使用することで、アプリ専用のディレクトリを取得できます。

  2. 写真を撮影し、作成したパスに保存

    CameraControllerクラスのtakePictureメソッドで画像の撮影を行うことができます。引数に指定したパスに画像を保存できます。

  3. 撮影した画像を表示する画面に遷移

    画像を表示する画面は次で実装します。ここでは、画面のコンストラクターに先ほど画像を保存したパスを渡します。

以上が画像の撮影から保存までの実装です。

ここまでの実装で、以下のようなカメラから取得した画像を表示し、撮影用のボタンを表示する画面が出来上がります。

f:id:ot14nano:20200927205607p:plain
カメラ撮影画面

最後に撮影ボタンを押下後、保存した画像を表示する画面を作成します。

撮影した画像を表示する

最後に、撮影し保存した画像を表示する画面を作成します。 画像を表示するためには、Imageウィジェットを使用します。コンストラクターに表示したい画像のパスを指定することで画像を表示できます。

class CameraDisplay extends StatelessWidget {
  // 表示する画像のパス
  final String imgPath;

  // 画面のコンストラクタ
  const CameraDisplay({Key key, this.imgPath}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Picture'),
      ),
      body: Column(
        // Imageウィジェットで画像を表示する
        children: [Expanded(child: Image.file(File(imgPath)))],
      )
    );
  }
}

これで以下のような画像を表示する画面を作成できます。

f:id:ot14nano:20200927223518p:plain
画像表示

実装は以上になります。全体のソースコードは以下になります。

全体のソースコード

全体のソースコード

import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' show join;
import 'package:path_provider/path_provider.dart';

Future<void> main() async {
  // runAppが実行される前に、cameraプラグインを初期化
  WidgetsFlutterBinding.ensureInitialized();

  // デバイスで使用可能なカメラの一覧を取得する
  final cameras = await availableCameras();

  // 利用可能なカメラの一覧から、指定のカメラを取得する
  final firstCamera = cameras.first;

  runApp(MyApp(camera: firstCamera));
}

class MyApp extends StatelessWidget {
  final CameraDescription camera;
  const MyApp({Key key, @required this.camera}) : super(key: key);
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(
        camera: camera,
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final CameraDescription camera;
  const MyHomePage({Key key, @required this.camera}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Camera Example'),
      ),
      body: Wrap(
        children: [
          RaisedButton(
              child: const Text('Camera'),
              onPressed: () {
                Navigator.push(context, MaterialPageRoute(builder: (context) {
                  return CameraHome(camera: camera,);
                }));
              }),
        ],
      ),
    );
  }
}

class CameraHome extends StatefulWidget {
  final CameraDescription camera;

  const CameraHome({Key key, @required this.camera}) : super(key: key);

  @override
  State<StatefulWidget> createState() => CameraHomeState();
}

class CameraHomeState extends State<CameraHome> {
  // デバイスのカメラを制御するコントローラ
  CameraController _cameraController;
  // コントローラーに設定されたカメラを初期化する関数
  Future<void> _initializeCameraController;

  @override
  void initState() {
    super.initState();

    // コントローラを初期化
    _cameraController = CameraController(
        // 使用するカメラをコントローラに設定
        widget.camera,
        // 使用する解像度を設定
        // low : 352x288 on iOS, 240p (320x240) on Android
        // medium : 480p (640x480 on iOS, 720x480 on Android)
        // high : 720p (1280x720)
        // veryHigh : 1080p (1920x1080)
        // ultraHigh : 2160p (3840x2160)
        // max : 利用可能な最大の解像度
        ResolutionPreset.max);
    // コントローラーに設定されたカメラを初期化
    _initializeCameraController = _cameraController.initialize();
  }

  @override
  void dispose() {
    // ウィジェットが破棄されたタイミングで、カメラのコントローラを破棄する
    _cameraController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(''),
      ),
      // FutureBuilderを実装
      body: FutureBuilder<void>(
        future: _initializeCameraController,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            // カメラの初期化が完了したら、プレビューを表示
            return CameraPreview(_cameraController);
          } else {
            // カメラの初期化中はインジケーターを表示
            return const Center(child: CircularProgressIndicator());
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.camera_alt),
        // ボタンが押下された際の処理
        onPressed: () async {
          try {

            // 画像を保存するパスを作成する
            final path = join(
              (await getApplicationDocumentsDirectory()).path,
              '${DateTime.now()}.png',
            );

            // カメラで画像を撮影する
            await _cameraController.takePicture(path);

            // 画像を表示する画面に遷移
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => CameraDisplay(imgPath: path),
              ),
            );
          } catch (e) {
            print(e);
          }
        },
      ),
    );
  }
}

class CameraDisplay extends StatelessWidget {
  // 表示する画像のパス
  final String imgPath;

  // 画面のコンストラクタ
  const CameraDisplay({Key key, this.imgPath}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Picture'),
      ),
      body: Column(
        // Imageウィジェットで画像を表示する
        children: [Expanded(child: Image.file(File(imgPath)))],
      )
    );
  }
}

まとめ

Flutterでカメラ機能を実装してみました。以前、Swiftでカメラ機能を実装したことがあるのですが、その時と比べ必要なクラスがプラグインですでに実装されているので、シンプルに実装することができました。 またFlutterの特徴として宣言的にUIを構築できるので、楽に開発できます。 今回はシンプルなカメラ機能だけの紹介でしたが、 GitHubにインカメの切り替えのできるコードを置いています。興味があれば確認してみてください。

参考

Copyright © RAKUS Co., Ltd. All rights reserved.