こんにちは。技術推進課のt_okkanです。
最近業務でFlutterを使用することになり、少しずつですがFlutterを学習しています。 そこで今回はFlutterで、実機(iPhone)のカメラを利用してみました。カメラを使用するためのプラグインの追加から、カメラでの画像の撮影と保存、保存した画像の表示までを行います。
環境
使用した環境は以下になります。
- 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パッケージとして提供しています。
アプリ作成〜プラグインのインストール
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にインカメの切り替えのできるコードを置いています。興味があれば確認してみてください。
参考
エンジニア中途採用サイト
ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
ご興味ありましたら是非ご確認をお願いします。
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