flutter开发实战-Camera自定义相机拍照功能实现

flutter开发实战-Camera自定义相机拍照功能实现
在这里插入图片描述

一、前言

在项目中使用image_picker插件时候,在android设备上使用无法默认设置前置摄像头(暂时不清楚什么原因),由于项目默认需要使用前置摄像头,所以最终采用自定义相机实现拍照功能。

二、Camera使用前设置

在工程的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插件

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。当音频访问受到限制并且用户无法授予权限(家长控制)时抛出。

2.1、camera功能设置

当使用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的常用设置,当然肯定不全,大致列了几条。

2.2、WidgetsBinding 生命周期改变相机设置

我们自定义Camera,需要在didChangeAppLifecycleState来处理相机。我们需要添加mixin WidgetsBindingObserver

在initState中添加WidgetsBinding.instance?.addObserver(this);

在dispose中移除WidgetsBinding.instance?.removeObserver(this);

这样我们就可以在app的生命周期状态改变时候,更新相机

  @overridevoid 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);}}

2.3、处理预览的画面出现变形的问题

在处理自定义相机功能,我们需要处理预览的画面出现变形的问题。这里我们需要使用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。

2.4、实现拍照功能

在我们代码中,我们使用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);//获取sizeSize size = renderBox.size;// 创建文件pathString 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来截图Widgetstatic 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");}}
}

2.5、拍照完重拍逻辑

当拍照后可能需要重新拍照,这时候我们需要重拍逻辑。

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_docsimport '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;@overrideState<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;@overridevoid 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);}@overridevoid dispose() {WidgetsBinding.instance?.removeObserver(this);controller?.dispose();super.dispose();}@overridevoid 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>();@overrideWidget 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);//获取sizeSize size = renderBox.size;// 创建文件pathString 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 scaleif (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自定义相机拍照功能实现

学习记录,每天不停进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/664255.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

面试经典150题 -- 区间(总结)

总的链接 : 面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台最经典 150 题&#xff0c;掌握面试所有知识点https://leetcode.cn/studyplan/top-interview-150/ 228 汇总区间 直接用双指针模拟即可 ; class Solution { public…

Linux 指令

Linux 指令 1 登入/登出1.1 关机1.2 重启1.3 切用户 2 文件和目录管理2.1 目录操作2.2 文件操作2.3 文件内容操作2.4 归档及压缩 3 文本编辑器 vim3.1 命令模式3.2 输入模式3.3 末行模式 4 用户和组管理4.1 用户和组帐号概述4.1.1 用户帐号4.1.2 UID和GID 4.2 用户管理 5 组管理…

拥抱个人成长与社会进步:自我认知与开放心态的相互影响

拥抱个人成长与社会进步&#xff1a;自我认知与开放心态的相互影响 Embracing Personal Growth and Societal Progress: The Interplay of Self-Awareness and Open-mindedness 一、引言 I. Introduction 在当今急速发展的时代&#xff0c;个人成长与社会进步交织在一起&…

[每周一更]-(第86期):NLP-实战操作-文本分类

NLP文本分类的应用场景 医疗领域 - 病历自动摘要&#xff1a; 应用&#xff1a; 利用NLP技术从医疗文档中自动生成病历摘要&#xff0c;以帮助医生更快速地了解患者的状况。 法律领域 - 法律文件分类&#xff1a; 应用&#xff1a; 使用文本分类技术自动分类法律文件&#xf…

后端软件三层架构

一、三层架构简介 三层架构是软件开发中广泛采用的一种经典架构模式&#xff0c;其核心价值在于通过清晰的任务划分来提高代码的可维护性和重用性。具体来说&#xff0c;三层架构主要包括以下三个层次&#xff1a; 持久层&#xff08;DAO层&#xff09;&#xff1a;这一层主要…

提升 Web 请求效率:Axios request 封装技巧

在开发中&#xff0c;为了提高效率&#xff0c;通常对 Axios 进行封装&#xff0c;简化了请求的发送和对响应的处理。同时&#xff0c;统一错误处理机制有助于维护代码的清晰和一致性。本文介绍了一些高效封装 Axios 请求的方法。 封装理念 通过创建一个请求函数&#xff0c;我…

2024Node.js零基础教程(小白友好型),nodejs新手到高手,(四)NodeJS入门——网络基础概念

041_网络基础概念_IP的介绍 hello&#xff0c;大家好&#xff0c;我们来一起认识一下IP。 在开始介绍 IP 之前&#xff0c;我们首先来介绍一个场景&#xff0c;方便大家去理解 IP 这个概念。比如这会儿强哥正在成都&#xff0c;然后还有另外一个小伙伴&#xff0c;谁呢&#x…

互补滤波算法介绍+SCL源代码(收放卷线速度处理)

工程上对测量信号进行处理,我们可以利用低通滤波器,还可以利用滑动平均值滤波等,关于低通滤波器和滑动平均值滤波器,可以参考专栏相关文章,常用链接如下: 博途PLC一阶滞后低通滤波器(支持采样频率设置) https://rxxw-control.blog.csdn.net/article/details/132972093h…

带着问题读源码——Spring MVC是怎么找到接口实现类的?

引言 我们的产品主打金融服务领域&#xff0c;以B端客户为我们的核心合作伙伴&#xff0c;然而&#xff0c;我们的服务最终将惠及C端消费者。在技术实现上&#xff0c;我们采用了公司自主研发的微服务框架&#xff0c;该框架基于SpringBoot&#xff0c;旨在提供高效、可靠的服…

MyBatis笔记梳理

文章目录 什么是 MyBatis&#xff1f;前期准备依赖配置文件mapper利用注解 增、删、改、查查增改删#{} 和 ${} 的区别类型别名 动态sqlwhere ifforeachsql引用不常用标签 多表查询多对一&#xff08;一对一&#xff09;一对多多对多多表查询 个人理解 延迟加载概念使用场景延迟…

Qt/C++音视频开发66-音频变速不变调/重采样/提高音量/变速变调/倍速播放/sonic库使用

一、前言 之前在做倍速这个功能的时候&#xff0c;发现快速播放会有滴滴滴的破音出现&#xff0c;正常1倍速没有这个问题&#xff0c;尽管这个破音间隔很短&#xff0c;要放大音量才能听到&#xff0c;但是总归是不完美的&#xff0c;后面发现&#xff0c;通过修改qaudiooutpu…

2024年美赛数学建模E题思路分析 - 财产保险的可持续性

# 1 赛题 问题E&#xff1a;财产保险的可持续性 极端天气事件正成为财产所有者和保险公司面临的危机。“近年来&#xff0c;世界已经遭受了1000多起极端天气事件造成的超过1万亿美元的损失”。[1]2022年&#xff0c;保险业的自然灾害索赔人数“比30年的平均水平增加了115%”。…

[Java]JDK 安装后运行环境的配置

这篇文章用于介绍jdk.exe安装之后的运行环境配置&#xff0c;以及如何检查是否安装成功 检查自己是否安装jdk环境&#xff0c;记住这个安装的改的路径: (应该要安装2个&#xff0c;一个是jdk,一个是jre) 安装后的在文件夹的样子(路径自定义&#xff0c;在java下面): 参考如下…

【Springcloud篇】学习笔记二(四至六章):Eureka、Zookeeper、Consul

第四章_Eureka服务注册与发现 1.Eureka基础知识 1.1Eureka工作流程-服务注册 1.2Eureka两大组件 2.单机Eureka构建步骤 IDEA生成EurekaServer端服务注册中心&#xff0c;类似于物业公司 EurekaClient端cloud-provider-payment8081将注册进EurekaServer成为服务提供者provide…

Pytest框架测试

Pytest 是什么? pytest 能够支持简单的单元测试和复杂的功能测试;pytest 可以结合 Requests 实现接口测试; 结合 Selenium、Appium 实现自动化功能测试;使用 pytest 结合 Allure 集成到 Jenkins 中可以实现持续集成。pytest 支持 315 种以上的插件;为什么要选择 Pytest 丰…

VUE项目导出excel

导出excel主要可分为以下两种&#xff1a; 1. 后端主导实现 流程&#xff1a;前端调用到导出excel接口 -> 后端返回excel文件流 -> 浏览器会识别并自动下载 场景&#xff1a;大部分场景都有后端来做 2. 前端主导实现 流程&#xff1a;前端获取要导出的数据 -> 把常规数…

跨平台开发:浅析uni-app及其他主流APP开发方式

随着智能手机的普及&#xff0c;移动应用程序&#xff08;APP&#xff09;的需求不断增长。开发一款优秀的APP&#xff0c;不仅需要考虑功能和用户体验&#xff0c;还需要选择一种适合的开发方式。随着技术的发展&#xff0c;目前有多种主流的APP开发方式可供选择&#xff0c;其…

亚马逊新店铺视频怎么上传?视频验证失败怎么办?——站斧浏览器

亚马逊新店铺视频怎么上传&#xff1f; 登录亚马逊卖家中心&#xff1a;首先&#xff0c;卖家需要登录亚马逊卖家中心。在登录后&#xff0c;可以点击左侧导航栏上的“库存”选项&#xff0c;然后选择“新增或管理商品”。 选择商品&#xff1a;接下来&#xff0c;在“新增或…

【Vue】3-2、组合式 API

一、setup 选项 <script> export default {/*** 1、setup 执行时机早于 beforeCreate* 2、setup 中无法获取 this* 3、数据和函数需要在 setup 最后 return&#xff0c;才能在模板中使用* 4、可以通过 setup 语法糖简化代码*/setup(){// console.log(setup function, thi…

云服务器安全组、防火墙、端口问题,结合telnet解决项目部署无法访问

无论是运维还是后台亲自操刀在云服务器上部署项目&#xff0c;往往会遇到项目部署上去了&#xff0c;也确定项目正常运行&#xff0c;但还是没法访问的问题。 如果没有经验的小伙伴&#xff0c;很容易陷入疑惑的状态&#xff0c;无从下手解决。 其实这涉及到云平台安全组、服…