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
- メディア: 単行本
- この商品を含むブログを見る