如果您曾经构建或使用过任何大型移动应用程序,则该应用程序很有可能会使用相机功能。如果您查看PlayStore中的热门图表,您会发现许多应用程序都使用相机执行各种任务。Flutter提供了一个相机插件,可以访问Android和iOS设备上的相机。在本文中,我们将探索Flutter相机插件,并且将构建一个小型相机应用程序以查看该插件可以做什么和不能做什么。
在继续前进之前,让我们看看我们将要构建什么。这个应用程式将可以拍照和录制影片。您可以在前置和后置摄像头之间切换。还有一个画廊,您可以在其中查看捕获的图像和录制的视频,并与其他应用程序共享它们或从设备中删除它们。
入门
该应用程序使用以下5个依赖项。您需要将这些依赖项添加到pubspec.yaml
。
- camera:提供用于与设备上的摄像头配合使用的工具。
- path_provider:查找正确的路径来存储媒体。
- video_player:播放录制的视频。
- esys_flutter_share:用于与其他应用程序共享媒体文件。
- thumbnails:用于从视频生成缩略图。
dependencies:
camera:
path_provider:
thumbnails:
git:
url: https://github.com/divyanshub024/Flutter_Thumbnails.git
video_player:
esys_flutter_share:
接下来,将文件中的最低Android SDK版本更新为21(或更高)android/app/build.gradle
。
将以下几行添加到您的ios/Runner/Info.plist
:
NSCameraUsageDescription
Can I use the camera please?
NSMicrophoneUsageDescription
Can I use the mic please?
NSAppTransportSecurity
NSAllowsArbitraryLoads
获取可用相机列表
首先,我们将使用相机插件获取相机列表。
List _cameras;
@override
void initState() {
_initCamera();
super.initState();
}
Future _initCamera() async {
_cameras = await availableCameras();
}
初始化相机控制器
现在,我们有可用相机的列表。接下来,我们将初始化相机控制器。摄像机控制器用于控制设备摄像机。CameraController
接受两个值CameraDescription
和ResolutionPreset
。最初,我们给出了一个摄像机说明,因为_camera[0]
它是我们的后置摄像机。
注意:这里我们
ResolutionPreset
以介质为准。如果冻结相机,请尝试避免使用更高的分辨率。请查看此问题以获取更多详细信息。
CameraController _controller;
Future _initCamera() async {
_controller = CameraController(_cameras[0], ResolutionPreset.medium);
_controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
相机预览
设置好相机后,我们将使用CameraPreview
小部件显示预览供稿。在显示摄像机预览之前,我们必须等待CameraController初始化。
@override
Widget build(BuildContext context) {
if (_controller != null) {
if (!_controller.value.isInitialized) {
return Container();
}
} else {
return const Center(
child: SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(),
),
);
}
}
初始化摄像机后,我们将显示摄像机预览。
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
key: _scaffoldKey,
extendBody: true,
body: Stack(
children: [
_buildCameraPreview(),
],
),
);
在内部,_buildCameraPreview()
我们将摄像机预览缩放到屏幕尺寸,以使其看起来为全屏。
Widget _buildCameraPreview() {
final size = MediaQuery.of(context).size;
return ClipRect(
child: Container(
child: Transform.scale(
scale: _controller.value.aspectRatio / size.aspectRatio,
child: Center(
child: AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: CameraPreview(_controller),
),
),
),
),
);
}
切换相机
下一步是要能够在前后摄像头之间切换或切换。为此,我们首先将图标按钮添加到stack widget中。
body: Stack(
children: [
_buildCameraPreview(),
Positioned(
top: 24.0,
left: 12.0,
child: IconButton(
icon: Icon(
Icons.switch_camera,
color: Colors.white,
),
onPressed: _onCameraSwitch,
),
),
],
),
_onCameraSwitch
按下时,此图标按钮调用方法。在此方法中,我们将先处理,CameraController
然后使用new初始化CameraController
和新的CameraDescription
。
Future _onCameraSwitch() async {
final CameraDescription cameraDescription =
(_controller.description == _cameras[0]) ? _cameras[1] : _cameras[0];
if (_controller != null) {
await _controller.dispose();
}
_controller = CameraController(cameraDescription, ResolutionPreset.medium);
_controller.addListener(() {
if (mounted) setState(() {});
if (_controller.value.hasError) {
showInSnackBar('Camera error ${_controller.value.errorDescription}');
}
});
try {
await _controller.initialize();
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
}
相机控制视图
在屏幕底部,我们将有一个控件视图,该视图基本上包含3个按钮。首先去画廊,其次去捕捉图像或录制视频,第三次在图像捕捉和视频录制之间切换。
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
key: _scaffoldKey,
extendBody: true,
body: ...
bottomNavigationBar: _buildBottomNavigationBar(),
);
该视图将显示在底部导航栏中。不要忘记添加extendBody: true.
Widget _buildBottomNavigationBar() {
return Container(
color: Theme.of(context).bottomAppBarColor,
height: 100.0,
width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
FutureBuilder(
future: getLastImage(),
builder: (context, snapshot) {
if (snapshot.data == null) {
return Container(
width: 40.0,
height: 40.0,
);
}
return GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Gallery(),
),
),
child: Container(
width: 40.0,
height: 40.0,
child: ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: Image.file(
snapshot.data,
fit: BoxFit.cover,
),
),
),
);
},
),
CircleAvatar(
backgroundColor: Colors.white,
radius: 28.0,
child: IconButton(
icon: Icon(
(_isRecordingMode)
? (_isRecording) ? Icons.stop : Icons.videocam
: Icons.camera_alt,
size: 28.0,
color: (_isRecording) ? Colors.red : Colors.black,
),
onPressed: () {
if (!_isRecordingMode) {
_captureImage();
} else {
if (_isRecording) {
stopVideoRecording();
} else {
startVideoRecording();
}
}
},
),
),
IconButton(
icon: Icon(
(_isRecordingMode) ? Icons.camera_alt : Icons.videocam,
color: Colors.white,
),
onPressed: () {
setState(() {
_isRecordingMode = !_isRecordingMode;
});
},
),
],
),
);
}
捕获图像
使用相机控制器捕获图像非常容易。
- 检查相机控制器是否已初始化。
- 构造目录并定义路径。
- 使用CameraController捕获图像并将其保存到给定路径。
void _captureImage() async {
if (_controller.value.isInitialized) {
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/media';
await Directory(dirPath).create(recursive: true);
final String filePath = '$dirPath/${_timestamp()}.jpeg';
await _controller.takePicture(filePath);
setState(() {});
}
}
录制视频
我们可以将录制视频过程分为两个部分:
开始录像:
- 检查相机控制器是否已初始化。
- 启动计时器以显示记录的视频时间。(可选的)
- 构造目录并定义路径。
- 使用摄像机控制器开始录制并将视频保存在定义的路径上。
Future startVideoRecording() async {
print('startVideoRecording');
if (!_controller.value.isInitialized) {
return null;
}
setState(() {
_isRecording = true;
});
_timerKey.currentState.startTimer();
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/media';
await Directory(dirPath).create(recursive: true);
final String filePath = '$dirPath/${_timestamp()}.mp4';
if (_controller.value.isRecordingVideo) {
// A recording is already started, do nothing.
return null;
}
try {
await _controller.startVideoRecording(filePath);
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
return filePath;
}
停止录像:
- 检查相机控制器是否已初始化。
- 停止计时器。
- 使用相机控制器停止视频录制。
Future stopVideoRecording() async {
if (!_controller.value.isRecordingVideo) {
return null;
}
_timerKey.currentState.stopTimer();
setState(() {
_isRecording = false;
});
try {
await _controller.stopVideoRecording();
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
}
这是相机屏幕的完整代码。
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_camera/gallery.dart';
import 'package:flutter_camera/video_timer.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:thumbnails/thumbnails.dart';
class CameraScreen extends StatefulWidget {
const CameraScreen({Key key}) : super(key: key);
@override
CameraScreenState createState() => CameraScreenState();
}
class CameraScreenState extends State
with AutomaticKeepAliveClientMixin {
CameraController _controller;
List _cameras;
final GlobalKey _scaffoldKey = GlobalKey();
bool _isRecordingMode = false;
bool _isRecording = false;
final _timerKey = GlobalKey();
@override
void initState() {
_initCamera();
super.initState();
}
Future _initCamera() async {
_cameras = await availableCameras();
_controller = CameraController(_cameras[0], ResolutionPreset.medium);
_controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
if (_controller != null) {
if (!_controller.value.isInitialized) {
return Container();
}
} else {
return const Center(
child: SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(),
),
);
}
if (!_controller.value.isInitialized) {
return Container();
}
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
key: _scaffoldKey,
extendBody: true,
body: Stack(
children: [
_buildCameraPreview(),
Positioned(
top: 24.0,
left: 12.0,
child: IconButton(
icon: Icon(
Icons.switch_camera,
color: Colors.white,
),
onPressed: () {
_onCameraSwitch();
},
),
),
if (_isRecordingMode)
Positioned(
left: 0,
right: 0,
top: 32.0,
child: VideoTimer(
key: _timerKey,
),
)
],
),
bottomNavigationBar: _buildBottomNavigationBar(),
);
}
Widget _buildCameraPreview() {
final size = MediaQuery.of(context).size;
return ClipRect(
child: Container(
child: Transform.scale(
scale: _controller.value.aspectRatio / size.aspectRatio,
child: Center(
child: AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: CameraPreview(_controller),
),
),
),
),
);
}
Widget _buildBottomNavigationBar() {
return Container(
color: Theme.of(context).bottomAppBarColor,
height: 100.0,
width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
FutureBuilder(
future: getLastImage(),
builder: (context, snapshot) {
if (snapshot.data == null) {
return Container(
width: 40.0,
height: 40.0,
);
}
return GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Gallery(),
),
),
child: Container(
width: 40.0,
height: 40.0,
child: ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: Image.file(
snapshot.data,
fit: BoxFit.cover,
),
),
),
);
},
),
CircleAvatar(
backgroundColor: Colors.white,
radius: 28.0,
child: IconButton(
icon: Icon(
(_isRecordingMode)
? (_isRecording) ? Icons.stop : Icons.videocam
: Icons.camera_alt,
size: 28.0,
color: (_isRecording) ? Colors.red : Colors.black,
),
onPressed: () {
if (!_isRecordingMode) {
_captureImage();
} else {
if (_isRecording) {
stopVideoRecording();
} else {
startVideoRecording();
}
}
},
),
),
IconButton(
icon: Icon(
(_isRecordingMode) ? Icons.camera_alt : Icons.videocam,
color: Colors.white,
),
onPressed: () {
setState(() {
_isRecordingMode = !_isRecordingMode;
});
},
),
],
),
);
}
Future getLastImage() async {
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/media';
final myDir = Directory(dirPath);
List _images;
_images = myDir.listSync(recursive: true, followLinks: false);
_images.sort((a, b) {
return b.path.compareTo(a.path);
});
var lastFile = _images[0];
var extension = path.extension(lastFile.path);
if (extension == '.jpeg') {
return lastFile;
} else {
String thumb = await Thumbnails.getThumbnail(
videoFile: lastFile.path, imageType: ThumbFormat.PNG, quality: 30);
return File(thumb);
}
}
Future _onCameraSwitch() async {
final CameraDescription cameraDescription =
(_controller.description == _cameras[0]) ? _cameras[1] : _cameras[0];
if (_controller != null) {
await _controller.dispose();
}
_controller = CameraController(cameraDescription, ResolutionPreset.medium);
_controller.addListener(() {
if (mounted) setState(() {});
if (_controller.value.hasError) {
showInSnackBar('Camera error ${_controller.value.errorDescription}');
}
});
try {
await _controller.initialize();
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
}
void _captureImage() async {
print('_captureImage');
if (_controller.value.isInitialized) {
SystemSound.play(SystemSoundType.click);
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/media';
await Directory(dirPath).create(recursive: true);
final String filePath = '$dirPath/${_timestamp()}.jpeg';
print('path: $filePath');
await _controller.takePicture(filePath);
setState(() {});
}
}
Future startVideoRecording() async {
print('startVideoRecording');
if (!_controller.value.isInitialized) {
return null;
}
setState(() {
_isRecording = true;
});
_timerKey.currentState.startTimer();
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/media';
await Directory(dirPath).create(recursive: true);
final String filePath = '$dirPath/${_timestamp()}.mp4';
if (_controller.value.isRecordingVideo) {
// A recording is already started, do nothing.
return null;
}
try {
// videoPath = filePath;
await _controller.startVideoRecording(filePath);
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
return filePath;
}
Future stopVideoRecording() async {
if (!_controller.value.isRecordingVideo) {
return null;
}
_timerKey.currentState.stopTimer();
setState(() {
_isRecording = false;
});
try {
await _controller.stopVideoRecording();
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
}
String _timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
void _showCameraException(CameraException e) {
logError(e.code, e.description);
showInSnackBar('Error: ${e.code}\n${e.description}');
}
void showInSnackBar(String message) {
_scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message)));
}
void logError(String code, String message) =>
print('Error: $code\nError Message: $message');
@override
bool get wantKeepAlive => true;
}
图库视图
我们的相机已经准备就绪,可以使用了。但是,我们如何查看捕获的图像和录制的视频?我们将创建一个画廊视图。它将由一个水平的网页浏览和一个底部的应用栏以及一个共享按钮和一个删除按钮组成。
在内部,PageView.builder
我们正在检查文件的扩展名。如果文件扩展名为jpeg
,则将其显示为图像,否则,将使用VideoPreview
小部件显示视频。
String currentFilePath;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
appBar: AppBar(
backgroundColor: Colors.black,
),
body: FutureBuilder(
future: _getAllImages(),
builder: (context, AsyncSnapshot> snapshot) {
if (!snapshot.hasData || snapshot.data.isEmpty) {
return Container();
}
print('${snapshot.data.length} ${snapshot.data}');
if (snapshot.data.length == 0) {
return Center(
child: Text('No images found.'),
);
}
return PageView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
currentFilePath = snapshot.data[index].path;
var extension = path.extension(snapshot.data[index].path);
if (extension == '.jpeg') {
return Container(
height: 300,
padding: const EdgeInsets.only(bottom: 8.0),
child: Image.file(
File(snapshot.data[index].path),
),
);
} else {
return VideoPreview(
videoPath: snapshot.data[index].path,
);
}
},
);
},
),
bottomNavigationBar: BottomAppBar(
child: Container(
height: 56.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: Icon(Icons.share),
onPressed: () => _shareFile(),
),
IconButton(
icon: Icon(Icons.delete),
onPressed: _deleteFile,
),
],
),
),
),
);
}
从设备获取媒体文件
Future> _getAllImages() async {
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/media';
final myDir = Directory(dirPath);
List _images;
_images = myDir.listSync(recursive: true, followLinks: false);
_images.sort((a, b) {
return b.path.compareTo(a.path);
});
return _images;
}
删除媒体文件
删除文件非常容易。只需将目录指向文件路径,然后使用deleteSync
函数将其删除。
_deleteFile() {
final dir = Directory(currentFilePath);
dir.deleteSync(recursive: true);
setState(() {});
}
共享媒体文件
为了共享文件,我们使用esys_flutter_share
插件。您可以使用Share.file()
将String title
,String name
,List < int >bytes
,String mimeType
作为强制参数的方法轻松共享文件。您可以使用readAsBytesSync
方法从文件中获取字节。
_shareFile() async {
var extension = path.extension(currentFilePath);
await Share.file(
'image',
(extension == '.jpeg') ? 'image.jpeg' : ' video.mp4',
File(currentFilePath).readAsBytesSync(),
(extension == '.jpeg') ? 'image/jpeg' : ' video/mp4',
);
}
我对相机插件的看法
在得出结论之前,我们应该知道Flutter Camera插件仍在开发中。该插件非常适合制作任何像样的相机应用程序,但是它有一些小问题,并且缺少许多高级功能,例如自动曝光和闪光灯支持。如果您想了解有关相机插件即将发生的变化的最新信息,请关注“相机插件的未来”问题。本期将讨论相机插件中即将提供的一些很酷的功能。
您可以在此处查看该项目的完整源代码。
翻译自:https://levelup.gitconnected.com/exploring-flutter-camera-plugin-d2c54ac95f05