深入理解Flutter相机插件【Flutter专题22】
前面给大家讲解http包和dio包,涉及到一个知识点就是上传图片,那么图片哪里来?
每个移动设备都带有一个内置的相机应用程序,用于捕捉图片、录制视频以及一些特定于每个设备的吸引人的功能。但是如果你正在开发一个需要相机访问的应用程序,那么你必须自己实现相机功能。
你可能会问,当默认的相机应用程序已经可用时,为什么我需要再次实现相机功能?
答案是因为,如果您想提供适合您的应用的独特用户界面,或者添加设备默认相机应用中不存在的功能,那么它是必需的。
在本文中,您将学习使用支持 Android 和 iOS 平台的官方为 Flutter 应用程序实现基本的相机功能。
应用概览
在深入研究代码之前,让我们回顾一下我们将要构建的应用程序。最终的应用程序将包含大部分基本的相机功能,包括:
捕获清晰度选择器
变焦控制
曝光控制
闪光模式选择器
翻转摄像头的按钮——后摄像头到前摄像头,反之亦然
用于捕获图像的按钮
用于从图像模式切换到视频模式的切换
视频模式控制——开始、暂停、恢复、停止
上次捕获的图像或视频预览
检索图像/视频文件
先看一眼最后的效果图。

入门
使用以下命令创建一个新的 Flutter 项目:
flutter create flutter_camera_demo
您可以使用自己喜欢的 IDE 打开项目,但在本示例中,我将使用 VS Code:
code flutter_camera_demo
将以下依赖项添加到您的文件中:
path_provider
:用于将图像或视频存储在目录中,可以轻松访问它们
dependencies: camera: ^0.8.1+7 videoplayer: ^2.1.14 path_provider: ^2.0.2
使用camera如果报错的话
尝试将 compileSdkVersion 和 targetSdkVersion 更新为 31。

将文件内容替换为以下内容:
import 'package:flutter/material.dart';
import 'screens/camera_screen.dart';
Future main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: CameraScreen(),
);
}
}
该将包含所有的相机功能的代码与它的用户界面一起。我们将稍后添加它,但在我们这样做之前,我们必须在设备上安装可用的摄像头。
检索可用的相机
在main.dart文件中,定义一个全局变量,称为我们将存储可用摄像机列表的位置。这将有助于我们以后轻松引用它们。
import 'package:camera/camera.dart';
List cameras = [];
您可以在使用该方法初始化应用程序之前检索函数内部的相机——只需确保该函数是异步的,因为它必须等待检索设备的可用相机,并且通常 Flutter 的函数是一个简单的函数,只有调用:
Future main() async {
try {
WidgetsFlutterBinding.ensureInitialized();
cameras = await availableCameras();
} on CameraException catch (e) {
print('Error in fetching the cameras: $e');
}
runApp(MyApp());
}
初始化相机
创建一个名为camera_screen.dart的新文件并在其中定义有状态小部件CameraScreen。
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import '../main.dart';
class CameraScreen extends StatefulWidget {
@override
_CameraScreenState createState() => _CameraScreenState();
}
class _CameraScreenState extends State {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
为相机定义一个控制器,为布尔变量定义一个值,您可以使用它来轻松了解相机是否已初始化并相应地刷新 UI:
class _CameraScreenState extends State {
CameraController? controller;
bool _isCameraInitialized = false;
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
控制器将帮助您访问相机的不同功能,但在使用它们之前,您必须初始化相机。
创建一个名为 的新方法。此方法将有助于处理两种情况:
class _CameraScreenState extends State {
// ...
void onNewCameraSelected(CameraDescription cameraDescription) async {
final previousCameraController = controller;
// Instantiating the camera controller
final CameraController cameraController = CameraController(
cameraDescription,
ResolutionPreset.high,
imageFormatGroup: ImageFormatGroup.jpeg,
);
// Dispose the previous controller
await previousCameraController?.dispose();
// Replace with the new controller
if (mounted) {
setState(() {
controller = cameraController;
});
}
// Update UI if controller updated
cameraController.addListener(() {
if (mounted) setState(() {});
});
// Initialize controller
try {
await cameraController.initialize();
} on CameraException catch (e) {
print('Error initializing camera: $e');
}
// Update the boolean
if (mounted) {
setState(() {
_isCameraInitialized = controller!.value.isInitialized;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
在initState()
索引的名单-后置摄像头
索引的名单-前置摄像头
lass _CameraScreenState extends State {
// ...
@override
void initState() {
onNewCameraSelected(cameras[0]);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
另外,不要忘记在相机未激活时释放方法中的内存:
@override
void dispose() {
controller?.dispose();
super.dispose();
}
处理相机生命周期状态
在任何设备上运行相机都被认为是一项占用大量内存的任务,因此如何处理释放内存资源以及何时释放内存资源非常重要。应用程序的生命周期状态有助于了解状态变化,以便您作为开发人员可以做出相应的反应。
在 Flutter 中,您可以通过添加 mixin 并管理生命周期更改。
class _CameraScreenState extends State
with WidgetsBindingObserver {
// ...
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final CameraController? cameraController = controller;
// App state changed before we got the chance to initialize.
if (cameraController == null || !cameraController.value.isInitialized) {
return;
}
if (state == AppLifecycleState.inactive) {
// Free up memory when camera not active
cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
// Reinitialize the camera with same properties
onNewCameraSelected(cameraController.description);
}
}
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
添加相机预览
现在我们已经完成了相机状态的初始化和管理,我们可以定义一个非常基本的用户界面来预览相机输出。
Flutter 的camera 插件自带一个方法,调用显示camera 输出,用户界面可以定义如下:
class _CameraScreenState extends State
with WidgetsBindingObserver {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
body: _isCameraInitialized
? AspectRatio(
aspectRatio: 1 / controller!.value.aspectRatio,
child: controller!.buildPreview(),
)
: Container(),
);
}
}
预览将如下所示:

您会注意到设备状态栏在顶部可见;您可以通过在方法中添加以下内容来隐藏它以防止它阻碍相机视图:
@override
void initState() {
// Hide the status bar
SystemChrome.setEnabledSystemUIOverlays([]);
onNewCameraSelected(cameras[0]);
super.initState();
}
基本的相机预览已准备就绪!现在,我们可以开始向相机添加功能。
添加清晰度选择器
您可以使用来定义相机视图的清晰度。在初始化相机时,我们使用了.
要更改相机视图的清晰度,您必须使用新值重新初始化相机控制器。我们将在相机视图的右上角添加一个下拉菜单,用户可以在其中选择分辨率预设。
在类中添加两个变量,一个用于保存所有值,另一个用于存储值。
final resolutionPresets = ResolutionPreset.values;
ResolutionPreset currentResolutionPreset = ResolutionPreset.high;
修改方法中的相机控制器实例以使用该变量:
final CameraController cameraController = CameraController(
cameraDescription,
currentResolutionPreset,
imageFormatGroup: ImageFormatGroup.jpeg,
);
在可被定义为如下:
DropdownButton(
dropdownColor: Colors.black87,
underline: Container(),
value: currentResolutionPreset,
items: [
for (ResolutionPreset preset
in resolutionPresets)
DropdownMenuItem(
child: Text(
preset
.toString()
.split('.')[1]
.toUpperCase(),
style:
TextStyle(color: Colors.white),
),
value: preset,
)
],
onChanged: (value) {
setState(() {
currentResolutionPreset = value!;
_isCameraInitialized = false;
});
onNewCameraSelected(controller!.description);
},
hint: Text("Select item"),
)
调用该方法以使用新的清晰度值重新初始化相机控制器。
录制的图片有点大,大家下载预览吧
变焦控制
您可以使用控制器上的方法并传递缩放值来设置相机的缩放级别。
在确定缩放级别之前,您应该知道设备相机的最小和最大缩放级别。
定义三个变量:
double _minAvailableZoom = 1.0;
double _maxAvailableZoom = 1.0;
double _currentZoomLevel = 1.0;
检索这些值的最佳位置是在相机初始化后的方法内部。您可以使用以下方法获得最小和最大缩放级别:
cameraController
.getMaxZoomLevel()
.then((value) => _maxAvailableZoom = value);
cameraController
.getMinZoomLevel()
.then((value) => _minAvailableZoom = value);
您可以实现一个滑块,让用户选择合适的缩放级别;构建的代码如下:
Row(
children: [
Expanded(
child: Slider(
value: _currentZoomLevel,
min: _minAvailableZoom,
max: _maxAvailableZoom,
activeColor: Colors.white,
inactiveColor: Colors.white30,
onChanged: (value) async {
setState(() {
_currentZoomLevel = value;
});
await controller!.setZoomLevel(value);
},
),
),
Container(
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(10.0),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
_currentZoomLevel.toStringAsFixed(1) +
'x',
style: TextStyle(color: Colors.white),
),
),
),
],
)
每次拖动滑块时,都会调用该方法来更新缩放级别值。在上面的代码中,我们还添加了一个小部件来显示当前的缩放级别值。
录制的图片有点大,大家下载预览吧
曝光控制
您可以使用控制器上的方法并传递曝光值来设置相机的曝光偏移值。
首先,让我们检索设备支持的相机曝光的最小值和最大值。
定义三个变量:
ouble _minAvailableExposureOffset = 0.0;
double _maxAvailableExposureOffset = 0.0;
double _currentExposureOffset = 0.0;
获取方法内的最小和最大相机曝光值:
cameraController
.getMinExposureOffset()
.then((value) => _minAvailableExposureOffset = value);
cameraController
.getMaxExposureOffset()
.then((value) => _maxAvailableExposureOffset = value);
我们将构建一个用于显示和控制曝光偏移的垂直滑块。Material Design 不提供垂直小部件,但您可以使用四分之三圈的来实现这一点。
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
_currentExposureOffset.toStringAsFixed(1) + 'x',
style: TextStyle(color: Colors.black),
),
),
),
Expanded(
child: RotatedBox(
quarterTurns: 3,
child: Container(
height: 30,
child: Slider(
value: _currentExposureOffset,
min: _minAvailableExposureOffset,
max: _maxAvailableExposureOffset,
activeColor: Colors.white,
inactiveColor: Colors.white30,
onChanged: (value) async {
setState(() {
_currentExposureOffset = value;
});
await controller!.setExposureOffset(value);
},
),
),
),
)
在上面的代码中,我们在滑块顶部构建了一个小部件来显示当前的曝光偏移值。
录制的图片有点大,大家下载预览吧
闪光模式选择器
您可以使用该方法并传递一个值来设置相机的闪光模式。
定义一个变量来存储 flash 模式的当前值:
FlashMode? _currentFlashMode;
然后获取方法内部的初始 flash 模式值:
_currentFlashMode = controller!.value.flashMode;
在用户界面上,我们将连续显示可用的闪光模式,用户可以点击其中任何一种来选择该闪光模式。
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () async {
setState(() {
_currentFlashMode = FlashMode.off;
});
await controller!.setFlashMode(
FlashMode.off,
);
},
child: Icon(
Icons.flash_off,
color: _currentFlashMode == FlashMode.off
? Colors.amber
: Colors.white,
),
),
InkWell(
onTap: () async {
setState(() {
_currentFlashMode = FlashMode.auto;
});
await controller!.setFlashMode(
FlashMode.auto,
);
},
child: Icon(
Icons.flash_auto,
color: _currentFlashMode == FlashMode.auto
? Colors.amber
: Colors.white,
),
),
InkWell(
onTap: () async {
setState(() {
_isCameraInitialized = false;
});
onNewCameraSelected(
cameras[_isRearCameraSelected ? 1 : 0],
);
setState(() {
_isRearCameraSelected = !_isRearCameraSelected;
});
},
child: Icon(
Icons.flash_on,
color: _currentFlashMode == FlashMode.always
? Colors.amber
: Colors.white,
),
),
InkWell(
onTap: () async {
setState(() {
_currentFlashMode = FlashMode.torch;
});
await controller!.setFlashMode(
FlashMode.torch,
);
},
child: Icon(
Icons.highlight,
color: _currentFlashMode == FlashMode.torch
? Colors.amber
: Colors.white,
),
),
],
)
选定的闪光模式将以琥珀色而不是白色突出显示。

翻转相机切换
要在前后摄像头之间切换,您必须通过向方法提供新值来重新初始化摄像头。
定义一个布尔变量来了解是否选择了后置摄像头,否则选择了前置摄像头。
bool _isRearCameraSelected = true;
以前,我们使用后置摄像头进行初始化,因此我们将存储在此布尔值中。
现在,我们将显示一个按钮来在后置摄像头和前置摄像头之间切换:
InkWell(
onTap: () {
setState(() {
_isCameraInitialized = false;
});
onNewCameraSelected(
cameras[_isRearCameraSelected ? 0 : 1],
);
setState(() {
_isRearCameraSelected = !_isRearCameraSelected;
});
},
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.circle,
color: Colors.black38,
size: 60,
),
Icon(
_isRearCameraSelected
? Icons.camera_front
: Icons.camera_rear,
color: Colors.white,
size: 30,
),
],
),
)
在上面的代码中,如果布尔值为,则作为索引传递给(翻转到前置摄像头),否则作为索引传递(翻转到后置摄像头)。
录制的图片有点大,大家下载预览吧
捕捉图像
您可以使用相机控制器上的方法使用设备相机拍照。捕获的图片作为a (这是一个跨平台的文件抽象)返回。
让我们定义一个函数来处理图片的捕获:
Future takePicture() async {
final CameraController? cameraController = controller;
if (cameraController!.value.isTakingPicture) {
// A capture is already pending, do nothing.
return null;
}
try {
XFile file = await cameraController.takePicture();
return file;
} on CameraException catch (e) {
print('Error occured while taking picture: $e');
return null;
}
}
该函数返回捕获的图片,如果捕获成功,则返回。
捕获按钮可以定义如下:
InkWell(
onTap: () async {
XFile? rawImage = await takePicture();
File imageFile = File(rawImage!.path);
int currentUnix = DateTime.now().millisecondsSinceEpoch;
final directory = await getApplicationDocumentsDirectory();
String fileFormat = imageFile.path.split('.').last;
await imageFile.copy(
'${directory.path}/$currentUnix.$fileFormat',
);
},
child: Stack(
alignment: Alignment.center,
children: [
Icon(Icons.circle, color: Colors.white38, size: 80),
Icon(Icons.circle, color: Colors.white, size: 65),
],
),
)
捕获成功后,会将图片保存到应用程序的文档目录中,并以时间戳作为图片名称,以便以后可以轻松访问所有捕获的图片。
在图像和视频模式之间切换
您可以连续使用两个s 在图像和视频模式之间切换。
定义一个布尔变量来存储所选模式:
bool _isVideoCameraSelected = false;
UI 按钮可以这样定义:
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 4.0,
),
child: TextButton(
onPressed: _isRecordingInProgress
? null
: () {
if (_isVideoCameraSelected) {
setState(() {
_isVideoCameraSelected = false;
});
}
},
style: TextButton.styleFrom(
primary: _isVideoCameraSelected
? Colors.black54
: Colors.black,
backgroundColor: _isVideoCameraSelected
? Colors.white30
: Colors.white,
),
child: Text('IMAGE'),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 4.0, right: 8.0),
child: TextButton(
onPressed: () {
if (!_isVideoCameraSelected) {
setState(() {
_isVideoCameraSelected = true;
});
}
},
style: TextButton.styleFrom(
primary: _isVideoCameraSelected
? Colors.black
: Colors.black54,
backgroundColor: _isVideoCameraSelected
? Colors.white
: Colors.white30,
),
child: Text('VIDEO'),
),
),
),
],
)

视频录制
要使用设备摄像头管理视频录制,您必须定义四个函数来处理录制过程的状态:
开始视频录制过程
停止视频录制过程
暂停录制,如果它已经在进行中
如果处于暂停状态,则恢复录制
此外,定义一个布尔变量来存储是否正在进行录制:
bool _isRecordingInProgress = false;
开始录制
您可以通过调用相机控制器上的方法开始视频录制:
Future startVideoRecording() async {
final CameraController? cameraController = controller;
if (controller!.value.isRecordingVideo) {
// A recording has already started, do nothing.
return;
}
try {
await cameraController!.startVideoRecording();
setState(() {
_isRecordingInProgress = true;
print(_isRecordingInProgress);
});
} on CameraException catch (e) {
print('Error starting to record video: $e');
}
}
开始录制后,布尔值设置为。
停止录制
可以通过调用控制器上的方法来停止已经在进行的视频录制:
Future stopVideoRecording() async {
if (!controller!.value.isRecordingVideo) {
// Recording is already is stopped state
return null;
}
try {
XFile file = await controller!.stopVideoRecording();
setState(() {
_isRecordingInProgress = false;
print(_isRecordingInProgress);
});
return file;
} on CameraException catch (e) {
print('Error stopping video recording: $e');
return null;
}
}
录制停止后,布尔值设置为。该方法以格式返回视频文件。
暂停录制
您可以通过调用控制器上的方法暂停正在进行的视频录制:
Future pauseVideoRecording() async {
if (!controller!.value.isRecordingVideo) {
// Video recording is not in progress
return;
}
try {
await controller!.pauseVideoRecording();
} on CameraException catch (e) {
print('Error pausing video recording: $e');
}
}
恢复录制
您可以通过调用控制器上的方法来恢复暂停的视频录制:
Future resumeVideoRecording() async {
if (!controller!.value.isRecordingVideo) {
// No video recording was in progress
return;
}
try {
await controller!.resumeVideoRecording();
} on CameraException catch (e) {
print('Error resuming video recording: $e');
}
}
开始和停止录制的按钮
您可以通过检查布尔值是否为真并在该位置显示视频开始/停止按钮来修改拍照按钮。
InkWell(
onTap: _isVideoCameraSelected
? () async {
if (_isRecordingInProgress) {
XFile? rawVideo = await stopVideoRecording();
File videoFile = File(rawVideo!.path);
int currentUnix = DateTime.now().millisecondsSinceEpoch;
final directory = await getApplicationDocumentsDirectory();
String fileFormat = videoFile.path.split('.').last;
_videoFile = await videoFile.copy(
'${directory.path}/$currentUnix.$fileFormat',
);
_startVideoPlayer();
} else {
await startVideoRecording();
}
}
: () async {
// code to handle image clicking
},
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.circle,
color: _isVideoCameraSelected
? Colors.white
: Colors.white38,
size: 80,
),
Icon(
Icons.circle,
color: _isVideoCameraSelected
? Colors.red
: Colors.white,
size: 65,
),
_isVideoCameraSelected &&
_isRecordingInProgress
? Icon(
Icons.stop_rounded,
color: Colors.white,
size: 32,
)
: Container(),
],
),
)
同样,在录制过程中,您可以检查布尔值是否为并显示暂停/恢复按钮而不是相机翻转按钮。
上次捕获的预览
让我们在相机视图的右下角显示最后拍摄的图片或录制的视频的预览。
为了实现这一点,我们还必须定义一种视频播放方法。
定义一个视频播放器控制器:
VideoPlayerController? videoController;
以下方法用于使用存储在变量中的视频文件启动视频播放器:
Future _startVideoPlayer() async {
if (_videoFile != null) {
videoController = VideoPlayerController.file(_videoFile!);
await videoController!.initialize().then((_) {
// Ensure the first frame is shown after the video is initialized,
// even before the play button has been pressed.
setState(() {});
});
await videoController!.setLooping(true);
await videoController!.play();
}
}
另外,不要忘记释放方法中的内存:
@override
void dispose() {
// ...
videoController?.dispose();
super.dispose();
}
预览的用户界面可以定义如下:
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(10.0),
border: Border.all(color: Colors.white, width: 2),
image: _imageFile != null
? DecorationImage(
image: FileImage(_imageFile!),
fit: BoxFit.cover,
)
: null,
),
child: videoController != null && videoController!.value.isInitialized
? ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: AspectRatio(
aspectRatio: videoController!.value.aspectRatio,
child: VideoPlayer(videoController!),
),
)
: Container(),
)
检索图像/视频文件
由于我们已将所有捕获的图像和录制的视频存储在应用程序文档目录的单个文件夹中,因此您可以轻松检索所有文件。如果您想在画廊视图中显示它们,或者您只想在预览中显示最后捕获的图像或视频文件的缩略图,这可能是必要的。
我们将定义一个方法,当新的捕获或录制完成时,该方法也将刷新预览图像/视频。
// To store the retrieved files
List allFileList = [];
refreshAlreadyCapturedImages() async {
// Get the directory
final directory = await getApplicationDocumentsDirectory();
List fileList = await directory.list().toList();
allFileList.clear();
List
总结
您已经创建了一个具有所有基本功能的成熟相机应用程序。您现在甚至可以向此应用程序添加自定义功能,并自定义用户界面以匹配您应用程序的设计调色板。