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.hqwc.cn/news/450314.html

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

相关文章

stack_queue:三个关键注意事项解析

一、stack与容器 template<class T, class Container> class stack { private:Container _con; };Container 为容器&#xff0c;在实例化创建对象时&#xff0c;我们可以传 vector<T> 或 list<T> 等作为栈的底层。 举例&#xff1a; int main() {stack<i…

《苍穹外卖》知识梳理P1-多模块项目的创建

《苍穹外卖》知识梳理P1 一.多模块项目的创建 个人认为对于一个项目的学习&#xff0c;应当先从它的项目结构入手&#xff0c;明确了各个模块的职责与功能&#xff0c;后边尽心编写以及改写的时候会更加得心应手。 该项目分为了3个模块&#xff1a;sky-common模块&#xff0…

西瓜书学习笔记——核化线性降维(公式推导+举例应用)

文章目录 算法介绍实验分析 算法介绍 核化线性降维是一种使用核方法&#xff08;Kernel Methods&#xff09;来进行降维的技术。在传统的线性降维方法中&#xff0c;例如主成分分析&#xff08;PCA&#xff09;和线性判别分析&#xff08;LDA&#xff09;&#xff0c;数据被映…

【备战蓝桥杯】——循环结构终篇

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-yl4Tqejg4LkjZLAM {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

云尘 -- 铁三域控

描述&#xff1a; flag1 直接fscan开扫 发现存活两台机子123和141&#xff0c;其中141这台机子扫出来有ms17-010漏洞 继续信息收集&#xff0c;用nmap扫一波全端口&#xff0c;看看有没有遗漏 141这台机子一开始没扫到&#xff0c;看着提示使用-Pn再扫一遍就行了。因为如果当…

如何回答消息确认不丢失

1、如何知道有消息丢失&#xff1f; 2、哪些环节可能d 3、如何确保消息不丢失 二、消息丢失的环节 三&#xff0c;防止消息丢失的阶段操作 消息生产阶段 消息存储阶段 消息消费阶段 如何解决消息积压的问题 从消费端解决问题&#xff1a;

Leetcode—203. 移除链表元素【简单】

2024每日刷题&#xff08;一零九&#xff09; Leetcode—203. 移除链表元素 实现代码 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(n…

BUUCTF-Real-[struts2]s2-013

struts2的标签中 <s:a> 和 <s:url> 都有一个 includeParams 属性&#xff0c;可以设置成如下值none - URL中不包含任何参数&#xff08;默认&#xff09; get - 仅包含URL中的GET参数 all - 在URL中包含GET和POST参数 当includeParamsall的时候&#xff0c;会将本次…

力扣刷题之旅:启程篇(二)

力扣&#xff08;LeetCode&#xff09;是一个在线编程平台&#xff0c;主要用于帮助程序员提升算法和数据结构方面的能力。以下是一些力扣上的入门题目&#xff0c;以及它们的解题代码。 --点击进入刷题地址 1.最后得到的余数 题目描述&#xff1a; 给定两个非空字符串 nu…

六大免费的Redis内存分析工具

在我们需要分析Redis实例的内存使用情况时&#xff0c;市场上有着许多免费的开源工具&#xff0c;同时也有少量的付费产品。如果您想更深层次地分析内存相关问题的话&#xff0c;就可能需要用到一些更具针对性的“独门”工具了。 【51CTO.com快译】在我们需要分析Redis实例的内…

内网信息收集-Windows篇

目录 内网信息收集 机器角色分析 本机的信息收集 密码信息 如何查找内网的网段 进程、端口、补丁、共享文件夹 总结 域环境信息收集 MSF信息收集 内网信息收集 机器角色分析 1、判断当前主机是什么服务器&#xff1f; web服务器、开发测试服务器、公共服务器、文件服…

机器学习——集成学习

&#x1f4d5;参考&#xff1a;ysu老师课件西瓜书 期末复习笔记 1.集成学习的基本概念 集成学习&#xff08;ensemble learing&#xff09;通过构建并结合多个学习器来完成学习任务。 有时也被称为多分类器系统&#xff08;multi-classifier system&#xff09;、基于委员会的…