آموزش استفاده از دوربین در Flutter
امروزه دوربین یکی از امکانات مهم گوشی های هوشمند به شمار می رود و کاربران زیادی روزانه از طریق موبایل عکس و فیلم تهیه میکنند.
همچنین برنامه های متنوعی وجود دارد که بنا به امکانات آنها نیاز است تا کاربران از دوربین گوشی استفاده کنند.
در این مطلب با همدیگه یاد میگیریم تا چجوری از دوربین جلو و عقب گوشی های موبایل در برنامه نویسی Flutter استفاده کنیم.
در حالت برای استفاده از دوربین باید از زبان جاوا یا کاتلین استفاده میکردیم و یک SurfaceView میساختیم پیش نمایش تصاویر دریافتی از دوربین را مشاهده کنیم.
همچنین برای آیفون هم از زبان و سویفت و AVFoundation Capture کمک میگرفتیم.
اما با استفاده از فلاتر فقط با یکبار برنامه نویسی در هر دو سیستم عامل این امکانات را استفاده میکنیم که فوق العاده است.
در این آموزش از پکیج camera که توسط تیم فلاتر توسعه داده شده استفاده میکنیم تا یک اپلیکیشن طراحی کنیم که کاربر بتونه بین دوربین جلو و عقب انتخاب کنه و پیش نمایش دوربین و ببینه سپس با گرفتن عکس در مسیر مشخصی آن را ذخیره کند و با دوستانش به اشتراک بگذارد.
برای شروع کار نیاز به ۵ تا پکیج مختلف داریم.
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:
حالا وارد فایل ios/Runner/Info.plist شوید و دستورات زیر را قرار دهید.
برای شروع ما باید لیست دوربین های گوشی را دریافت کنیم که از کد زیر استفاده میکنیم.
List _cameras;
@override
void initState() {
_initCamera();
super.initState();
}
Future _initCamera() async {
_cameras = await availableCameras();
}
در مرحله بعدی یک CameraController بسازیم و آنرا مقداردهی کنیم.
همانطور که از نامش پیداست کار کنترل دوربین گوشی را با CameraController انجام می دهیم.
CameraController دو ویژگی مهم دارد به نام های CameraDescription و ResolutionPreset.
برای CameraDescription مقدار _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 قرار میدیم.
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 قبلی و پاک میکنیم و با اطلاعات جدید که شامل 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(() {});
}
}
در پایین صفحه برای کنترل کردن ویو های خودمون از سه تا آیکون استفاده میکنیم که در تصویر دمو بالای صفحه مشاهده کردید.
آیکون اول وارد گالری می شودی, آیکون دوم ثبت تصویر و ویدیو و آیکون سوم بین گرفتن عکس و فیلم جا به جا می شود.
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
key: _scaffoldKey,
extendBody: true,
body: ...
bottomNavigationBar: _buildBottomNavigationBar(),
);
پیاده سازی آیکون های پایین صفحه را در متدی به نام bottomNavigationBar انجام میدهیم.
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;
});
},
),
],
),
);
}
حالا نوبت به عکس گرفتن شده.
برای این کار ما سه مرحله و انجام میدیم.
اول: چک میکنیم ببینیم camera controller مقداردهی شده یا نه.
دوم: ساختن یک پوشه و مشخص کردن مسیر ذخیره شدن تصویر.
سوم: گرفتن عکس با استفاده از 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(() {});
}
}
برای ضبط ویدیو نیز در ۴ مرحله این کار و انجام میدهیم.
اول: چک میکنیم ببینیم camera controller مقداردهی شده یا نه.
دوم: نمایش تایمر برای نشان دادن مدت زمان ویدیو.
سوم: مشخص کردن مسیر ذخیره سازی ویدیو.
چهارم: استفاده از camera controller برای ضبط ویدیو و ذخیره کردن.
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;
}
حالا ما باید عملیات قطع کردن ضبط ویدیو را نیز پیاده سازی کنیم.
برای اینکار اول چک میکنیم که camera controller مقدار دهی شده باشه و سپس عملیات ضبط کردن و تامیر را غیر فعال میکنیم.
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 '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 به صورت افقی به همراه دو آیکون برای پاک کردن تصویر و به اشتراک گزاری در پایین صفحه استفاده میکنیم.
درون بدنه 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() عملیات اشتراک گزاری را انجام میدهیم.
این دستور شامل پارامتری های ورودی مانند نام,عنوان, لیستی از بایت ها می باشد.
_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',
);
}
کار ما در این پروژه به پایان رسید و امیدوارم که این آموزش برای شما برنامه نویسان مفید بوده باشه.
مطالب زیر را حتما مطالعه کنید
آموزش پیاده سازی لینت Lint در برنامه نویسی فلاتر
آموزش الگوی تزریق وابستگی در فلاتر Dependency Injection
کتاب های آموزش برنامه نویسی فلاتر + دانلود PDF
آموزش نصب فلاتر و رفع خطاهای رایج ساخت پروژه + ویدیو
آموزش استفاده از نقشه در فلاتر
آموزش ساخت Navigation Drawer در فلاتر
2 دیدگاه
به گفتگوی ما بپیوندید و دیدگاه خود را با ما در میان بگذارید.
سلام نوع نگارش تون خوبه اما من که نتونستم با استفاده از این آموزش پیاده سازی کنم کار رو.
چون در خصوص lib نام شون و .. توضیحی نبود که فایل چگونه ایجاد بشه. من همین رو به علاوه دانش قبلی خودم تکمیل کردم ۳۴ تا ارور داده..
سلام
اسم و لینک کتابخونه های مورد استفاده داخل آموزش قرار داده شده است. در کدوم بخش مشکل دارید؟