เนื้อหานี้จะขอต่อยอดจากตอนที่แล้ว เกี่ยวกับการจัดการไฟล์
ซึ่งตอนที่แล้วเราได้รู้จักการจัดการเกี่ยวกับไฟล์เสียงหรือ auido ไฟล์
เนื้อหานี้เราจะมาดูเกี่ยวกับการจัดการไฟล์รูปภาพ และไฟล์วิดีโอ ซึ่ง
สำหรับไฟล์รูปภาพ เราได้มีเนื้อหาบ้างแล้วเกี่ยวกับการแสดงรูปในแบบ
Photo Gallery นั่นคือนำข้อมูลรูปภาพจากไฟล์ข้อมูลที่เรามีอยู่แล้ว
มาแสดง แต่ในตอนนี้เราจะพูดถึงการบันทึกหรือภารถ่ายภาพและบันทึก
เป็นไฟล์ในแอป
ทบทวนเกี่ยวกับ Photo Gallery ได้ที่
สร้าง Photo Gallery จาก API อย่างง่าย ใน Flutter http://niik.in/1071
นอกจากรูปภาพหรือไฟล์ภาพแล้ว เราจะพูดถึงการบันทึกวิดีโอหรือไฟล์ video ซึ่งเนื้อหา
เกี่ยวกับการแสดงวิดีโอนั้นเราพูดไปแล้วในบทความด้านล่าง
การจัดการและใช้งาน เกี่ยวกับ Video ใน Flutter ตอนที่ 1 http://niik.in/1113
ประยุกต์แสดงคลิป Video คล้าย TikTok ใน Flutter ตอนที่ 2 http://niik.in/1114
ซึ่งเป็นการนำข้อมูลไฟล์วิดีโอที่เรามีอยู่แล้วมาใช้งานหรือแสดง แต่ในบทความนี้เราจะพูดถึงการบันทึก
ไฟล์วิดีโอผ่านกล้องหรือ camera เพื่อใช้งานในแอป และแน่นอนว่าเราจะมีการนำโค้ดจากบทความ
ที่เกี่ยวข้องมาใช้งาน เช่นการเปิดแสดงรูปภาพที่เราบันทึกไปแล้ว หรือการเล่นไฟล์วิดีโอที่เราได้บันทึก
ไป แบบนี้เป็นต้น
การกำหนดการขอสิทธิ์เข้าถึงการใช้งานข้อมูล
ก่อนที่จะลงรายละเอียด สิ่งแรกที่เราต้องทำคือการขอสิทธิ์การเข้าถึงส่วนจัดการต่างๆ ในบทความนี้
จะมีเนื้อหาจากตอนที่แล้วด้วย ก็จะมีทั้ง การบันทึกเสียง การใช้กล้อง การอ่านไฟล์ เขียนไฟล์ ให้เรา
กำหนดดังนี้
ไฟล์ android > app > src > main > AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> <!-- For Android 13+ --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <application .... .... </manifest>
นอกจากการขอสิทธิ์ต่างเหล่านี้ในไฟล์ AndroidManifest.xml ในการกำหนดนี้แล้ว เรายังอาจจะ
ต้องกำหนดการขอใช้สิทธิ์ในการใช้งานตอนรันแอป (runtime permission) เพิ่มด้วย
ติดตั้ง package ที่จำเป็นเพิ่มเติม ตามรายการด้านล่าง
แพ็กเก็จที่จำเป็นต้องติดตั้งเพิ่มเติม สำหรับการทำงานมีดังนี้
font_awesome_flutter: ^10.7.0 path_provider: ^2.1.4 record: ^5.1.2 permission_handler: ^11.3.1 audioplayers: ^6.1.0 http: ^1.2.2 intl: ^0.17.0 file_picker: ^8.1.2 camera: ^0.11.0+2 image_picker: ^1.1.2 video_player: ^2.9.1
ในการบันทึกวิดีโอหรือรูปภาพด้วยกล้อง เราจะแนะนำการใช้งานทั้งสอง package คือ image_picker
และ camera โดยตัว image_picker จะมีรูปแบบการใช้งานหรือการตั้งค่าที่ง่ายกว่า รองรับทั้งการ
ถ่ายรูปด้วยกล้องและการดึงรูปภาพจาก gallery ในขณะที่ camera จะรองรับการปรับแต่งที่มากกว่า
แต่ก็จะมาพร้อมกับการตั้งค่าที่ซับซ้อนขึ้น ดังนั้น เราเลือกใช้ได้ตามความเหมาะสมกับการใช้งาน
การใช้งาน Image_Picker โหลดหรือบันทึกไฟล์ภาพและวิดีโอ
ในตัวอย่างเราจะสร้างไฟล์หน้าสำหรับใช้งาน image_picker คำอธิบายแสดงในโค้ด โดยในโค้ดจะ
เป็นการใช้งานฟังก์ชั่นเกี่ยวกับการโหลดไฟล์ภาพหรือไฟล์วิดีโอจาก gallery และการบันทึกไฟล์ หรือ
ไฟล์วิดีโอจากกล้อง camera ให้เป็นแนวทางนำไปปรับใช้งานได้
ไฟล์ imagepicker.dart
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; class Picker extends StatefulWidget { static const routeName = '/picker'; const Picker({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _PickerState(); } } class _PickerState extends State<Picker> { final ImagePicker _picker = ImagePicker(); XFile? _videoFile; XFile? _imageFile; // ตัวแปรสำหรับเก็บไฟล์รูปภาพที่เลือก // ฟังก์ชันสำหรับการเลือกวิดีโอ Future<void> pickVideo(ImageSource source) async { XFile? video = await _picker.pickVideo(source: source); setState(() { _videoFile = video; }); } // ฟังก์ชันสำหรับการเลือกภาพ Future<void> pickImage(ImageSource source) async { XFile? image = await _picker.pickImage(source: source); setState(() { _imageFile = image; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Image & Video Picker'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Image & Video Picker Screen'), // แสดงภาพที่เลือก _imageFile != null ? Image.file( File(_imageFile!.path), height: 200, ) : Text("No image selected"), // แสดงวิดีโอที่เลือก _videoFile != null ? Text("Video saved to: ${_videoFile!.path}") : Text("No video selected"), // ปุ่มสำหรับเลือกภาพจากแกลเลอรี ElevatedButton( onPressed: () => pickImage(ImageSource.gallery), child: Text('Pick Image from Gallery'), ), // ปุ่มสำหรับเลือกภาพจากกล้อง ElevatedButton( onPressed: () => pickImage(ImageSource.camera), child: Text('Capture Image from Camera'), ), // ปุ่มสำหรับเลือกวิดีโอจากแกลเลอรี ElevatedButton( onPressed: () => pickVideo(ImageSource.gallery), child: Text('Pick Video from Gallery'), ), // ปุ่มสำหรับบันทึกวิดีโอ ElevatedButton( onPressed: () => pickVideo(ImageSource.camera), child: Text('Record Video from Camera'), ), ], )), ); } }
ผลลัพธ์ที่ได้
จากตัวอย่างโค้ด จะเห็นว่ามีวิธีการเรียกใช้งานที่ค่อนข้างง่ายไม่ยุ่งยาก หลักการงานคือ ถ้าเราเลือกเป็น
การโหลดไฟล์ภาพหรือวิดีโอจาก gallery หรือจากในเครื่อง ข้อมูลไฟล์ก็จะถูกนำไปเก็บในโฟลเดอร์
แคช (cache) โดยมีโฟลเดอร์สำหรับแคซไฟล์เก็บไฟล์เหล่านั้นไว้อีกที ในขณะที่ถ้าเราเลือกเป้นการ
ถ่ายรูปหรือวิดีโอจากกล้อง ก็จะเป็นการการเก็บไว้ในโฟลเดอร์แคชเช่นกัน แต่ไม่มีโฟลเดอร์เก็บไฟล์นั้น
ในตัวอย่าง ถ้าเป็นไฟล์รูป เราสามารถใช้การตรวจสอบชื่อไฟล์และแสดงรูปได้ทันที ในขณะที่ถ้าเป็นไฟล์
วิดีโอ เราจะแสดงที่ path หรือตำแหน่งของไฟล์ที่ถูกบันทึกไว้ ซึ่งนี้ทำให้เราเห็นแนวทางการทำงานและ
การจัดเก็บไฟล์เท่านั้น หากนำไปใช้งานจริง เราอาจจะต้องนำไฟล์เหล่านี้ไปไว้ที่ app_flutter เพื่อนำ
ไปใช้งานในแอปต่อไป เพราะไฟล์ในโฟลเดอร์แคชจะเป็นไฟล์ชั่วคราว อาจจะโดนลบจากระบบในตอนไหน
ก็เป็นได้
เราจะปรับแต่งโค้ดและแยกเป็นอีกตัวอย่างเพื่อให้เปรียบเทียบกัน โดยตัวแก้ไข เราจะมีการคัดลอกไฟล์
จากแคชมายังโฟลเดอร์ app_flutter หรือโฟลเดอร์ข้อมูลของแอป และมีการกำหนดชื่อไฟล์ใหม่ใน
รูปแบบ video_ddMMyyyyTHHmmss และ image_ddMMyyyyTHHmmss ดังโค้ดด้านล่าง
ไฟล์ imagepicker.dart (ปรับปรุง)
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:path_provider/path_provider.dart'; import 'package:intl/intl.dart'; class Picker extends StatefulWidget { static const routeName = '/picker'; const Picker({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _PickerState(); } } class _PickerState extends State<Picker> { final ImagePicker _picker = ImagePicker(); XFile? _videoFile; XFile? _imageFile; // ตัวแปรสำหรับเก็บไฟล์รูปภาพที่เลือก Directory? _currentFolder; @override void initState() { super.initState(); _getAppDirectory(); } // ฟังก์ชันเพื่อให้ได้ path ของโฟลเดอร์แอป Future<void> _getAppDirectory() async { _currentFolder = await getApplicationDocumentsDirectory(); } // ฟังก์ชันสำหรับการเลือกวิดีโอ Future<void> pickVideo(ImageSource source) async { XFile? video = await _picker.pickVideo(source: source); try { if (await Permission.storage.request().isGranted) { if (video != null) { // สร้างชื่อและ path ใหม่สำหรับไฟล์วิดีโอ String timestamp = DateFormat('ddMMyyyyTHHmmss').format(DateTime.now()); String newFileName = 'video_$timestamp'; String newPath = "${_currentFolder!.path}/$newFileName${video.name.substring(video.name.lastIndexOf('.'))}"; // กำหนด file object var cacheFile = File(video.path); // คัดลอกไฟล์จาก cache ไปยัง path ใหม่ใน app await cacheFile.copy(newPath); // ใช้ File เพื่อเข้าถึงข้อมูลเกี่ยวกับไฟล์ File newFile = File(newPath); int fileSize = await newFile.length(); // ใช้ await เพื่อรับขนาดไฟล์ // แสดงข้อมูลไฟล์ print("Video file copied to: $newPath"); print("File name: ${video.name}"); print("File size: $fileSize bytes"); print("File path: ${video.path}"); // โหลดข้อมูลใหม่อีกครั้งถ้าต้องการ setState(() { _videoFile = video; // เก็บไฟล์วิดีโอใน state }); } else { print("No video selected."); } } else { print("Storage permission is denied."); } } catch (e) { print("Error occurred: $e"); } } // ฟังก์ชันสำหรับการเลือกภาพ Future<void> pickImage(ImageSource source) async { XFile? image = await _picker.pickImage(source: source); try { if (await Permission.storage.request().isGranted) { if (image != null) { // สร้าง ชื่อใหม่ และ path ใหม่สำหรับไฟล์ภาพ String timestamp = DateFormat('ddMMyyyyTHHmmss').format(DateTime.now()); String newFileName = 'image_$timestamp'; String newPath = "${_currentFolder!.path}/$newFileName${image.name.substring(image.name.lastIndexOf('.'))}"; // กำหนด file object var cacheFile = File(image.path); // คัดลอกไฟล์จาก cache ไปยัง path ใหม่ใน app await cacheFile.copy(newPath); // ใช้ File เพื่อเข้าถึงข้อมูลเกี่ยวกับไฟล์ File newFile = File(newPath); int fileSize = await newFile.length(); // ใช้ await เพื่อรับขนาดไฟล์ // แสดงข้อมูลไฟล์ print("Image file copied to: $newPath"); print("File name: ${image.name}"); print("File size: $fileSize bytes"); print("File path: ${image.path}"); // โหลดข้อมูลใหม่อีกครั้งถ้าต้องการ setState(() { _imageFile = image; // เก็บไฟล์ภาพใน state }); } else { print("No image selected."); } } else { print("Storage permission is denied."); } } catch (e) { print("Error occurred: $e"); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Image & Video Picker'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Image & Video Picker Screen'), // แสดงภาพที่เลือก _imageFile != null ? Image.file( File(_imageFile!.path), height: 200, ) : Text("No image selected"), // แสดงวิดีโอที่เลือก _videoFile != null ? Text("Video saved to: ${_videoFile!.path}") : Text("No video selected"), // ปุ่มสำหรับเลือกภาพจากแกลเลอรี ElevatedButton( onPressed: () => pickImage(ImageSource.gallery), child: Text('Pick Image from Gallery'), ), // ปุ่มสำหรับเลือกภาพจากกล้อง ElevatedButton( onPressed: () => pickImage(ImageSource.camera), child: Text('Capture Image from Camera'), ), // ปุ่มสำหรับเลือกวิดีโอจากแกลเลอรี ElevatedButton( onPressed: () => pickVideo(ImageSource.gallery), child: Text('Pick Video from Gallery'), ), // ปุ่มสำหรับบันทึกวิดีโอ ElevatedButton( onPressed: () => pickVideo(ImageSource.camera), child: Text('Record Video from Camera'), ), ], )), ); } }
ผลลัพธ์ที่ได้
จะเห็นว่าไฟล์ที่เราทำการเลือกเข้ามาหรือถ่ายด้วยกล้องจะถูกคัดลอกมาไว้ในโฟลเดอร์ app_flutter
พร้อมใช้รูปแบบชื่อไฟล์ใหม่ตามที่เราต้องการ
การใช้งาน Camera บันทึกไฟล์ภาพและวิดีโอ
ส่วนของการใช้งาน camera plugin ถึงแม้จะรองรับการจัดการที่หลากหลายแต่ก็ไม่รองรับการ
เลือกหรือเรียกดูไฟล์จาก gallery ได้ จะเป็นลักษณะการใช้งานจากกล้องเป็นหลัก คือการบันทึกรูปภาพ
หรือการบันทึกไฟล์วิดีโอจากกล้อง โค้ดด้านล่างเป็นตัวอย่างอย่างง่ายในการใช้งาน ที่เราสามารถแสดง
พรีวิวหน้าจอกล้อง โดยสามารถกดเพื่อบันทึกวิดีโอ หรือถ่ายเป็นไฟล์รูปภาพแล้วบันทึกไว้ในโฟลเดอร์
app_flutter คำอธิบายแสดงในโค้ด
ไฟล์ camera.dart
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:camera/camera.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:intl/intl.dart'; class Camera extends StatefulWidget { static const routeName = '/camara'; const Camera({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _CameraState(); } } class _CameraState extends State<Camera> { // ส่วนควบคุมกล้อง CameraController? _cameraController; List<CameraDescription>? cameras; bool isRecording = false; String? _imagePath; String? _videoPath; Directory? _currentFolder; @override void initState() { super.initState(); _getAppDirectory(); initializeCamera(); } // ฟังก์ชันเพื่อให้ได้ path ของโฟลเดอร์แอป Future<void> _getAppDirectory() async { _currentFolder = await getApplicationDocumentsDirectory(); } // เตรียมความพร้อมในส่วนของกล้อง Future<void> initializeCamera() async { // Request camera permission final cameraStatus = await Permission.camera.request(); if (cameraStatus.isGranted) { cameras = await availableCameras(); _cameraController = CameraController(cameras![0], ResolutionPreset.high); await _cameraController!.initialize(); setState(() {}); } else { // Handle the case when the user denies permission print("Camera permission denied."); } } // ฟังก์ชั่นทำงานการบันทึกวิดีโอจากกล้อง Future<void> startVideoRecording() async { try { if (await Permission.storage.request().isGranted) { if (_cameraController != null && !_cameraController!.value.isRecordingVideo) { // สร้างชื่อไฟล์วิดีโอ String timestamp = DateFormat('ddMMyyyyTHHmmss').format(DateTime.now()); _videoPath = '${_currentFolder!.path}/video_$timestamp.mp4'; await _cameraController!.startVideoRecording(); setState(() { isRecording = true; }); } } else { print("Storage permission is denied."); } } catch (e) { print("Error occurred: $e"); } } // ฟังก์ชั่นหยุดการบันทึกวิดีโอ Future<void> stopVideoRecording() async { if (_cameraController != null && _cameraController!.value.isRecordingVideo) { XFile videoFile = await _cameraController!.stopVideoRecording(); setState(() { isRecording = false; }); print("Video saved to: ${videoFile.path}"); // บันทึกไฟล์วิดีโอไปยัง app โฟลเดอร์ app_flutter String newVideoPath = '${_currentFolder!.path}/video_${DateFormat('ddMMyyyyTHHmmss').format(DateTime.now())}.mp4'; await videoFile.saveTo(newVideoPath); print("Video moved to: $newVideoPath"); } } // ฟังก์ชั่นสำหรับบันทึกไฟล์ภาพ หรือจับภาพจากกล้อง Future<void> takePicture() async { try { if (await Permission.storage.request().isGranted) { if (_cameraController != null && !_cameraController!.value.isTakingPicture) { // สร้างชื่อไฟล์รูปภาพ String timestamp = DateFormat('ddMMyyyyTHHmmss').format(DateTime.now()); _imagePath = '${_currentFolder!.path}/image_$timestamp.jpg'; // ถ่ายรูป XFile imageFile = await _cameraController!.takePicture(); // บันทึกไฟล์รูปไปยัง app โฟลเดอร์ app_flutter await imageFile.saveTo(_imagePath!); print("Image saved to: $_imagePath"); } } else { print("Storage permission is denied."); } } catch (e) { print("Error occurred: $e"); } } @override void dispose() { _cameraController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Camera'), ), body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ _cameraController != null && _cameraController!.value.isInitialized ? Column( children: [ CameraPreview(_cameraController!), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: isRecording ? stopVideoRecording : startVideoRecording, child: Text(isRecording ? 'Stop Recording' : 'Start Recording'), ), ElevatedButton( onPressed: takePicture, child: Text('Take Picture'), ), ], ), ], ) : Center(child: CircularProgressIndicator()), ], ), ), ); } }
ผลลัพธ์ที่ได้
ข้างต้นเป็นรูปแบบการใช้งานอย่างง่าย ในกรณีเราไม่ต้องการโค้ดหรือการทำงานที่ซับซ้อน อย่างไรก็ดี
ด้วยความสามารถของตัว plugin ยังสามารถปรับแต่งส่วนต่างๆ หากต้องการได้ ไม่ว่าจะเป็นการกำหนด
เกี่ยวกับกล้องหน้ากล้องหลัง การใช้ไฟล์แฟลซ การโฟกัส เหล่านี้เป็นต้น ดูตัวอย่างรูปแบบปรับแต่ง เพิ่ม
เติมได้ที่โค้ดด้านล่าง สามารถเลือกเอาส่วนที่ต้องการไปประยุกต์ใช้งานได้
ไฟล์ camera.dart (ปรับแต่งเพิ่มเติมจากโค้ดตัวอย่าง)
import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:camera/camera.dart'; import 'package:video_player/video_player.dart'; import 'package:flutter/scheduler.dart'; class Camera extends StatefulWidget { static const routeName = '/camara'; const Camera({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _CameraState(); } } class _CameraState extends State<Camera> with WidgetsBindingObserver, TickerProviderStateMixin { CameraController? controller; bool _cameraReady = false; XFile? imageFile; XFile? videoFile; VideoPlayerController? videoController; VoidCallback? videoPlayerListener; // ค่าสำหรับกำหนดมีเสียงหรือไม่มีสียงเมื่อบันทึกวิดีโอ bool enableAudio = true; // ค่าสำหรับการกำหนดการชดเชยแสง double _minAvailableExposureOffset = 0.0; double _maxAvailableExposureOffset = 0.0; double _currentExposureOffset = 0.0; // ค่าจัดการเกี่ยวกับ animation การซ่อนแสดงปุ่มต่างๆ late AnimationController _flashModeControlRowAnimationController; late Animation<double> _flashModeControlRowAnimation; late AnimationController _exposureModeControlRowAnimationController; late Animation<double> _exposureModeControlRowAnimation; late AnimationController _focusModeControlRowAnimationController; late Animation<double> _focusModeControlRowAnimation; // ค่าเกี่ยวกับการซูม double _minAvailableZoom = 1.0; double _maxAvailableZoom = 1.0; double _currentScale = 1.0; double _baseScale = 1.0; // เก็บจำนวนนิ้วผู้ใช้บนหน้าจอที่กำลังกดหรือใช้งานอยู่ int _pointers = 0; @override void initState() { super.initState(); initialCamera(); // ส่วนสำหรับตรวจจับสถาะของ state เพื่อใช้งาน AppLifecycle WidgetsBinding.instance.addObserver(this); // ส่วนจัดการ animation การแสดงของปุ่มต่างๆ _flashModeControlRowAnimationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _flashModeControlRowAnimation = CurvedAnimation( parent: _flashModeControlRowAnimationController, curve: Curves.easeInCubic, ); _exposureModeControlRowAnimationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _exposureModeControlRowAnimation = CurvedAnimation( parent: _exposureModeControlRowAnimationController, curve: Curves.easeInCubic, ); _focusModeControlRowAnimationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _focusModeControlRowAnimation = CurvedAnimation( parent: _focusModeControlRowAnimationController, curve: Curves.easeInCubic, ); } // ฟังก์ชั่นเตรียมพร้อมกล้องเพื่อใช้งาน Future<void> initialCamera() async { try { WidgetsFlutterBinding.ensureInitialized(); _cameras = await availableCameras(); if (_cameras.isNotEmpty) { setState(() { _cameraReady = true; }); } } on CameraException catch (e) { _logError(e.code, e.description); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _flashModeControlRowAnimationController.dispose(); _exposureModeControlRowAnimationController.dispose(); super.dispose(); } // #docregion AppLifecycle @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) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { _initializeCameraController(cameraController.description); } } // #enddocregion AppLifecycle @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('การใข้งานกล้อง'), ), body: Column( children: <Widget>[ Expanded( child: Container( decoration: BoxDecoration( color: Colors.black, border: Border.all( color: controller != null && controller!.value.isRecordingVideo ? Colors.redAccent : Colors.grey, width: 3.0, ), ), child: Padding( padding: const EdgeInsets.all(1.0), child: Center( child: _cameraPreviewWidget(), ), ), ), ), _captureControlRowWidget(), _modeControlRowWidget(), Padding( padding: const EdgeInsets.all(5.0), child: Row( children: <Widget>[ _cameraTogglesRowWidget(), _thumbnailWidget(), ], ), ), ], ), ); } // สร้างภาพหรือวิดีโอตัวอย่างจากกล้องที่กำลังเปิดใช้งานรอบันทึก Widget _cameraPreviewWidget() { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { return const Text( 'เลือกกล้อง(ถ้ามี)', style: TextStyle( color: Colors.white, fontSize: 24.0, fontWeight: FontWeight.w900, ), ); } else { return Listener( onPointerDown: (_) => _pointers++, onPointerUp: (_) => _pointers--, child: CameraPreview( controller!, child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return GestureDetector( behavior: HitTestBehavior.opaque, onScaleStart: _handleScaleStart, onScaleUpdate: _handleScaleUpdate, onTapDown: (TapDownDetails details) => onViewFinderTap(details, constraints), ); }), ), ); } } // ฟังก์ชั่นจัดการสเกลค่าเริ่มต้น void _handleScaleStart(ScaleStartDetails details) { _baseScale = _currentScale; } // ฟังก์ชั่นสำหรับจัดการสเกลการซูมกล้อง โดยใช้มือซูมเข้าออกหรือปรับตำแหน่งโฟกัส Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async { if (controller == null || _pointers != 2) { return; } _currentScale = (_baseScale * details.scale) .clamp(_minAvailableZoom, _maxAvailableZoom); await controller!.setZoomLevel(_currentScale); } // ฟังก์ชั่นแสดง thumbnail ของรูปหรือวิดีโอ Widget _thumbnailWidget() { final VideoPlayerController? localVideoController = videoController; return Expanded( child: Align( alignment: Alignment.centerRight, child: Row( mainAxisSize: MainAxisSize.min, children: <Widget>[ if (localVideoController == null && imageFile == null) Container() else SizedBox( width: 64.0, height: 64.0, child: (localVideoController == null) ? ( kIsWeb ? Image.network(imageFile!.path) : Image.file(File(imageFile!.path))) : Container( decoration: BoxDecoration( border: Border.all(color: Colors.pink)), child: Center( child: AspectRatio( aspectRatio: localVideoController.value.aspectRatio, child: VideoPlayer(localVideoController)), ), ), ), ], ), ), ); } // ฟังก์ชั่นสำหรับสร้างปุ่มควบคุมรวมหลัก Widget _modeControlRowWidget() { return Column( children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ IconButton( icon: const Icon(Icons.flash_on), color: Colors.blue, onPressed: controller != null ? onFlashModeButtonPressed : null, ), ...!kIsWeb ? <Widget>[ IconButton( icon: const Icon(Icons.exposure), color: Colors.blue, onPressed: controller != null ? onExposureModeButtonPressed : null, ), IconButton( icon: const Icon(Icons.filter_center_focus), color: Colors.blue, onPressed: controller != null ? onFocusModeButtonPressed : null, ) ] : <Widget>[], IconButton( icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), color: Colors.blue, onPressed: controller != null ? onAudioModeButtonPressed : null, ), IconButton( icon: Icon(controller?.value.isCaptureOrientationLocked ?? false ? Icons.screen_lock_rotation : Icons.screen_rotation), color: Colors.blue, onPressed: controller != null ? onCaptureOrientationLockButtonPressed : null, ), ], ), _flashModeControlRowWidget(), _exposureModeControlRowWidget(), _focusModeControlRowWidget(), ], ); } // ฟังก์ชั่นสร้างปุ่มควบคุมเกี่ยวกับแฟลช Widget _flashModeControlRowWidget() { return SizeTransition( sizeFactor: _flashModeControlRowAnimation, child: ClipRect( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ IconButton( icon: const Icon(Icons.flash_off), color: controller?.value.flashMode == FlashMode.off ? Colors.orange : Colors.blue, onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.off) : null, ), IconButton( icon: const Icon(Icons.flash_auto), color: controller?.value.flashMode == FlashMode.auto ? Colors.orange : Colors.blue, onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.auto) : null, ), IconButton( icon: const Icon(Icons.flash_on), color: controller?.value.flashMode == FlashMode.always ? Colors.orange : Colors.blue, onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.always) : null, ), IconButton( icon: const Icon(Icons.highlight), color: controller?.value.flashMode == FlashMode.torch ? Colors.orange : Colors.blue, onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.torch) : null, ), ], ), ), ); } // ฟังก์ชั่นสร้างปุ่มควบคุมเกี่ยวกับการชดเชยแสง Widget _exposureModeControlRowWidget() { final ButtonStyle styleAuto = TextButton.styleFrom( foregroundColor: controller?.value.exposureMode == ExposureMode.auto ? Colors.orange : Colors.blue, ); final ButtonStyle styleLocked = TextButton.styleFrom( foregroundColor: controller?.value.exposureMode == ExposureMode.locked ? Colors.orange : Colors.blue, ); return SizeTransition( sizeFactor: _exposureModeControlRowAnimation, child: ClipRect( child: ColoredBox( color: Colors.grey.shade50, child: Column( children: <Widget>[ const Center( child: Text('Exposure Mode'), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ TextButton( style: styleAuto, onPressed: controller != null ? () => onSetExposureModeButtonPressed(ExposureMode.auto) : null, onLongPress: () { if (controller != null) { controller!.setExposurePoint(null); showInSnackBar('Resetting exposure point'); } }, child: const Text('AUTO'), ), TextButton( style: styleLocked, onPressed: controller != null ? () => onSetExposureModeButtonPressed(ExposureMode.locked) : null, child: const Text('LOCKED'), ), TextButton( style: styleLocked, onPressed: controller != null ? () => controller!.setExposureOffset(0.0) : null, child: const Text('RESET OFFSET'), ), ], ), const Center( child: Text('Exposure Offset'), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text(_minAvailableExposureOffset.toString()), Slider( value: _currentExposureOffset, min: _minAvailableExposureOffset, max: _maxAvailableExposureOffset, label: _currentExposureOffset.toString(), onChanged: _minAvailableExposureOffset == _maxAvailableExposureOffset ? null : setExposureOffset, ), Text(_maxAvailableExposureOffset.toString()), ], ), ], ), ), ), ); } // ฟังก์ชั่นสร้างปุ่มควบคุมเกี่ยวกับการโฟกัส Widget _focusModeControlRowWidget() { final ButtonStyle styleAuto = TextButton.styleFrom( foregroundColor: controller?.value.focusMode == FocusMode.auto ? Colors.orange : Colors.blue, ); final ButtonStyle styleLocked = TextButton.styleFrom( foregroundColor: controller?.value.focusMode == FocusMode.locked ? Colors.orange : Colors.blue, ); return SizeTransition( sizeFactor: _focusModeControlRowAnimation, child: ClipRect( child: ColoredBox( color: Colors.grey.shade50, child: Column( children: <Widget>[ const Center( child: Text('Focus Mode'), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ TextButton( style: styleAuto, onPressed: controller != null ? () => onSetFocusModeButtonPressed(FocusMode.auto) : null, onLongPress: () { if (controller != null) { controller!.setFocusPoint(null); } showInSnackBar('Resetting focus point'); }, child: const Text('AUTO'), ), TextButton( style: styleLocked, onPressed: controller != null ? () => onSetFocusModeButtonPressed(FocusMode.locked) : null, child: const Text('LOCKED'), ), ], ), ], ), ), ), ); } // ฟังก์ชั่นสร้างปุ่มควบคุมรูปภาพหรือวิดีโอ Widget _captureControlRowWidget() { final CameraController? cameraController = controller; return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ IconButton( icon: const Icon(Icons.camera_alt), color: Colors.blue, onPressed: cameraController != null && cameraController.value.isInitialized && !cameraController.value.isRecordingVideo ? onTakePictureButtonPressed : null, ), IconButton( icon: const Icon(Icons.videocam), color: Colors.blue, onPressed: cameraController != null && cameraController.value.isInitialized && !cameraController.value.isRecordingVideo ? onVideoRecordButtonPressed : null, ), IconButton( icon: cameraController != null && cameraController.value.isRecordingPaused ? const Icon(Icons.play_arrow) : const Icon(Icons.pause), color: Colors.blue, onPressed: cameraController != null && cameraController.value.isInitialized && cameraController.value.isRecordingVideo ? (cameraController.value.isRecordingPaused) ? onResumeButtonPressed : onPauseButtonPressed : null, ), IconButton( icon: const Icon(Icons.stop), color: Colors.red, onPressed: cameraController != null && cameraController.value.isInitialized && cameraController.value.isRecordingVideo ? onStopButtonPressed : null, ), IconButton( icon: const Icon(Icons.pause_presentation), color: cameraController != null && cameraController.value.isPreviewPaused ? Colors.red : Colors.blue, onPressed: cameraController == null ? null : onPausePreviewButtonPressed, ), ], ); } /// แถวแสดงข้อมูลกล้องให้เลือก ถ้ามี หรือแสดงข้อความถ้าไม่มีกล้องที่รองรับ Widget _cameraTogglesRowWidget() { final List<Widget> toggles = <Widget>[]; void onChanged(CameraDescription? description) { if (description == null) { return; } // เมื่อเลือกกล้องที่จะใช้งาน onNewCameraSelected(description); } if (!_cameraReady) { return const Text('กำลังเตรียมกล้อง..'); } else if (_cameras.isEmpty) { SchedulerBinding.instance.addPostFrameCallback((_) async { showInSnackBar('ไม่พบกล้องพร้อมใช้งาน.'); }); return const Text('ไม่พบกล้องพร้อมใช้งาน'); } else { for (final CameraDescription cameraDescription in _cameras) { toggles.add( SizedBox( width: 90.0, child: RadioListTile<CameraDescription>( title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), groupValue: controller?.description, value: cameraDescription, onChanged: onChanged, ), ), ); } } return Row(children: toggles); } // ฟังก์ชั่นเลือกกล้องเพื่อใช้งาน Future<void> onNewCameraSelected(CameraDescription cameraDescription) async { if (controller != null) { return controller!.setDescription(cameraDescription); } else { return _initializeCameraController(cameraDescription); } } // เตรียมตัวควบคุมกล้องตามกล้องที่เลือก Future<void> _initializeCameraController( CameraDescription cameraDescription) async { final CameraController cameraController = CameraController( cameraDescription, kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); controller = cameraController; // เมื่อตัวควบคุมมีการเปลี่ยนแปลง ให้อัปเดท UI cameraController.addListener(() { if (mounted) { setState(() {}); } if (cameraController.value.hasError) { showInSnackBar( 'Camera error ${cameraController.value.errorDescription}'); } }); try { await cameraController.initialize(); await Future.wait(<Future<Object?>>[ // The exposure mode is currently not supported on the web. ...!kIsWeb ? <Future<Object?>>[ cameraController.getMinExposureOffset().then( (double value) => _minAvailableExposureOffset = value), cameraController .getMaxExposureOffset() .then((double value) => _maxAvailableExposureOffset = value) ] : <Future<Object?>>[], cameraController .getMaxZoomLevel() .then((double value) => _maxAvailableZoom = value), cameraController .getMinZoomLevel() .then((double value) => _minAvailableZoom = value), ]); } on CameraException catch (e) { switch (e.code) { case 'CameraAccessDenied': showInSnackBar('You have denied camera access.'); case 'CameraAccessDeniedWithoutPrompt': // iOS only showInSnackBar('Please go to Settings app to enable camera access.'); case 'CameraAccessRestricted': // iOS only showInSnackBar('Camera access is restricted.'); case 'AudioAccessDenied': showInSnackBar('You have denied audio access.'); case 'AudioAccessDeniedWithoutPrompt': // iOS only showInSnackBar('Please go to Settings app to enable audio access.'); case 'AudioAccessRestricted': // iOS only showInSnackBar('Audio access is restricted.'); default: _showCameraException(e); break; } } if (mounted) { setState(() {}); } } // ฟังก์ชั่นคืนค่าข้อความเวลา ณ ปัจจุบัน String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); // ฟังก์ชั่นแสดงข้อความใน SnackBar void showInSnackBar(String message) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); } // ฟังก์ชันควบคุมเมื่อกดระบุตำแหน่งในจอที่จะทำการโฟกัสหรือการชดเชยแสง void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { if (controller == null) { return; } final CameraController cameraController = controller!; final Offset offset = Offset( details.localPosition.dx / constraints.maxWidth, details.localPosition.dy / constraints.maxHeight, ); cameraController.setExposurePoint(offset); cameraController.setFocusPoint(offset); } // ฟังก์ชั่นส่วนทำงานการ ถ่าย รูปภาพ void onTakePictureButtonPressed() { takePicture().then((XFile? file) { if (mounted) { setState(() { imageFile = file; videoController?.dispose(); videoController = null; }); if (file != null) { showInSnackBar('Picture saved to ${file.path}'); } } }); } // ฟังก์ชั่นควบคุมการซ่อนหรือแสดงปุ่มเกี่ยวกับแฟลช เมื่อปุ่มเลือกโหมดแฟลชถูกกด void onFlashModeButtonPressed() { // ตรวจสอบว่าแอนิเมชันของแถบควบคุมแฟลชเสร็จสิ้นหรือยัง // (value == 1 หมายถึงแอนิเมชันได้เสร็จสิ้นแล้ว) if (_flashModeControlRowAnimationController.value == 1) { // ถ้าแอนิเมชันของแถบควบคุมแฟลชเสร็จสิ้นแล้ว (แสดงว่าแถบแฟลชเปิดอยู่) // จะเรียกใช้ reverse() เพื่อเลื่อนแถบควบคุมแฟลชกลับไปปิด _flashModeControlRowAnimationController.reverse(); } else { // เรียกใช้ forward() เพื่อเริ่มแอนิเมชันแถบควบคุมแฟลชให้เลื่อนออกมา (เปิดแถบควบคุมแฟลช) _flashModeControlRowAnimationController.forward(); // ปิด (เลื่อนกลับ) แถบควบคุมการชดเชยแสง (Exposure) หากเปิดอยู่ _exposureModeControlRowAnimationController.reverse(); // ปิด (เลื่อนกลับ) แถบควบคุมโหมดโฟกัส หากเปิดอยู่ _focusModeControlRowAnimationController.reverse(); } } // ฟังก์ชั่นควบคุมการชดเชยแสง เมื่อปุ่มควบคุมการชดเชยแสงถูกกด void onExposureModeButtonPressed() { // ตรวจสอบว่าแถบควบคุมการชดเชยแสงถูกเปิดอยู่หรือไม่ // (value == 1 หมายถึงแถบควบคุมเปิดเต็มที่) if (_exposureModeControlRowAnimationController.value == 1) { // หากแถบควบคุมการชดเชยแสงเปิดอยู่ จะเลื่อนแถบนี้กลับไปปิด (ย้อนกลับแอนิเมชัน) _exposureModeControlRowAnimationController.reverse(); } else { // เริ่มแอนิเมชันเพื่อเปิดแถบควบคุมการชดเชยแสง _exposureModeControlRowAnimationController.forward(); // หากแถบควบคุมแฟลชเปิดอยู่ จะปิด (เลื่อนกลับ) แถบควบคุมแฟลชลง _flashModeControlRowAnimationController.reverse(); // หากแถบควบคุมโหมดโฟกัสเปิดอยู่ จะปิด (เลื่อนกลับ) แถบควบคุมโหมดโฟกัสลง _focusModeControlRowAnimationController.reverse(); } } // ตรวจสอบว่าแถบควบคุมโหมดโฟกัสถูกเปิดอยู่หรือไม่ // (value == 1 หมายถึงแถบควบคุมเปิดเต็มที่) void onFocusModeButtonPressed() { if (_focusModeControlRowAnimationController.value == 1) { // ถ้าแถบควบคุมโหมดโฟกัสเปิดอยู่ จะเลื่อนแถบนี้กลับลงไปปิด (ย้อนกลับแอนิเมชัน) _focusModeControlRowAnimationController.reverse(); } else { // เริ่มแอนิเมชันเพื่อเปิดแถบควบคุมโหมดโฟกัส _focusModeControlRowAnimationController.forward(); // ถ้าแถบควบคุมแฟลชเปิดอยู่ จะปิด (เลื่อนกลับ) แถบควบคุมแฟลชลง _flashModeControlRowAnimationController.reverse(); // ถ้าแถบควบคุมการชดเชยแสงเปิดอยู่ จะปิด (เลื่อนกลับ) แถบควบคุมการชดเชยแสงลง _exposureModeControlRowAnimationController.reverse(); } } // เปลี่ยนค่าของตัวแปร enableAudio ให้ตรงข้ามกับค่าปัจจุบัน //(ถ้าเปิดเสียงอยู่ก็จะปิด ถ้าปิดอยู่ก็จะเปิด) void onAudioModeButtonPressed() { enableAudio = !enableAudio; if (controller != null) { // เรียกฟังก์ชัน onNewCameraSelected โดยส่งคำอธิบายของกล้องที่ใช้อยู่ (description) // ไปเป็นพารามิเตอร์ เพื่อเลือกกล้องใหม่และอัปเดตการตั้งค่าเสียงตามสถานะของ enableAudio onNewCameraSelected(controller!.description); } } // เมื่อกดปุ่มควบคุมการล็อกแนวกล้อง Future<void> onCaptureOrientationLockButtonPressed() async { try { if (controller != null) { final CameraController cameraController = controller!; if (cameraController.value.isCaptureOrientationLocked) { await cameraController.unlockCaptureOrientation(); showInSnackBar('Capture orientation unlocked'); } else { await cameraController.lockCaptureOrientation(); showInSnackBar( 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); } } } on CameraException catch (e) { _showCameraException(e); } } // ฟังก์ชั่นสำหรับควบคุมปุ่มกดการตั้งค่าแฟลช void onSetFlashModeButtonPressed(FlashMode mode) { setFlashMode(mode).then((_) { if (mounted) { setState(() {}); } showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); }); } // ฟังก์ชั่นสำหรับกำหนดการชดเชยค่าแสง void onSetExposureModeButtonPressed(ExposureMode mode) { setExposureMode(mode).then((_) { if (mounted) { setState(() {}); } showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); }); } // ฟังก์ชั่นควบคุมการโฟกัสของกล้อง void onSetFocusModeButtonPressed(FocusMode mode) { setFocusMode(mode).then((_) { if (mounted) { setState(() {}); } showInSnackBar('Focus mode set to ${mode.toString().split('.').last}'); }); } // ฟังก์ชั่นควบคุมการเริ่มบันทึกวิดีโอ void onVideoRecordButtonPressed() { startVideoRecording().then((_) { if (mounted) { setState(() {}); } }); } // ฟังก์ชั่นควบคุมการหยุดบันทึกวิดีโอ void onStopButtonPressed() { stopVideoRecording().then((XFile? file) { if (mounted) { setState(() {}); } if (file != null) { showInSnackBar('Video recorded to ${file.path}'); videoFile = file; _startVideoPlayer(); } }); } // ฟังก์ชันสำหรับการหยุถพรีวิววิดีโอจากกล้อง Future<void> onPausePreviewButtonPressed() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { showInSnackBar('Error: select a camera first.'); return; } if (cameraController.value.isPreviewPaused) { await cameraController.resumePreview(); } else { await cameraController.pausePreview(); } if (mounted) { setState(() {}); } } // ฟังก์ชันสำหรับควบคุมการหยุดการบันทึกวิดีโอชั่วคราว void onPauseButtonPressed() { pauseVideoRecording().then((_) { if (mounted) { setState(() {}); } showInSnackBar('Video recording paused'); }); } // ฟังก์ชั่นสำหรับควบคุมการกลับไปบันทึกวิดีโอต่อหลังจากหยุดชั่วคราว void onResumeButtonPressed() { resumeVideoRecording().then((_) { if (mounted) { setState(() {}); } showInSnackBar('Video recording resumed'); }); } // ฟังก์ชั่นเริ่มการทำงานการบันทึกวิดีโอ Future<void> startVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { showInSnackBar('Error: select a camera first.'); return; } if (cameraController.value.isRecordingVideo) { // A recording is already started, do nothing. return; } try { await cameraController.startVideoRecording(); } on CameraException catch (e) { _showCameraException(e); return; } } // ฟังก์ชันสำหรับหยุดการบันทึกวิดีโอ Future<XFile?> stopVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isRecordingVideo) { return null; } try { return cameraController.stopVideoRecording(); } on CameraException catch (e) { _showCameraException(e); return null; } } // ฟังก์ชั่นสำหรับหยุดการบันทึกวิดีโอชั่วคราว Future<void> pauseVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isRecordingVideo) { return; } try { await cameraController.pauseVideoRecording(); } on CameraException catch (e) { _showCameraException(e); rethrow; } } // ฟังก์ชั่นสำหรับกลับไปบันทึกวิดีโอต่อ หลังจากหยุดชั่วคราว Future<void> resumeVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isRecordingVideo) { return; } try { await cameraController.resumeVideoRecording(); } on CameraException catch (e) { _showCameraException(e); rethrow; } } // ฟังก์ชั่นสำหรับตั้งค่าแฟลช Future<void> setFlashMode(FlashMode mode) async { if (controller == null) { return; } try { await controller!.setFlashMode(mode); } on CameraException catch (e) { _showCameraException(e); rethrow; } } // ฟังก์ชั่นสำหรับตั้งค่าการชดเชยแสง Future<void> setExposureMode(ExposureMode mode) async { if (controller == null) { return; } try { await controller!.setExposureMode(mode); } on CameraException catch (e) { _showCameraException(e); rethrow; } } // ฟังก์ชั่นปรับระดับการชดเชยแสงของกล้อง Future<void> setExposureOffset(double offset) async { if (controller == null) { return; } setState(() { _currentExposureOffset = offset; }); try { offset = await controller!.setExposureOffset(offset); } on CameraException catch (e) { _showCameraException(e); rethrow; } } // ฟังก์ชั่นตั้งค่าการโฟกัส Future<void> setFocusMode(FocusMode mode) async { if (controller == null) { return; } try { await controller!.setFocusMode(mode); } on CameraException catch (e) { _showCameraException(e); rethrow; } } // ฟังก์ชั่นสำหรับเล่นวิดีโอ Future<void> _startVideoPlayer() async { if (videoFile == null) { return; } final VideoPlayerController vController = kIsWeb ? VideoPlayerController.networkUrl(Uri.parse(videoFile!.path)) : VideoPlayerController.file(File(videoFile!.path)); videoPlayerListener = () { if (videoController != null) { // Refreshing the state to update video player with the correct ratio. if (mounted) { setState(() {}); } videoController!.removeListener(videoPlayerListener!); } }; vController.addListener(videoPlayerListener!); await vController.setLooping(true); await vController.initialize(); await videoController?.dispose(); if (mounted) { setState(() { imageFile = null; videoController = vController; }); } await vController.play(); } // ฟังก์ชั่นสำหรับถ่ายรูปด้วยกล้อง คืนค่าเป็นไฟล์กลับออกไป Future<XFile?> takePicture() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { showInSnackBar('Error: select a camera first.'); return null; } if (cameraController.value.isTakingPicture) { // A capture is already pending, do nothing. return null; } try { final XFile file = await cameraController.takePicture(); return file; } on CameraException catch (e) { _showCameraException(e); return null; } } // ฟังก์ชั่นจัดการข้อผิดพลาดหรือการยกเว้นกรณีเกิดข้อผิดพลาดในการทำงาน void _showCameraException(CameraException e) { _logError(e.code, e.description); showInSnackBar('Error: ${e.code}n${e.description}'); } } List<CameraDescription> _cameras = <CameraDescription>[]; /// จัดการไอคอนของกล้องที่เหมาะสม [direction]. 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; } // ignore: dead_code return Icons.camera; } void _logError(String code, String? message) { // ignore: avoid_print print('Error: $code${message == null ? '' : 'nError Message: $message'}'); }
ผลลัพธ์ที่ได้
โค้ดข้างต้นเป็นโค้ดการปรับแต่งและการใช้งานทั้งหมดของ plugin camera ที่เราสามารถปรับแต่ง
ส่วนต่างๆ ได้เอง สามารถเลือกส่วนที่ต้องการไปปรับใช้งานได้
*จากที่ทดสอบการทำงานยังมี bug ในส่วนของการหยุดภาพพรีวิว กับการกำหนดเรื่องการชดเชยแสง
จากการใช้งานทั้งสองแพ็กเก็จ ทั้ง image_picker และ camera ที่ใช้สำหรับบันภาพหรือวิดีโอ
จะเห็นว่าตัว image_picker จะใช้งานง่ายและสะดวกกว่า กรณีเราต้องการความเรียบง่าย ไม่ต้อง
ปรับแต่งอะไรเยอะ เพราะใช้การจัดการของกล้องของระบบ ในขณะที่ถ้าเราใช้งาน camera หากต้อง
การปรับแต่งค่าๆ ต่างเป็นแบบที่ต้องการก็จะสามารถทำได้มากกว่า แต่ก็จะยากและซับซ้อนเพิ่มชึ้น
เนื้อหานี้เรานำเสนอให้เข้าใจถึงแนวทางและวิธีการใช้งานการบันทึกไฟล์วิดีโอใน flutter สำหรับนำ
ไปปรับใช้กับโปรเจ็คแอปของเรา กรณีที่ต้องการจัดการในส่วนนี้ หวังว่าจะเป็นประโยชน์ ไม่มากก็น้อย
สำหรับในตอนหน้าจะเป็นอะไร รอติดตาม