赞
踩
flutter开发实战-Camera自定义相机拍照功能实现
在项目中使用image_picker插件时候,在android设备上使用无法默认设置前置摄像头(暂时不清楚什么原因),由于项目默认需要使用前置摄像头,所以最终采用自定义相机实现拍照功能。
在工程的iOS的info.plist文件中添加相机、麦克风权限描述
<key>NSCameraUsageDescription</key>
<string>your usage description here</string>
<key>NSMicrophoneUsageDescription</key>
<string>your usage description here</string>
在工程的Android的gradle设置minSdkVersion
找到android/app/build.gradle文件
minSdkVersion 21
camera : 适用于iOS、Android和Web的Flutter插件,允许访问设备摄像头。
我们需要在工程中引入camera插件
pubspec.yaml中引入插件
# Camera相机拍照等
camera: ^0.10.5+5
处理相机访问权限
在初始化相机控制器时可能会引发权限错误,需要处理这些错误。
CameraAccessDenied:当用户拒绝相机访问权限时抛出。
CameraAccessDeniedWithoutPrompt:仅限iOS。当用户先前拒绝该权限时抛出。iOS不允许再次提示警报对话框。用户必须进入“设置”>“隐私”>“相机”才能访问相机。
CameraAccessRestricted:仅限iOS。当摄像头访问受到限制且用户无法授予权限(家长控制)时抛出。
AudioAccessDenied:当用户拒绝音频访问权限时抛出。
AudioAccessDeniedWithoutPrompt:目前仅限iOS。当用户先前拒绝该权限时抛出。iOS不允许再次提示警报对话框。用户必须转到“设置”>“隐私”>“麦克风”才能启用音频访问。
AudioAccessRestricted:目前仅限iOS。当音频访问受到限制并且用户无法授予权限(家长控制)时抛出。
当使用camera时,我们需要设置一些camera的属性内容,比如切换前后摄像头、开启拍照、开启预览、停止预览等。
获取cameras
final cameras = await availableCameras();
camera中使用CameraController来控制相关功能。
设置缩放级别zoomLevel
Future<void> setZoomLevel(double scale) async {
await controller!.setZoomLevel(scale);
}
切换闪光灯模式
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}');
});
}
开启预览
Future<void> onResumePreview() async {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
print('Error: select a camera first.');
return;
}
if (cameraController.value.isPreviewPaused) {
await cameraController.resumePreview();
}
}
暂停预览
Future<void> onPausePreview() async {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
print('Error: select a camera first.');
return;
}
if (!cameraController.value.isPreviewPaused) {
await cameraController.pausePreview();
}
}
切换前后摄像头
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); } Future<void> onNewCameraSelected(CameraDescription cameraDescription) async { final CameraController cameraController = CameraController( cameraDescription, ResolutionPreset.high, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); controller = cameraController; // If the controller is updated then update the UI. cameraController.addListener(() { if (mounted) { setState(() {}); } if (cameraController.value.hasError) { print("Camera error ${cameraController.value.errorDescription}"); } }); try { await cameraController.initialize(); await Future.wait(<Future<Object>>[ // The exposure mode is currently not supported on the web. cameraController .getMaxZoomLevel() .then((double value) => _maxAvailableZoom = value), cameraController .getMinZoomLevel() .then((double value) => _minAvailableZoom = value), ]); } on CameraException catch (e) { // _showCameraException(e); } setState(() { isCameraStarting = true; }); controller!.initialize().then((_) { if (!mounted) { return; } setState(() { isCameraStarting = false; }); }).catchError((Object e) { if (e is CameraException) { switch (e.code) { case 'CameraAccessDenied': // Handle access errors here. break; default: // Handle other errors here. break; } } }); if (mounted) { setState(() {}); } }
上面介绍了一些CameraController的常用设置,当然肯定不全,大致列了几条。
我们自定义Camera,需要在didChangeAppLifecycleState来处理相机。我们需要添加mixin WidgetsBindingObserver
在initState中添加WidgetsBinding.instance?.addObserver(this);
在dispose中移除WidgetsBinding.instance?.removeObserver(this);
这样我们就可以在app的生命周期状态改变时候,更新相机
@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) { onNewCameraSelected(cameraController.description); } }
在处理自定义相机功能,我们需要处理预览的画面出现变形的问题。这里我们需要使用CameraPreview。
我们需要使用Transform.scale来进行处理,处理预览的画面出现变形的问题的解决代码如下
Widget buildCameraPreviewWidget(BuildContext context) { final Size size = MediaQuery.of(context).size; final CameraController? cameraController = controller; return Container( width: size.width, height: size.height, child: Stack( alignment: Alignment.center, clipBehavior: Clip.hardEdge, children: [ RepaintBoundary( key: _cameraViewGlobalKey, child: Transform.scale( scale: 1.0, // scale: controller!.value.aspectRatio / deviceRatio, alignment: Alignment.center, child: AspectRatio( aspectRatio: size.aspectRatio, child: OverflowBox( alignment: Alignment.center, child: FittedBox( fit: BoxFit.fitHeight, child: SizedBox( width: size.width, height: size.width * cameraController!.value.aspectRatio, child: Stack(fit: StackFit.expand, children: <Widget>[ _cameraPreviewWidget(), ]), ), ), ), ), ), ), ], ), ); } /// Display the preview from the camera (or a message if the preview is not available). Widget _cameraPreviewWidget() { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { return const Text( 'cameraController未初始化完成', 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), ); }), ), ); } }
在代码中,我们使用Transform.scale设置为1.0,当设置AspectRatio来设置size.aspectRatio。
在我们代码中,我们使用takePicture来实现拍照,拍照代码如下
Future<void> onTakePicture() async { setState(() { isTaking = true; }); takePicture().then((XFile? file) async { if (mounted) { onPausePreview(); if (file != null) { // 保存到相册 // await SaveToAlbumUtil.saveLocalImage(file.path); RenderBox renderBox = _cameraContainerGlobalKey.currentContext! .findRenderObject() as RenderBox; // offset.dx , offset.dy 就是控件的左上角坐标 Offset offset = renderBox.localToGlobal(Offset.zero); //获取size Size size = renderBox.size; // 创建文件path String imageDir = await PathUtil.createDirectory("local_images"); String imagePath = '$imageDir/${TimeUtil.currentTimeMillis()}.png'; // // 获取当前设备的像素比 double dpr = ui.window.devicePixelRatio; print("devicePixelRatio:${dpr}"); print( "offset:(${offset.dx},${offset.dy})--size:(${size.width},${size.height})"); File? targetFile = await ImageUtil.cropImage( file.path, imagePath, x: (dpr * offset.dx).floor(), y: (dpr * offset.dy).floor(), width: (dpr * size.width).ceil(), height: (dpr * size.height).ceil(), flipHorizontal: isCameraFront, ); print("cropImage targetFile:${targetFile}"); if (targetFile != null) { selectedImagePath = targetFile.path; // await SaveToAlbumUtil.saveLocalImage(targetFile.path); } setState(() { isHasTakePhoto = true; }); } else { // 没有获得图片,重试 } setState(() { isTaking = false; }); } }); }
在裁剪图片中实现如下
import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; import 'dart:math' as math; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:image/image.dart' as IMG; class ImageUtil { //拿到图片的字节数组 static Future<ui.Image> loadImageByFile(String path) async { var list = await File(path).readAsBytes(); return ImageUtil.loadImageByUInt8List(list); } //通过[Uint8List]获取图片 static Future<ui.Image> loadImageByUInt8List(Uint8List list) async { ui.Codec codec = await ui.instantiateImageCodec(list); ui.FrameInfo frame = await codec.getNextFrame(); return frame.image; } // 根据GlobalKey来截图Widget static Future<Uint8List?> makeImageUInt8List(GlobalKey globalKey) async { RenderRepaintBoundary boundary = globalKey.currentContext?.findRenderObject() as RenderRepaintBoundary; // 这个可以获取当前设备的像素比 var dpr = ui.window.devicePixelRatio; ui.Image image = await boundary.toImage(pixelRatio: dpr); ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); Uint8List? pngBytes = byteData?.buffer.asUint8List(); return pngBytes; } static Future<File?> cropSquare( String srcFilePath, String destFilePath, bool flip) async { var bytes = await File(srcFilePath).readAsBytes(); IMG.Image? src = IMG.decodeImage(bytes); if (src != null) { var cropSize = min(src.width, src.height); int offsetX = (src.width - min(src.width, src.height)) ~/ 2; int offsetY = (src.height - min(src.width, src.height)) ~/ 2; // IMG.Image destImage = IMG.copyCrop(src, offsetX, offsetY, cropSize, cropSize); IMG.Image destImage = IMG.copyCrop(src, x: offsetX, y: offsetY, width: cropSize, height: cropSize); if (flip) { destImage = IMG.flipVertical(destImage); } var jpg = IMG.encodeJpg(destImage); return await File(destFilePath).writeAsBytes(jpg); } else { throw StateError("cropSquare error"); } } static Future<File?> cropImage( String srcFilePath, String destFilePath, { required int x, required int y, required int width, required int height, bool flipVertical = false, bool flipHorizontal = false, }) async { var bytes = await File(srcFilePath).readAsBytes(); IMG.Image? src = IMG.decodeImage(bytes); if (src != null) { print("cropImage scr size:(${src.width},${src.height})"); IMG.Image destImage = IMG.copyCrop(src, x: x, y: y, width: width, height: height); if (flipVertical) { destImage = IMG.flipVertical(destImage); } if (flipHorizontal) { destImage = IMG.flipHorizontal(destImage); } var jpg = IMG.encodeJpg(destImage); return await File(destFilePath).writeAsBytes(jpg); } else { throw StateError("cropSquare error"); } } }
当拍照后可能需要重新拍照,这时候我们需要重拍逻辑。
void onRetakeButtonPressed() { setState(() { isHasTakePhoto = false; }); selectedImagePath = null; onResumePreview(); } Future<void> onResumePreview() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { print('Error: select a camera first.'); return; } if (cameraController.value.isPreviewPaused) { await cameraController.resumePreview(); } }
我们实现了实现自定义相机拍照的功能完整代码如下
// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: public_member_api_docs import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_app_demolab/image_util.dart'; import 'package:flutter_app_demolab/path_util.dart'; import 'dart:ui' as ui; import 'package:flutter_app_demolab/tools/utils/color_util.dart'; import 'package:flutter_app_demolab/tools/utils/time_util.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; class MyCameraPage extends StatefulWidget { const MyCameraPage({ super.key, required this.cameras, required this.onSelectedImagePathPressed, }); final List<CameraDescription> cameras; final Function(String? selectedImagePath) onSelectedImagePathPressed; @override State<MyCameraPage> createState() => _MyCameraPageState(); } class _MyCameraPageState extends State<MyCameraPage> with WidgetsBindingObserver, TickerProviderStateMixin { CameraController? controller; GlobalKey _cameraViewGlobalKey = GlobalKey(); GlobalKey _cameraContainerGlobalKey = GlobalKey(); bool enableAudio = false; // Counting pointers (number of user fingers on screen) ///以下是关于手指缩放画面的变量 int _pointers = 0; double _minAvailableZoom = 1.0; double _maxAvailableZoom = 1.0; double _currentScale = 1.0; double _baseScale = 1.0; Size? mediaSize; double? scale; double? defaultZoomLevel; bool isHasTakePhoto = false; bool isCameraFront = true; String? selectedImagePath; bool isTaking = false; bool isCameraStarting = false; @override void initState() { super.initState(); // To display the current output from the Camera, // create a CameraController. if (widget.cameras.isNotEmpty && widget.cameras.length >= 2) { controller = CameraController( // Get a specific camera from the list of available cameras. widget.cameras[1], // Define the resolution to use. ResolutionPreset.high, ); // Next, initialize the controller. This returns a Future. setState(() { isCameraStarting = true; }); controller!.initialize().then((_) { if (!mounted) { return; } setState(() { isCameraStarting = false; }); }).catchError((Object e) { if (e is CameraException) { switch (e.code) { case 'CameraAccessDenied': // Handle access errors here. break; default: // Handle other errors here. break; } } }); } WidgetsBinding.instance?.addObserver(this); } @override void dispose() { WidgetsBinding.instance?.removeObserver(this); controller?.dispose(); super.dispose(); } @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) { onNewCameraSelected(cameraController.description); } } final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); @override Widget build(BuildContext context) { return Scaffold( key: _scaffoldKey, body: buildCameraContainer(context), ); } Widget buildCameraContainer(BuildContext context) { final Size size = MediaQuery.of(context).size; if (widget.cameras.isEmpty) { return Container( width: size.width, height: size.height, decoration: const BoxDecoration( color: Colors.black, ), child: Text( "未获取到可用的相机,请退出重试。", textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, softWrap: true, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, fontStyle: FontStyle.normal, color: ColorUtil.hexColor(0xffffff), decoration: TextDecoration.none, ), ), ); } else { return Container( key: _cameraContainerGlobalKey, width: size.width, height: size.height, decoration: const BoxDecoration( color: Colors.black, ), child: Stack( alignment: Alignment.center, children: [ Column( children: [ Expanded( child: buildFutureBuilder(context), ) ], ), buildStackBarWidget(context), ], ), ); } } Widget buildFutureBuilder(BuildContext context) { if (controller != null && controller!.value.isInitialized) { ///初始化完成以后,再获取可以缩放画面最大最小的参数 mediaSize = MediaQuery.of(context).size; scale = 1 / (controller!.value.aspectRatio * mediaSize!.aspectRatio); controller! .getMaxZoomLevel() .then((double value) => _maxAvailableZoom = value); controller! .getMinZoomLevel() .then((double value) => _minAvailableZoom = value); return buildCameraPreviewWidget(context); } return const Center(child: CircularProgressIndicator()); } Widget buildStackBarWidget(BuildContext context) { final Size size = MediaQuery.of(context).size; double bottomBarHeight = 120; double cameraHeight = size.height - bottomBarHeight; EdgeInsets viewPadding = MediaQuery.of(context).viewPadding; return Container( child: Stack( children: [ Positioned( bottom: 0, child: Container( width: size.width, height: bottomBarHeight, color: Colors.transparent, child: Stack( alignment: Alignment.center, children: [ Positioned( left: 25, child: buildCloseIcon(context), ), buildTakePhotoButton(context), Positioned( right: 25, child: buildRetakeButton(context), ), ], ), ), ), Positioned( top: viewPadding.top + 25, right: 10, child: buildExchangeButton(context), ), ], ), ); } Widget buildCameraPreviewWidget(BuildContext context) { final Size size = MediaQuery.of(context).size; final CameraController? cameraController = controller; return Container( width: size.width, height: size.height, child: Stack( alignment: Alignment.center, clipBehavior: Clip.hardEdge, children: [ RepaintBoundary( key: _cameraViewGlobalKey, child: Transform.scale( scale: 1.0, // scale: controller!.value.aspectRatio / deviceRatio, alignment: Alignment.center, child: AspectRatio( aspectRatio: size.aspectRatio, child: OverflowBox( alignment: Alignment.center, child: FittedBox( fit: BoxFit.fitHeight, child: SizedBox( width: size.width, height: size.width * cameraController!.value.aspectRatio, child: Stack(fit: StackFit.expand, children: <Widget>[ _cameraPreviewWidget(), ]), ), ), ), ), ), ), ], ), ); } /// Display the preview from the camera (or a message if the preview is not available). Widget _cameraPreviewWidget() { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { return const Text( 'cameraController未初始化完成', 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), ); }), ), ); } } Widget buildCloseIcon(BuildContext context) { return GestureDetector( onTap: () { Navigator.pop(context); }, child: Container( color: Colors.transparent, child: Container( width: 50, height: 50, decoration: BoxDecoration( color: Colors.transparent, border: Border.all( color: Colors.transparent, style: BorderStyle.solid, width: 1, ), borderRadius: BorderRadius.all(Radius.circular(20)), ), child: Icon( Icons.close, size: 30, color: Colors.white, weight: 0.5, ), ), ), ); } Widget buildTakePhotoButton(BuildContext context) { return GestureDetector( onTap: () { if (isTaking == false) { if (isHasTakePhoto == true) { widget.onSelectedImagePathPressed(selectedImagePath); Navigator.pop(context); } else { onTakePicturePressed(); } } }, child: Container( color: Colors.transparent, child: Container( width: 60, height: 60, decoration: const BoxDecoration( color: Colors.transparent, ), child: Stack( alignment: Alignment.center, children: [ Image.asset( "assets/camera/my_take_photo.png", width: 60.0, height: 60.0, fit: BoxFit.contain, ), buildHasCheck(context), ], ), ), ), ); } Widget buildHasCheck(BuildContext context) { if (isTaking == true) { return buildLoading(context); } if (isHasTakePhoto) { return Icon( Icons.check, size: 30, color: Colors.black, weight: 0.5, ); } return Container(); } Widget buildExchangeButton(BuildContext context) { if (isHasTakePhoto == true) { return Container(); } return GestureDetector( onTap: () { onExchangeCameraPressed(); }, child: Container( color: Colors.transparent, child: Container( width: 50, height: 50, decoration: BoxDecoration( color: Colors.transparent, border: Border.all( color: Colors.transparent, style: BorderStyle.solid, width: 2, ), borderRadius: BorderRadius.all(Radius.circular(20)), ), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.transparent, border: Border.all( color: Colors.transparent, style: BorderStyle.solid, width: 5, ), borderRadius: BorderRadius.all(Radius.circular(20)), ), child: Image.asset( "assets/camera/my_exchange_camera.png", width: 50.0, height: 50.0, fit: BoxFit.contain, ), ), ), ), ); } Widget buildRetakeButton(BuildContext context) { if (isHasTakePhoto == false) { return Container(); } return GestureDetector( onTap: () { onRetakeButtonPressed(); }, child: Container( color: Colors.transparent, child: Container( width: 70, height: 38, alignment: Alignment.center, decoration: BoxDecoration( color: ColorUtil.hexColor(0x000000, alpha: 0.25), border: Border.all( color: Colors.transparent, style: BorderStyle.solid, width: 2, ), borderRadius: BorderRadius.all(Radius.circular(5)), ), child: Text( "重拍", textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, softWrap: true, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, fontStyle: FontStyle.normal, color: ColorUtil.hexColor(0xffffff), decoration: TextDecoration.none, ), ), ), ), ); } Widget buildLoading(BuildContext context) { return SizedBox( height: 58, width: 58, child: CircularProgressIndicator( backgroundColor: Colors.grey[200], valueColor: AlwaysStoppedAnimation(Colors.blue), ), ); } void onRetakeButtonPressed() { setState(() { isHasTakePhoto = false; }); selectedImagePath = null; onResumePreview(); } Future<void> onPausePreview() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { print('Error: select a camera first.'); return; } if (!cameraController.value.isPreviewPaused) { await cameraController.pausePreview(); } } Future<void> onResumePreview() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { print('Error: select a camera first.'); return; } if (cameraController.value.isPreviewPaused) { await cameraController.resumePreview(); } } Future<void> onExchangeCameraPressed() async { setState(() { isHasTakePhoto = false; }); if (isCameraFront == true) { if (widget.cameras.isNotEmpty && widget.cameras.length >= 2) { onNewCameraSelected(widget.cameras[0]); } isCameraFront = false; } else { if (widget.cameras.isNotEmpty && widget.cameras.length >= 2) { onNewCameraSelected(widget.cameras[1]); } isCameraFront = true; } } void onTakePicturePressed() { onTakePicture(); } Future<void> onTakePicture() async { setState(() { isTaking = true; }); takePicture().then((XFile? file) async { if (mounted) { onPausePreview(); if (file != null) { // 保存到相册 // await SaveToAlbumUtil.saveLocalImage(file.path); RenderBox renderBox = _cameraContainerGlobalKey.currentContext! .findRenderObject() as RenderBox; // offset.dx , offset.dy 就是控件的左上角坐标 Offset offset = renderBox.localToGlobal(Offset.zero); //获取size Size size = renderBox.size; // 创建文件path String imageDir = await PathUtil.createDirectory("local_images"); String imagePath = '$imageDir/${TimeUtil.currentTimeMillis()}.png'; // // 获取当前设备的像素比 double dpr = ui.window.devicePixelRatio; print("devicePixelRatio:${dpr}"); print( "offset:(${offset.dx},${offset.dy})--size:(${size.width},${size.height})"); File? targetFile = await ImageUtil.cropImage( file.path, imagePath, x: (dpr * offset.dx).floor(), y: (dpr * offset.dy).floor(), width: (dpr * size.width).ceil(), height: (dpr * size.height).ceil(), flipHorizontal: isCameraFront, ); print("cropImage targetFile:${targetFile}"); if (targetFile != null) { selectedImagePath = targetFile.path; // await SaveToAlbumUtil.saveLocalImage(targetFile.path); } setState(() { isHasTakePhoto = true; }); } else { // 没有获得图片,重试 } setState(() { isTaking = false; }); } }); } Future<void> _handleScaleStart(ScaleStartDetails details) async { _baseScale = _currentScale; await controller!.setZoomLevel(_minAvailableZoom); } Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async { // When there are not exactly two fingers on screen don't scale if (controller == null || _pointers != 2) { return; } _currentScale = (_baseScale * details.scale) .clamp(_minAvailableZoom, _maxAvailableZoom); await controller!.setZoomLevel(_currentScale); } 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); } Future<void> onNewCameraSelected(CameraDescription cameraDescription) async { final CameraController cameraController = CameraController( cameraDescription, ResolutionPreset.high, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); controller = cameraController; // If the controller is updated then update the UI. cameraController.addListener(() { if (mounted) { setState(() {}); } if (cameraController.value.hasError) { print("Camera error ${cameraController.value.errorDescription}"); } }); try { await cameraController.initialize(); await Future.wait(<Future<Object>>[ // The exposure mode is currently not supported on the web. cameraController .getMaxZoomLevel() .then((double value) => _maxAvailableZoom = value), cameraController .getMinZoomLevel() .then((double value) => _minAvailableZoom = value), ]); } on CameraException catch (e) { // _showCameraException(e); } setState(() { isCameraStarting = true; }); controller!.initialize().then((_) { if (!mounted) { return; } setState(() { isCameraStarting = false; }); }).catchError((Object e) { if (e is CameraException) { switch (e.code) { case 'CameraAccessDenied': // Handle access errors here. break; default: // Handle other errors here. break; } } }); if (mounted) { setState(() {}); } } Future<XFile?> takePicture() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { print("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) { print("takePicture CameraException e:${e.toString()}"); return null; } } }
当需要拍照时候,我们调用showModalBottomSheet来打开camera
//显示底部弹窗 static void bottomSheetDialog(BuildContext context, Widget widget) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (ctx) { return widget; }, ); } //返回上一级 static void pop(BuildContext context) { Navigator.pop(context); }
打开自定义相机页面
Future<void> testCustomCamera(BuildContext context) async { final cameras = await availableCameras(); DialogUtils.bottomSheetDialog( context, MyCameraPage( cameras: cameras, onSelectedImagePathPressed: (String? selectedImagePath) { print("selectedImageFilePath:${selectedImagePath}"); if (selectedImagePath != null) { // File imageFile = File(selectedImagePath!); // if (callback != null) { // callback(imageFile); // } } }, ), ); }
https://brucegwo.blog.csdn.net/article/details/135997096
flutter开发实战-Camera自定义相机拍照功能实现
学习记录,每天不停进步。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。