Flutter高性能相机功能插件flutter_better_camera的使用
Flutter高性能相机功能插件flutter_better_camera的使用
Better Camera Plugin
(注意这是一款官方相机插件的分叉版本,您可以自由使用直到它支持缺失的功能。这里是官方插件 这里)
(请注意,这款插件目前尚不稳定,但我们正在积极开发此插件)
一个允许访问设备摄像头的 Flutter 插件。
Dart 包
https://pub.flutter-io.cn/packages/flutter_better_camera
特性
- 在小部件中显示实时相机预览。
- 可以捕获快照并保存到文件。
- 录制视频。
- 从 Dart 访问图像流。
- 闪光灯控制。
- 放大缩小控制。
- 自动曝光开关。
- 自动对焦开关。
安装
克隆此仓库并在 flutter
的 pubspec.yaml
文件中添加依赖项。
iOS
在 ios/Runner/Info.plist
中添加两行:
- 一行带有键
Privacy - Camera Usage Description
和描述。 - 一行带有键
Privacy - Microphone Usage Description
和描述。
或者以文本格式添加以下键:
<key>NSCameraUsageDescription</key>
<string>Can I use the camera please?</string>
<key>NSMicrophoneUsageDescription</key>
<string>Can I use the mic please?</string>
Android
在 android/app/build.gradle
文件中将最小 Android SDK 版本更改为 21 或更高版本。
修改 AndroidManifest.xml
:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FLASHLIGHT" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.front" />
<uses-feature android:name="android.hardware.camera2" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
minSdkVersion 21
此仓库中有一个用于相机的示例。
下一步
- 点击聚焦
- 连拍模式
- 控制相机预览比例
- 优化图像质量
- 控制白平衡
请随意添加问题并贡献您的力量。
示例代码
以下是完整的示例代码:
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:io';
import 'package:flutter_better_camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart';
class CameraExampleHome extends StatefulWidget {
[@override](/user/override)
_CameraExampleHomeState createState() {
return _CameraExampleHomeState();
}
}
/// 返回一个适合镜头方向的相机图标。
IconData getCameraLensIcon(CameraLensDirection? direction) {
switch (direction) {
case CameraLensDirection.back:
return Icons.camera_rear;
case CameraLensDirection.front:
return Icons.camera_front;
case CameraLensDirection.external:
return Icons.camera;
}
throw ArgumentError('Unknown lens direction');
}
void logError(String code, String? message) =>
print('Error: $code\nError Message: $message');
class _CameraExampleHomeState extends State<CameraExampleHome>
with WidgetsBindingObserver {
CameraController? controller;
String? imagePath;
late String videoPath;
VideoPlayerController? videoController;
late VoidCallback videoPlayerListener;
bool enableAudio = true;
FlashMode flashMode = FlashMode.off;
[@override](/user/override)
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
}
[@override](/user/override)
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
[@override](/user/override)
void didChangeAppLifecycleState(AppLifecycleState state) {
// 应用状态发生变化时,如果控制器尚未初始化,则返回。
if (controller == null || !controller!.value.isInitialized!) {
return;
}
if (state == AppLifecycleState.inactive) {
controller?.dispose();
} else if (state == AppLifecycleState.resumed) {
if (controller != null) {
onNewCameraSelected(controller!.description);
}
}
}
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('相机示例'),
),
body: Column(
children: <Widget>[
Expanded(
child: Container(
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Center(
child: ZoomableWidget(
child: _cameraPreviewWidget(),
onTapUp: (scaledPoint) {
//controller.setPointOfInterest(scaledPoint);
},
onZoom: (zoom) {
print('zoom');
if (zoom < 11) {
controller!.zoom(zoom);
}
})),
),
decoration: BoxDecoration(
color: Colors.black,
border: Border.all(
color: controller != null && controller!.value.isRecordingVideo!
? Colors.redAccent
: Colors.grey,
width: 3.0,
),
),
),
),
_captureControlRowWidget(),
_toggleAudioWidget(),
Padding(
padding: const EdgeInsets.all(5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
_cameraTogglesRowWidget(),
_thumbnailWidget(),
],
),
),
],
),
);
}
/// 显示相机预览或一条消息(如果预览不可用)。
Widget _cameraPreviewWidget() {
if (controller == null || !controller!.value.isInitialized!) {
return const Text(
'点击选择相机',
style: TextStyle(
color: Colors.white,
fontSize: 24.0,
fontWeight: FontWeight.w900,
),
);
} else {
return AspectRatio(
aspectRatio: controller!.value.aspectRatio,
child: CameraPreview(controller!),
);
}
}
/// 切换录制音频
Widget _toggleAudioWidget() {
return Padding(
padding: const EdgeInsets.only(left: 25),
child: Row(
children: <Widget>[
const Text('启用音频:'),
Switch(
value: enableAudio,
onChanged: (bool value) {
enableAudio = value;
if (controller != null) {
onNewCameraSelected(controller!.description);
}
},
),
],
),
);
}
/// 显示捕获的图像或视频缩略图。
Widget _thumbnailWidget() {
return Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
videoController == null && imagePath == null
? Container()
: SizedBox(
child: (videoController == null)
? Image.file(File(imagePath!))
: Container(
child: Center(
child: AspectRatio(
aspectRatio:
videoController!.value.size != null
? videoController!.value.aspectRatio
: 1.0,
child: VideoPlayer(videoController!)),
),
decoration: BoxDecoration(
border: Border.all(color: Colors.pink)),
),
width: 64.0,
height: 64.0,
),
],
),
),
);
}
/// 显示控制栏,包含拍摄照片和录制视频的按钮。
Widget _captureControlRowWidget() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
IconButton(
icon: const Icon(Icons.camera_alt),
color: Colors.blue,
onPressed: controller != null &&
controller!.value.isInitialized! &&
!controller!.value.isRecordingVideo!
? onTakePictureButtonPressed
: null,
),
IconButton(
icon: const Icon(Icons.videocam),
color: Colors.blue,
onPressed: controller != null &&
controller!.value.isInitialized! &&
!controller!.value.isRecordingVideo!
? onVideoRecordButtonPressed
: null,
),
IconButton(
icon: controller != null && controller!.value.isRecordingPaused
? Icon(Icons.play_arrow)
: Icon(Icons.pause),
color: Colors.blue,
onPressed: controller != null &&
controller!.value.isInitialized! &&
controller!.value.isRecordingVideo!
? (controller != null && controller!.value.isRecordingPaused
? onResumeButtonPressed
: onPauseButtonPressed)
: null,
),
IconButton(
icon: controller != null && controller!.value.autoFocusEnabled!
? Icon(Icons.access_alarm)
: Icon(Icons.access_alarms),
color: Colors.blue,
onPressed: (controller != null && controller!.value.isInitialized!)
? toogleAutoFocus
: null,
),
_flashButton(),
IconButton(
icon: const Icon(Icons.stop),
color: Colors.red,
onPressed: controller != null &&
controller!.value.isInitialized! &&
controller!.value.isRecordingVideo!
? onStopButtonPressed
: null,
),
],
);
}
/// 闪光灯切换按钮
Widget _flashButton() {
IconData iconData = Icons.flash_off;
Color color = Colors.black;
if (flashMode == FlashMode.alwaysFlash) {
iconData = Icons.flash_on;
color = Colors.blue;
} else if (flashMode == FlashMode.autoFlash) {
iconData = Icons.flash_auto;
color = Colors.red;
}
return IconButton(
icon: Icon(iconData),
color: color,
onPressed: controller != null && controller!.value.isInitialized!
? _onFlashButtonPressed
: null,
);
}
/// 切换闪光灯
Future<void> _onFlashButtonPressed() async {
bool hasFlash = false;
if (flashMode == FlashMode.off || flashMode == FlashMode.torch) {
// 打开闪光灯
flashMode = FlashMode.alwaysFlash;
} else if (flashMode == FlashMode.alwaysFlash) {
// 如果需要打开闪光灯
flashMode = FlashMode.autoFlash;
} else {
// 关闭闪光灯
flashMode = FlashMode.off;
}
// 应用新模式
await controller!.setFlashMode(flashMode);
// 更改UI状态
setState(() {});
}
/// 显示可以选择的相机的行控件。
Widget _cameraTogglesRowWidget() {
final List<Widget> toggles = [];
if (cameras.isEmpty) {
return const Text('未找到相机');
} else {
for (CameraDescription cameraDescription in cameras) {
toggles.add(
SizedBox(
width: 90.0,
child: RadioListTile<CameraDescription>(
title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
groupValue: controller?.description,
value: cameraDescription,
onChanged: controller != null && controller!.value.isRecordingVideo!
? null
: onNewCameraSelected,
),
),
);
}
}
return Row(children: toggles);
}
String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
void showInSnackBar(String message) {
_scaffoldKey.currentState!.showSnackBar(SnackBar(content: Text(message)));
}
void onNewCameraSelected(CameraDescription? cameraDescription) async {
if (controller != null) {
await controller!.dispose();
}
controller = CameraController(
cameraDescription!,
ResolutionPreset.medium,
enableAudio: enableAudio,
);
// 如果控制器被更新,则更新UI。
controller!.addListener(() {
if (mounted) setState(() {});
if (controller!.value.hasError) {
showInSnackBar('相机错误 ${controller!.value.errorDescription}');
}
});
try {
await controller!.initialize();
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
}
void onTakePictureButtonPressed() {
takePicture().then((String? filePath) {
if (mounted) {
setState(() {
imagePath = filePath;
videoController?.dispose();
videoController = null;
});
if (filePath != null) showInSnackBar('照片已保存到 $filePath');
}
});
}
void onVideoRecordButtonPressed() {
startVideoRecording().then((String? filePath) {
if (mounted) setState(() {});
if (filePath != null) showInSnackBar('正在保存视频到 $filePath');
});
}
void onStopButtonPressed() {
stopVideoRecording().then((_) {
if (mounted) setState(() {});
showInSnackBar('视频已记录到: $videoPath');
});
}
void onPauseButtonPressed() {
pauseVideoRecording().then((_) {
if (mounted) setState(() {});
showInSnackBar('视频录制已暂停');
});
}
void onResumeButtonPressed() {
resumeVideoRecording().then((_) {
if (mounted) setState(() {});
showInSnackBar('视频录制已恢复');
});
}
void toogleAutoFocus() {
controller!.setAutoFocus(!controller!.value.autoFocusEnabled!);
showInSnackBar('切换自动对焦');
}
Future<String?> startVideoRecording() async {
if (!controller!.value.isInitialized!) {
showInSnackBar('错误:请选择相机');
return null;
}
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/Movies/flutter_test';
await Directory(dirPath).create(recursive: true);
final String filePath = '$dirPath/${timestamp()}.mp4';
if (controller!.value.isRecordingVideo!) {
// 正在录制视频,无需操作。
return null;
}
try {
videoPath = filePath;
await controller!.startVideoRecording(filePath);
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
return filePath;
}
Future<void> stopVideoRecording() async {
if (!controller!.value.isRecordingVideo!) {
return null;
}
try {
await controller!.stopVideoRecording();
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
await _startVideoPlayer();
}
Future<void> pauseVideoRecording() async {
if (!controller!.value.isRecordingVideo!) {
return null;
}
try {
await controller!.pauseVideoRecording();
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> resumeVideoRecording() async {
if (!controller!.value.isRecordingVideo!) {
return null;
}
try {
await controller!.resumeVideoRecording();
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> _startVideoPlayer() async {
final VideoPlayerController vcontroller =
VideoPlayerController.file(File(videoPath));
videoPlayerListener = () {
if (videoController != null && videoController!.value.size != null) {
// 刷新状态以更新视频播放器。
if (mounted) setState(() {});
videoController!.removeListener(videoPlayerListener);
}
};
vcontroller.addListener(videoPlayerListener);
await vcontroller.setLooping(true);
await vcontroller.initialize();
await videoController?.dispose();
if (mounted) {
setState(() {
imagePath = null;
videoController = vcontroller;
});
}
await vcontroller.play();
}
Future<String?> takePicture() async {
if (!controller!.value.isInitialized!) {
showInSnackBar('错误:请选择相机');
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;
}
try {
await controller!.takePicture(filePath);
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
return filePath;
}
void _showCameraException(CameraException e) {
logError(e.code, e.description);
showInSnackBar('错误:${e.code}\n${e.description}');
}
}
class CameraApp extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return
MaterialApp(
theme: ThemeData(
accentTextTheme: TextTheme(body2: TextStyle(color: Colors.white)),
),
home: CameraExampleHome(),
);
}
}
List<CameraDescription> cameras = [];
Future<void> main() async {
// 初始化应用之前获取可用的摄像头。
try {
WidgetsFlutterBinding.ensureInitialized();
cameras = await availableCameras();
} on CameraException catch (e) {
logError(e.code, e.description);
}
runApp(CameraApp());
}
// 缩放控件,将是一个单独的组件
class ZoomableWidget extends StatefulWidget {
final Widget? child;
final Function? onZoom;
final Function? onTapUp;
const ZoomableWidget({Key? key, this.child, this.onZoom, this.onTapUp})
: super(key: key);
[@override](/user/override)
_ZoomableWidgetState createState() => _ZoomableWidgetState();
}
class _ZoomableWidgetState extends State<ZoomableWidget> {
Matrix4 matrix = Matrix4.identity();
double zoom = 1;
double prevZoom = 1;
bool showZoom = false;
Timer? t1;
bool handleZoom(newZoom){
if (newZoom >= 1) {
if (newZoom > 10) {
return false;
}
setState(() {
showZoom = true;
zoom = newZoom;
});
if (t1 != null) {
t1!.cancel();
}
t1 = Timer(Duration(milliseconds: 2000), () {
setState(() {
showZoom = false;
});
});
}
widget.onZoom!(zoom);
return true;
}
[@override](/user/override)
Widget build(BuildContext context) {
return GestureDetector(
onScaleStart: (scaleDetails) {
print('scalStart');
setState(() => prevZoom = zoom);
//print(scaleDetails);
},
onScaleUpdate: (ScaleUpdateDetails scaleDetails) {
var newZoom = (prevZoom * scaleDetails.scale);
handleZoom(newZoom);
},
onScaleEnd: (scaleDetails) {
print('end');
//print(scaleDetails);
},
onTapUp: (TapUpDetails det) {
final RenderBox box = context.findRenderObject() as RenderBox;
final Offset localPoint = box.globalToLocal(det.globalPosition);
final Offset scaledPoint =
localPoint.scale(1 / box.size.width, 1 / box.size.height);
// TODO 实现
// widget.onTapUp(scaledPoint);
},
child: Stack(children: [
Column(
children: <Widget>[
Container(
child: Expanded(
child: widget.child!,
),
),
],
),
Visibility(
visible: showZoom, //默认为true
child: Positioned.fill(
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Align(
alignment: Alignment.bottomCenter,
child:
SliderTheme(
data: SliderTheme.of(context).copyWith(
valueIndicatorTextStyle: TextStyle(
color: Colors.amber, letterSpacing: 2.0, fontSize: 30),
valueIndicatorColor: Colors.blue,
// 这是你需要的
inactiveTrackColor: Color(0xFF8D8E98),
// 自定义灰色
activeTrackColor: Colors.white,
thumbColor: Colors.red,
overlayColor: Color(0x29EB1555),
// 自定义拇指覆盖颜色
thumbShape:
RoundSliderThumbShape(enabledThumbRadius: 12.0),
overlayShape:
RoundSliderOverlayShape(overlayRadius: 20.0),
),
child: Slider(
value: zoom,
onChanged: (double newValue) {
handleZoom(newValue);
},
label: "$zoom",
min: 1,
max: 10,
),
),
),
],
)),
),
//maintainSize: bool. 当为真时,相当于invisible;
//replacement: Widget. 默认为Sizedbox.shrink,0x0
)
]));
}
}
更多关于Flutter高性能相机功能插件flutter_better_camera的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter高性能相机功能插件flutter_better_camera的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,下面是一个关于如何使用Flutter高性能相机功能插件flutter_better_camera
的代码示例。这个插件提供了对相机功能的更高级和更灵活的访问。
首先,确保你已经在pubspec.yaml
文件中添加了flutter_better_camera
依赖项:
dependencies:
flutter:
sdk: flutter
flutter_better_camera: ^x.y.z # 替换为最新版本号
然后,运行flutter pub get
来获取依赖项。
接下来,在你的Flutter应用中实现相机功能。下面是一个基本的实现示例:
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_better_camera/flutter_better_camera.dart';
import 'package:flutter_better_camera_example/camera_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Better Camera Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CameraScreen(),
);
}
}
camera_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_better_camera/flutter_better_camera.dart';
import 'package:flutter_better_camera/flutter_better_camera_controller.dart';
class CameraScreen extends StatefulWidget {
@override
_CameraScreenState createState() => _CameraScreenState();
}
class _CameraScreenState extends State<CameraScreen> {
late FlutterBetterCameraController _controller;
@override
void initState() {
super.initState();
_controller = FlutterBetterCameraController(
cameraId: 0, // 默认使用后置摄像头,0为后置,1为前置
enableAudio: true,
enableVideoCapture: true,
enablePhotoCapture: true,
enablePreview: true,
flashMode: FlashMode.off,
orientation: CameraOrientation.portrait,
previewAspectRatio: 16 / 9,
previewResolution: PreviewResolution.high,
videoResolution: VideoResolution.high,
photoResolution: PhotoResolution.high,
photoFormat: PhotoFormat.jpeg,
);
_controller.initialize().then((_) {
if (mounted) {
setState(() {});
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Better Camera'),
),
body: Stack(
children: [
if (_controller.isPreviewActive)
FlutterBetterCameraPreview(
controller: _controller,
),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () async {
final result = await _controller.capturePhoto();
if (result != null) {
// 处理拍照结果
print('Photo path: ${result.path}');
}
},
child: Text('Capture Photo'),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () async {
final result = await _controller.startVideoCapture();
if (result != null) {
// 处理录像结果
print('Video path: ${result.path}');
}
},
child: Text('Start Video Capture'),
),
],
),
],
),
],
),
);
}
}
说明
- 初始化相机控制器:在
initState
方法中,我们初始化了FlutterBetterCameraController
并设置了相机参数。 - 预览相机画面:使用
FlutterBetterCameraPreview
小部件来显示相机预览。 - 拍照和录像:提供两个按钮,分别用于拍照和录像。拍照按钮调用
capturePhoto
方法,录像按钮调用startVideoCapture
方法。
注意事项
- 请确保在
AndroidManifest.xml
和Info.plist
文件中添加必要的相机和麦克风权限。 - 根据需要调整相机参数,例如分辨率、闪光灯模式等。
- 处理拍照和录像结果时,注意文件路径的访问和权限管理。
希望这个示例能帮助你更好地理解和使用flutter_better_camera
插件。