🤳
Flutterでシンプルなカメラプレビュー
Flutterアプリでカメラを使って試したいことがあると思います。その第一歩として、まずはカメラプレビューするだけのコードを載せます。Androidで動作確認済みです。
おことわり:現状、カメラのアスペクト比とプレビューとの関係について修正の余地があり、ややぺちゃんこに見えます。また拡大気味にプレビューされます。対応できたら更新します。
pubspec.yaml
pubspec.yaml
dependencies:
flutter:
sdk: flutter
camera: ^0.11.1
AndroidManifest.xmlに権限を追加
manifestタグ直下に追加。
<!-- 必要な権限 -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- カメラ機能を使用 -->
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
applicationタグには下記を追加。
android:requestLegacyExternalStorage="true"
全体は下記の通り。
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 必要な権限 -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- カメラ機能を使用 -->
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<application
android:label="hello_camera"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"> <!-- Android10以下も対象の場合 -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
main.dart
import 'dart:math' as math;
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
final kTITLE = 'Hello Camera';
late List<CameraDescription> _cameras;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
_cameras = await availableCameras();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: kTITLE,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: HelloCameraScreen(
title: kTITLE,
),
);
}
}
class HelloCameraScreen extends StatefulWidget {
const HelloCameraScreen({super.key, required this.title});
final String title;
@override
State<HelloCameraScreen> createState() => _HelloCameraScreenState();
}
class _HelloCameraScreenState extends State<HelloCameraScreen> {
late CameraController _cameraController;
late List<CameraDescription> cameras;
@override
void initState() {
super.initState();
_initCamera();
}
Future<void> _initCamera() async {
_cameraController = CameraController(_cameras.first, ResolutionPreset.max);
await _cameraController.initialize().then((_) {
if (!mounted) return;
setState(() {
// 状態を更新
});
}).catchError((Object e) {
if (e is CameraException) {
switch (e.code) {
case 'CameraAccessDenied':
// Handle access errors here.
default:
// Handle other errors here.x
}
}
});
}
@override
void dispose() {
_cameraController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_cameraController.value.isInitialized) {
return Container(
color: Colors.lightBlue,
);
}
// 画面サイズを取得
final screenSize = MediaQuery.of(context).size;
final screenAspectRatio = screenSize.width / screenSize.height;
final cameraAspectRatio = _cameraController.value.aspectRatio;
// 画面とカメラのアスペクト比の差を考慮して拡大率を決定
double scale = 1.0;
if (screenAspectRatio > cameraAspectRatio) {
// 画面がカメラより横長 → 縦を拡大
scale = screenAspectRatio / cameraAspectRatio;
} else {
// 画面がカメラより縦長 → 横を拡大
scale = cameraAspectRatio / screenAspectRatio;
}
// 画面の向きに応じて回転角度を設定
double rotationAngle = 0;
switch (_cameraController.description.sensorOrientation) {
case 90:
rotationAngle = math.pi / 2; // 90度回転
break;
case 270:
rotationAngle = -math.pi / 2; // -90度回転
break;
}
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Transform.scale(
scale: scale,
child: Transform.rotate(
angle: rotationAngle,
child: CameraPreview(_cameraController),
),
),
));
}
}
下記を追加しないとビルドが通りませんでした。
android/app/proguard-rules.pro
android.defaults.buildfeatures.namespaces=true
Discussion