如果您曾經(jīng)構(gòu)建或使用過任何大型移動(dòng)應(yīng)用程序龄章,則該應(yīng)用程序很有可能會(huì)使用相機(jī)功能想括。如果您查看PlayStore中的熱門圖表,您會(huì)發(fā)現(xiàn)許多應(yīng)用程序都使用相機(jī)執(zhí)行各種任務(wù)褪子。Flutter提供了一個(gè)相機(jī)插件辙售,可以訪問Android和iOS設(shè)備上的相機(jī)轻抱。在本文中,我們將探索Flutter相機(jī)插件旦部,并且將構(gòu)建一個(gè)小型相機(jī)應(yīng)用程序以查看該插件可以做什么和不能做什么祈搜。
在繼續(xù)前進(jìn)之前较店,讓我們看看我們將要構(gòu)建什么。這個(gè)應(yīng)用程式將可以拍照和錄制影片容燕。您可以在前置和后置攝像頭之間切換梁呈。還有一個(gè)畫廊,您可以在其中查看捕獲的圖像和錄制的視頻蘸秘,并與其他應(yīng)用程序共享它們或從設(shè)備中刪除它們官卡。
入門
該應(yīng)用程序使用以下5個(gè)依賴項(xiàng)。您需要將這些依賴項(xiàng)添加到pubspec.yaml
醋虏。
- camera:提供用于與設(shè)備上的攝像頭配合使用的工具味抖。
- path_provider:查找正確的路徑來存儲媒體。
- video_player:播放錄制的視頻灰粮。
- esys_flutter_share:用于與其他應(yīng)用程序共享媒體文件。
- 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
:
<key>NSCameraUsageDescription</key>
<string>Can I use the camera please?</string>
<key>NSMicrophoneUsageDescription</key>
<string>Can I use the mic please?</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
獲取可用相機(jī)列表
首先佩研,我們將使用相機(jī)插件獲取相機(jī)列表柑肴。
List<CameraDescription> _cameras;
@override
void initState() {
_initCamera();
super.initState();
}
Future<void> _initCamera() async {
_cameras = await availableCameras();
}
初始化相機(jī)控制器
現(xiàn)在,我們有可用相機(jī)的列表旬薯。接下來晰骑,我們將初始化相機(jī)控制器。攝像機(jī)控制器用于控制設(shè)備攝像機(jī)绊序。CameraController
接受兩個(gè)值CameraDescription
和ResolutionPreset
硕舆。最初,我們給出了一個(gè)攝像機(jī)說明骤公,因?yàn)?code>_camera[0]它是我們的后置攝像機(jī)抚官。
注意:這里我們
ResolutionPreset
以介質(zhì)為準(zhǔn)。如果凍結(jié)相機(jī)阶捆,請嘗試避免使用更高的分辨率凌节。請查看此問題以獲取更多詳細(xì)信息。
CameraController _controller;
Future<void> _initCamera() async {
_controller = CameraController(_cameras[0], ResolutionPreset.medium);
_controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
相機(jī)預(yù)覽
設(shè)置好相機(jī)后洒试,我們將使用CameraPreview
小部件顯示預(yù)覽供稿倍奢。在顯示攝像機(jī)預(yù)覽之前,我們必須等待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(),
),
);
}
}
初始化攝像機(jī)后卒煞,我們將顯示攝像機(jī)預(yù)覽。
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
key: _scaffoldKey,
extendBody: true,
body: Stack(
children: <Widget>[
_buildCameraPreview(),
],
),
);
在內(nèi)部捕犬,_buildCameraPreview()
我們將攝像機(jī)預(yù)覽縮放到屏幕尺寸跷坝,以使其看起來為全屏酵镜。
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),
),
),
),
),
);
}
切換相機(jī)
下一步是要能夠在前后攝像頭之間切換或切換。為此柴钻,我們首先將圖標(biāo)按鈕添加到stack widget中淮韭。
body: Stack(
children: <Widget>[
_buildCameraPreview(),
Positioned(
top: 24.0,
left: 12.0,
child: IconButton(
icon: Icon(
Icons.switch_camera,
color: Colors.white,
),
onPressed: _onCameraSwitch,
),
),
],
),
_onCameraSwitch
按下時(shí)婉商,此圖標(biāo)按鈕調(diào)用方法渤早。在此方法中,我們將先處理休傍,CameraController
然后使用new初始化CameraController
和新的CameraDescription
毫蚓。
Future<void> _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(() {});
}
}
相機(jī)控制視圖
在屏幕底部占键,我們將有一個(gè)控件視圖,該視圖基本上包含3個(gè)按鈕元潘。首先去畫廊畔乙,其次去捕捉圖像或錄制視頻,第三次在圖像捕捉和視頻錄制之間切換翩概。
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
key: _scaffoldKey,
extendBody: true,
body: ...
bottomNavigationBar: _buildBottomNavigationBar(),
);
該視圖將顯示在底部導(dǎo)航欄中牲距。不要忘記添加extendBody: true.
Widget _buildBottomNavigationBar() {
return Container(
color: Theme.of(context).bottomAppBarColor,
height: 100.0,
width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
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;
});
},
),
],
),
);
}
捕獲圖像
使用相機(jī)控制器捕獲圖像非常容易。
- 檢查相機(jī)控制器是否已初始化钥庇。
- 構(gòu)造目錄并定義路徑牍鞠。
- 使用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(() {});
}
}
錄制視頻
我們可以將錄制視頻過程分為兩個(gè)部分:
開始錄像:
- 檢查相機(jī)控制器是否已初始化评姨。
- 啟動(dòng)計(jì)時(shí)器以顯示記錄的視頻時(shí)間难述。(可選的)
- 構(gòu)造目錄并定義路徑。
- 使用攝像機(jī)控制器開始錄制并將視頻保存在定義的路徑上吐句。
Future<String> 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;
}
停止錄像:
- 檢查相機(jī)控制器是否已初始化胁后。
- 停止計(jì)時(shí)器。
- 使用相機(jī)控制器停止視頻錄制嗦枢。
Future<void> 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;
}
}
這是相機(jī)屏幕的完整代碼择同。
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<CameraScreen>
with AutomaticKeepAliveClientMixin {
CameraController _controller;
List<CameraDescription> _cameras;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
bool _isRecordingMode = false;
bool _isRecording = false;
final _timerKey = GlobalKey<VideoTimerState>();
@override
void initState() {
_initCamera();
super.initState();
}
Future<void> _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: <Widget>[
_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: <Widget>[
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<FileSystemEntity> getLastImage() async {
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/media';
final myDir = Directory(dirPath);
List<FileSystemEntity> _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<void> _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<String> 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<void> 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;
}
圖庫視圖
我們的相機(jī)已經(jīng)準(zhǔn)備就緒,可以使用了净宵。但是敲才,我們?nèi)绾尾榭床东@的圖像和錄制的視頻?我們將創(chuàng)建一個(gè)畫廊視圖择葡。它將由一個(gè)水平的網(wǎng)頁瀏覽和一個(gè)底部的應(yīng)用欄以及一個(gè)共享按鈕和一個(gè)刪除按鈕組成紧武。
在內(nèi)部,PageView.builder
我們正在檢查文件的擴(kuò)展名敏储。如果文件擴(kuò)展名為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<List<FileSystemEntity>> 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: <Widget>[
IconButton(
icon: Icon(Icons.share),
onPressed: () => _shareFile(),
),
IconButton(
icon: Icon(Icons.delete),
onPressed: _deleteFile,
),
],
),
),
),
);
}
從設(shè)備獲取媒體文件
Future<List<FileSystemEntity>> _getAllImages() async {
final Directory extDir = await getApplicationDocumentsDirectory();
final String dirPath = '${extDir.path}/media';
final myDir = Directory(dirPath);
List<FileSystemEntity> _images;
_images = myDir.listSync(recursive: true, followLinks: false);
_images.sort((a, b) {
return b.path.compareTo(a.path);
});
return _images;
}
刪除媒體文件
刪除文件非常容易滥酥。只需將目錄指向文件路徑,然后使用deleteSync
函數(shù)將其刪除畦幢。
_deleteFile() {
final dir = Directory(currentFilePath);
dir.deleteSync(recursive: true);
setState(() {});
}
共享媒體文件
為了共享文件坎吻,我們使用esys_flutter_share
插件。您可以使用Share.file()
將String title
宇葱,String name
瘦真,List < int >bytes
,String mimeType
作為強(qiáng)制參數(shù)的方法輕松共享文件黍瞧。您可以使用readAsBytesSync
方法從文件中獲取字節(jié)诸尽。
_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',
);
}
我對相機(jī)插件的看法
在得出結(jié)論之前,我們應(yīng)該知道Flutter Camera插件仍在開發(fā)中印颤。該插件非常適合制作任何像樣的相機(jī)應(yīng)用程序您机,但是它有一些小問題,并且缺少許多高級功能年局,例如自動(dòng)曝光和閃光燈支持往产。如果您想了解有關(guān)相機(jī)插件即將發(fā)生的變化的最新信息,請關(guān)注“相機(jī)插件的未來”問題某宪。本期將討論相機(jī)插件中即將提供的一些很酷的功能。
您可以在此處查看該項(xiàng)目的完整源代碼锐朴。
翻譯自:https://levelup.gitconnected.com/exploring-flutter-camera-plugin-d2c54ac95f05