Flutterでカメラアプリを作る〜撮影編〜
Flutterという、ワンソースでiOSアプリもAndroidアプリもビルドできるGoogle製フレームワークがありまして、そのFlutterを使ってカメラのプレビュー・撮影・保存までのやり方をまとめました。
以下の記事では、環境設定とプレビューまでをまとめました。
ちなみに本記事の次では、保存に関してまとめてあります。
前提
- Flutter 0.11.3
- MacBookPro(Mac Mojave)環境
- Flutterは初めてさわる
撮影コード追加
package導入
まず、package.yamlにファイルパスをサポートしてくれるpath_providerを導入して、$ flutter packages getします。
name: hello_world_project
description: HelloWorldProject
version: 1.0.0+1
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
english_words: ^3.1.0
camera: ^0.2.4
path_provider: ^0.4.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
プログラムを書く
プレビュー編で行なった通りcameraモジュールの動作要件に合わせ、AndroidのminSdkVersionは21にしておきましょう。
cameraモジュールのExampleにサンプルコードが掲載されているんですが、 静止画撮影・動画撮影の両方のコードが書いてあり、ボリュームも大きいので初心者にはちょっと難易度が高いです。
また、FlutterのUIに関するコードも結構な量あるため、どこがカメラ制御用のコードなのかわからないっていう難しさもあります。
ということで、今回はFlutterでカメラを使えるようになることを目的として、
- 例外処理・UI装飾をできるだけなくして見通しをよくする
- 録画関連・サムネイル表示のコードを削除
- コメントを追加
といった対応を入れて、撮影・保存のみできるようにします。
main.dartを以下のようにしていきました。
import 'dart:async'; // 非同期処理(async/await)
import 'dart:io'; // ファイルの入出力
import 'package:camera/camera.dart'; // カメラモジュール
import 'package:flutter/material.dart'; // マテリアルデザイン
import 'package:path_provider/path_provider.dart'; // ファイルパスモジュール
List<CameraDescription> cameras; // 使用できるカメラのリスト
// ここから始まる
Future<Null> main() async {
cameras = await availableCameras();
runApp(CameraApp());
}
// 親玉のApp
class CameraApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CameraWidget(),
);
}
}
// 親玉の中身
class CameraWidget extends StatefulWidget {
@override
_CameraWidgetState createState() {
return _CameraWidgetState();
}
}
// 実際はこれがやることやる
class _CameraWidgetState extends State<CameraWidget> {
CameraController controller;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String timestamp() =>
DateTime.now().millisecondsSinceEpoch.toString(); // ファイル名にはタイムスタンプ入れる。
void showInSnackBar(String message) => _scaffoldKey.currentState
.showSnackBar(SnackBar(content: Text(message))); // SnackBarでメッセージ表示
@override
Widget build(BuildContext context) {
Scaffold sc = Scaffold(
key: _scaffoldKey,
body: Column(
children: <Widget>[
Expanded(
child: Container(
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Center(
child: _cameraPreviewWidget(), // カメラのプレビューを表示するWidget
),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
IconButton(
// カメラの撮影ボタン
icon: const Icon(Icons.camera_alt),
onPressed: controller != null && controller.value.isInitialized
? onTakePictureButtonPressed // 撮影ボタンを押された時にコールバックされる関数
: null,
),
],
)
],
),
);
// カメラのセットアップ。セットアップが終わったらもう一回buildが走るので、
// controllerがnullかどうかで処理実施有無を判定。
if (controller == null) {
setUpCamera(cameras[0]);
}
return sc;
}
/// カメラプレビューを表示するWidget
Widget _cameraPreviewWidget() {
if (controller == null || !controller.value.isInitialized) {
// カメラの準備ができるまではテキストを表示
return const Text('Tap a camera');
} else {
// 準備ができたらプレビュー表示
return AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: CameraPreview(controller),
);
}
}
// カメラを準備する
void setUpCamera(CameraDescription cameraDescription) async {
if (controller != null) {
await controller.dispose();
}
controller = CameraController(cameraDescription, ResolutionPreset.high);
// カメラの情報が更新されたら呼ばれるリスナー設定
controller.addListener(() {
if (mounted) setState(() {}); // 準備終わったらbuildし直す。
if (controller.value.hasError) {
showInSnackBar('Camera error ${controller.value.errorDescription}');
}
});
await controller.initialize();
if (mounted) {
setState(() {});
}
}
// 撮影ボタンが押されたら撮影して、画像を保存する
void onTakePictureButtonPressed() {
takePicture().then((String filePath) {
if (mounted) {
setState(() {});
if (filePath != null) showInSnackBar('Picture saved to $filePath');
}
});
}
// 画像保存処理
Future<String> takePicture() async {
if (!controller.value.isInitialized) {
return null;
}
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/Pictures/flutter_test';
await Directory(dirPath).create(recursive: true);
final String filePath = '$dirPath/${timestamp()}.jpg';
if (controller.value.isTakingPicture) {
return null;
}
await controller.takePicture(filePath);
return filePath;
}
}
実機で実行
実機で実行すると、カメラプレビューと撮影ボタンがこんな感じで表示されます。

課題
撮影してみるとわかるんですが、iOSもAndroidもtakePictureのなかで保存先ファイルパスをgetApplicationDocumentsDirectoryで取得していて、アプリ領域に画像を保存しているためため、ギャラリーアプリや写真アプリで撮影画像を見ることができません。
次回はギャラリーアプリや写真アプリで見れる領域に保存する方法をまとめます。

Android/iOSクロス開発フレームワーク Flutter入門
- 作者: 掌田津耶乃
- 出版社/メーカー: 秀和システム
- 発売日: 2018/09/14
- メディア: 単行本
- この商品を含むブログを見る