こんにちは。技術推進課のt_okkanです。 最近業務でFlutterを使用することになり、少しずつですがFlutterを学習しています。 そこで今回はFlutterで、実機( iPhone )のカメラを利用してみました。カメラを使用するための プラグイン の追加から、カメラでの画像の撮影と保存、保存した画像の表示までを行います。 環境 Flutterについて アプリ作成〜プラグインのインストール 実装 使用できるカメラの取得 カメラ制御の初期化 カメラのプレビューの実装 画像の撮影と保存の実装 撮影した画像を表示する 全体のソースコード まとめ 参考 環境 使用した環境は以下になります。 Flutter 1.22.0 Visual Studio Code macOS Catalina 10.15.6 iOS 13.7 Flutterの環境構築は、公式HPに詳しく記載されています。 https://flutter.dev/docs/get-started/install 上記を参考に、 macOS + VSCode の環境を構築してください。 Flutterについて Flutterについて少しだけ紹介します。 Google が提供しているUIツールキットで、主に iOS ・ Android アプリの クロスプラットフォーム ツールとして利用されています。最近はWeb対応が進み、モバイル・ウェブ・デスクトップと真の クロスプラットフォーム を目指している注目のツールです。 Flutterは Dart という言語で実装されています。カメラなどのデ バイス 固有の機能にアクセスするには、各プラットフォームのネイティブの実装を必要とします。 Flutterではそのような各ネイティブの実装を必要とする プラグイン をPluginパッケージとして提供しています。 プラグイン は pub.dev で公開されています。 アプリ作成〜 プラグイン のインストール Flutterのプロジェクトを作成して、 camera の プラグイン と、カメラとマイクへのアクセス許可の確認までを設定します。また、画像を保存するために各プラットフォームのパスを取得する必要があるので、 path_provider と path プラグイン もインストールします。 まずはコマンドで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) {} } カメラを起動する画面を構築する カメラを起動する画面として StatefulWidget を使用し、画面を構築します。 コンストラクター で CameraDescription クラスを受け取り、使用するカメラを設定します。 カメラコントローラーを初期化します CameraController の コンストラクター の第一引数に CameraDescription を指定し、制御対象のカメラを設定します。第二引数に、列挙型 ResolutionPreset からカメラの解像度を指定します。今回は使用できる最大の解像度を指定します。 カメラコントローラーに設定されたカメラを初期化します CameraController クラスに設定された端末のカメラを、 initialize メソッドを実行して初期化します。 initialize メソッドは非同期処理を行うメソッドのため、 Feature オブジェクトを返します。 ウィジェット 破棄のメソッドの追加 StatefulWidget の dispose メソッド内で、 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 で実装します。 FloatingActionButton の onPressed 引数でボタンが押下された場合の実装をします。 @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); } }, ), } 画像を保存するパスを作成 path プラグイン の join メソッドを使用して、画像を保存するパスを作成します。 path_provider プラグイン の getApplicationDocumentsDirectory メソッドを使用することで、アプリ専用の ディレクト リを取得できます。 写真を撮影し、作成したパスに保存 CameraController クラスの takePicture メソッドで画像の撮影を行うことができます。引数に指定したパスに画像を保存できます。 撮影した画像を表示する画面に遷移 画像を表示する画面は次で実装します。ここでは、画面の コンストラクター に先ほど画像を保存したパスを渡します。 以上が画像の撮影から保存までの実装です。 ここまでの実装で、以下のようなカメラから取得した画像を表示し、撮影用のボタンを表示する画面が出来上がります。 カメラ撮影画面 最後に撮影ボタンを押下後、保存した画像を表示する画面を作成します。 撮影した画像を表示する 最後に、撮影し保存した画像を表示する画面を作成します。 画像を表示するためには、 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)))], ) ); } } これで以下のような画像を表示する画面を作成できます。 画像表示 実装は以上になります。全体の ソースコード は以下になります。 全体の ソースコード 全体のソースコード 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 にインカメの切り替えのできるコードを置いています。興味があれば確認してみてください。 参考 Take a picture using the camera | Flutter エンジニア 中途採用 サイト ラク スでは、エンジニア・デザイナーの 中途採用 を積極的に行っております! ご興味ありましたら是非ご確認をお願いします。 https://career-recruit.rakus.co.jp/career_engineer/ カジュアル面談お申込みフォーム どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。 以下フォームよりお申込みください。 rakus.hubspotpagebuilder.com ラク スDevelopers登録フォーム https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/ イベント情報 会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! ◆TECH PLAY techplay.jp ◆connpass rakus.connpass.com