当前位置: 首页 > news >正文

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自定义相机拍照功能实现

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

相关文章:

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

flutter开发实战-Camera自定义相机拍照功能实现 一、前言 在项目中使用image_picker插件时候&#xff0c;在android设备上使用无法默认设置前置摄像头&#xff08;暂时不清楚什么原因&#xff09;&#xff0c;由于项目默认需要使用前置摄像头&#xff0c;所以最终采用自定义…...

LeetCode15. 三数之和

15. 三数之和 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三元组。 **注意&#xff1a;**答案中不可以包含重复…...

Docker搭建MySQL8主从复制

之前文章我们了解了面试官&#xff1a;说一说Binlog是怎么实现的&#xff0c;这里我们用Docker搭建主从复制环境。 docker安装主从MySQL 这里我们使用MySQL8.0.32版本&#xff1a; 主库配置 master.cnf //基础配置 [client] port3306 socket/var/run/mysqld/mysql.sock [m…...

【前端】日期转换

记录项目中需要处理的日期格式 默认vue2 初级版 将后端传来的数组 [2024/01/29 08:55:18, 2024/01/29 09:55:18, 2024/01/29 10:11:18]转为 [2024-01-29 08:55, 2024-01-29 09:55, 2024-01-29 10:11]方法 convertDateTimeFormat(arr) {var tempArr arr.map(function (dateT…...

Git 怎么设置用户的权限

在团队协作的软件开发中&#xff0c;对于版本控制系统Git来说&#xff0c;确保代码与数据的安全性至关重要。为了实现这一目标&#xff0c;Git提供了灵活且可定制的用户权限管理机制。下面将简单的探讨一下Git如何设置用户的权限&#xff0c;以及如何保护代码和数据。 用户身份…...

大端和小端模式介绍

介绍 “大端”和“小端”通常指的是字节序&#xff08;Byte Order&#xff09;的两种类型&#xff0c;也被称为端序&#xff08;Endianness&#xff09;。在多字节的数据类型&#xff08;如整数&#xff09;中&#xff0c;字节可以以不同的顺序存储&#xff0c;这影响了计算机…...

【vue】报错 Duplicate keys detected 解决方案

错误描述&#xff1a;Duplicate keys detected. This may cause an update error.错误直译&#xff1a;检测到重复的键。这可能会导致错误。错误原因&#xff1a;有相同父元素的多个子元素的v-for有相同的key值。 解决方法&#xff1a; return:{dataList:[{name:张三&#xf…...

机器学习_13_SVM支持向量机、感知器模型

文章目录 1 感知器模型1.1 感知器的思想1.2 感知器模型构建1.3 损失函数构建、求解 2 SVM3 线性可分SVM3.1 线性可分SVM—概念3.2 线性可分SVM —SVM 模型公式表示3.3 线性可分SVM —SVM 损失函数3.4 优化函数求解3.5 线性可分SVM—算法流程3.6 线性可分SVM—案例3.7 线性可分S…...

OpenCV学习记录——轮廓检测

文章目录 前言一、寻找、绘制轮廓二、具体应用代码 前言 寻找目标图像的轮廓并绘制出该轮廓是我们进行图像识别时常用的手段&#xff0c;轮廓是图像中连续的边界线&#xff0c;可以用于物体检测、形状分析等应用。为了获取更高的准确性&#xff0c;会先进行二值化处理&#xff…...

FreeRTOS任务挂起以及延时部分源码分析

layout: post title: “任务状态” date: 2023-7-19 15:39:08 0800 tags: FreeRTOS 任务状态 fireRTOS代码分析 任务挂起 //把一个任务挂起 void vTaskSuspend( TaskHandle_t xTaskToSuspend ) {TCB_t *pxTCB;taskENTER_CRITICAL();//进入临界区{/* 参数是NULL的时候设置为当…...

oracle数据库慢查询SQL

目录 场景&#xff1a; 环境&#xff1a; 慢SQL查询一&#xff1a; 问题一&#xff1a;办件列表查询慢 分析&#xff1a; 解决方法&#xff1a; 问题二&#xff1a;系统性卡顿 分析&#xff1a; 解决方法&#xff1a; 慢SQL查询二 扩展&#xff1a; 场景&#xff1a; 线…...

C语言搭配EasyX实现贪吃蛇小游戏

封面展示 内部展示 完整代码 #define _CRT_SECURE_NO_WARNINGS #include<easyx.h> #include<stdio.h> #include<mmsystem.h> #pragma comment (lib,"winmm.lib") #define width 40//宽有40个格子 #define height 30//长有40个格子 #define size 2…...

# 软件安装-Linux搭建nginx(单机版)

软件安装-Linux搭建nginx(单机版) 安装版本:nginx-1.24.0 文章目录 软件安装-Linux搭建nginx(单机版)一、Nginx包下载二、创建用户1.新建组和用户2.设置用户密码3.登录自己创建的目录三、安装依赖组件四、安装Nginx五、启动Nginx六、配置Nginx一、Nginx包下载 1. nginx-1.24下…...

成熟的汽车制造供应商协同平台 要具备哪些功能特性?

汽车行业是一个产业链长且“重”的行业&#xff0c;整个业务流程包括了研发、设计、采购、库存、生产、销售、售后等一系列环节&#xff0c;在每一个环节都涉及到很多信息交换的需求。对内要保证研发、采购、营销等业务环节信息流通高效安全&#xff0c;对外要与上、下游合作伙…...

React16源码: React中处理ref的核心流程源码实现

ref的实现过程 1 &#xff09;概述 在更新流程当中如何去设置ref上面的对象的过程在我们创建fiber的时候去处理ref这个属性那我们什么时候创建fiber对象? 就是我们去更新某一个节点&#xff0c;然后要去调和它的子节点的时候这个时候我们会对每一个子节点去创建这个fiber对象…...

ref和reactive

看尤雨溪说&#xff1a;为什么Vue3 中应该使用 Ref 而不是 Reactive&#xff1f;...

掌握数据预测的艺术:线性回归模型详解

线性回归是统计学中用于建模两个或多个变量之间线性关系的一种方法,广泛应用于数据分析、机器学习等领域。从数学建模的角度出发,线性回归旨在找到一个线性方程,最好地描述自变量(或称为解释变量、特征变量)和因变量(或称为目标变量)之间的关系。本文将通过Python代码示…...

STM32F407移植OpenHarmony笔记8

继上一篇笔记&#xff0c;成功开启了littlefs文件系统&#xff0c;能读写FLASH上的文件了。 今天继续研究网络功能&#xff0c;让控制台的ping命令能工作。 轻量级系统使用的是liteos_m内核lwip协议栈实现网络功能&#xff0c;需要进行配置开启lwip支持。 lwip的移植分为两部分…...

C++:输入流/输出流

C流类库简介 C为了克服C语言中的scanf和printf存在的缺点。&#xff0c;使用cin/cout控制输入/输出。 cin&#xff1a;表示标准输入的istream类对象&#xff0c;cin从终端读入数据。cout&#xff1a;表示标准输出的ostream类对象&#xff0c;cout向终端写数据。cerr&#xff…...

十、Qt三维图表

一、Data Visualization模块概述 Data Visualization的三维显示功能主要有三种三维图形来实现&#xff0c;三各类的父类都是QAbstract3DGraph&#xff0c;从QWindow继承而来。这三类分别是&#xff1a;三维柱状图Q3DBar三维空间散点Q3DScatter三维曲面Q3DSurface 1、相关类的…...

挑战杯推荐项目

“人工智能”创意赛 - 智能艺术创作助手&#xff1a;借助大模型技术&#xff0c;开发能根据用户输入的主题、风格等要求&#xff0c;生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用&#xff0c;帮助艺术家和创意爱好者激发创意、提高创作效率。 ​ - 个性化梦境…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

spring:实例工厂方法获取bean

spring处理使用静态工厂方法获取bean实例&#xff0c;也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下&#xff1a; 定义实例工厂类&#xff08;Java代码&#xff09;&#xff0c;定义实例工厂&#xff08;xml&#xff09;&#xff0c;定义调用实例工厂&#xff…...

Qt Http Server模块功能及架构

Qt Http Server 是 Qt 6.0 中引入的一个新模块&#xff0c;它提供了一个轻量级的 HTTP 服务器实现&#xff0c;主要用于构建基于 HTTP 的应用程序和服务。 功能介绍&#xff1a; 主要功能 HTTP服务器功能&#xff1a; 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

排序算法总结(C++)

目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指&#xff1a;同样大小的样本 **&#xff08;同样大小的数据&#xff09;**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...

关于uniapp展示PDF的解决方案

在 UniApp 的 H5 环境中使用 pdf-vue3 组件可以实现完整的 PDF 预览功能。以下是详细实现步骤和注意事项&#xff1a; 一、安装依赖 安装 pdf-vue3 和 PDF.js 核心库&#xff1a; npm install pdf-vue3 pdfjs-dist二、基本使用示例 <template><view class"con…...