Flutter 自定义水印拍照相机
文章目录
- 前言
- 一、相机预览
- 1. 引入库
- 2. 自定拍照状态
- 3. 预览布局
- 二、核心功能实现
- 1. 拍照
- 2. 确认时,区域截图
- 3. 打开水印拍照
- 4. 定位获取
- 三、调用示例
- 四、完整代码
前言
先说一下我这里的基本需求:
- 限制图片拍照比例 4:3
- 拍照时,需添加时间、地理位置的水印
然后看一下功能预览:
相机预览 | 生成水印图片 |
---|---|
一、相机预览
1. 引入库
使用camera库:
dependencies:camera: ^0.7.0+2
2. 自定拍照状态
enum TakeStatus {/// 准备中preparing,/// 拍摄中taking,/// 待确认confirm,/// 已完成done
}
3. 预览布局
Widget _buildCameraArea() {Widget area;if (_takeStatus == TakeStatus.confirm && _curFile != null) {// 待确认状态下,显示图片(按照宽度填充,高度超出部分隐藏)area = Image.file(File(_curFile.path), fit: BoxFit.fitWidth,);} else if (_cameraController != null && _cameraController.value.isInitialized) {// 相机预览final double screenWidth = MediaQuery.of(context).size.width;// 超出部分裁剪area = ClipRect(child: OverflowBox(alignment: Alignment.center,child: FittedBox(fit: BoxFit.fitWidth,child: Container(width: screenWidth,height: screenWidth * _cameraController.value.aspectRatio,child: CameraPreview(_cameraController),))),);} else {// 加载时,显示空白area = Container(color: Colors.black,);}return Center(// 指定需要截图的区域child: RepaintBoundary(key: _cameraKey,child: Stack(children: [AspectRatio(aspectRatio: widget.aspectRatio ?? 4 / 3,child: area,),Positioned(left: 10,right: 120,bottom: 10,child: Column(mainAxisSize: MainAxisSize.min,crossAxisAlignment: CrossAxisAlignment.start,children: [Text(_time ?? '', style: TextStyle(color: Colors.white, fontSize: 13),),Text(_address ?? '', style: TextStyle(color: Colors.white, fontSize: 13),),],)),],),),);}
注意:CameraPreview显示在指定区域会被拉伸变形。故,需要CameraPreview正常比例显示,然后在指定区域内超出隐藏。
二、核心功能实现
1. 拍照
/// 拍照void _takePicture() async {if (_cameraController == null || _cameraController.value.isTakingPicture) return;_timer?.cancel();XFile file = await _cameraController.takePicture();setState(() {_curFile = file;_takeStatus = TakeStatus.confirm;});}
注意:CameraController.takePicture() 拍出的照片,是按照默认相机比例的图片。在回显的时候,按照图片宽度铺满,高度隐藏,即可得到预览相机的图片。
2. 确认时,区域截图
/// 确认。返回图片数据void _confirm() async {if (_isCapturing) return;_isCapturing = true;try {// 获取指定区域RenderRepaintBoundary boundary = _cameraKey.currentContext.findRenderObject();// 转成图片ui.Image image = await boundary.toImage(pixelRatio: widget.pixelRatio ?? 2.0);ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);Uint8List imgBytes = byteData.buffer.asUint8List();// 保存图片文件在本地String basePath = await findSavePath(WatermarkPhoto.SAVE_DIR);File file = File('$basePath/${DateTime.now().millisecondsSinceEpoch}.jpg');file.writeAsBytesSync(imgBytes);// 页面返回图片文件Navigator.of(context).pop(file);} catch (e) {print(e);}_isCapturing = false;}
注意:通过RepaintBoundary包裹需要截图的指定区域,并指定key。通过key获取到该区域的信息,然后实现截图。
3. 打开水印拍照
Future<File> takeWatermarkPhoto(BuildContext context, {double aspectRatio,double pixelRatio,
}) async {return await Navigator.of(context).push(PageRouteBuilder(opaque:false,pageBuilder: (BuildContext context, Animation<double> animation,Animation<double> secondaryAnimation) {return WatermarkPhoto(aspectRatio: aspectRatio, pixelRatio: pixelRatio);},transitionsBuilder: (BuildContext context,Animation<double> animation,Animation<double> secondaryAnimation,Widget child,) => FadeTransition(opacity: animation,child: child,),));
}
设置默认的打开页面交互方式,只需关注水印拍照后返回的结果。
4. 定位获取
本例子中,定位功能是使用高德地图Android定位SDK实现的。你也可以直接使用别人封装好的Flutter定位插件。
三、调用示例
final File pickedFile = await takeWatermarkPhoto(context);
if (pickedFile != null) {// 可通过Image.file()来显示图片
}
四、完整代码
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;import 'package:camera/camera.dart';
import 'package:date_format/date_format.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:stmy_mobile/plugin/amap/amap_location.dart';
import 'package:stmy_mobile/plugin/amap/amap_location_option.dart';
import 'package:stmy_mobile/utils/permission_util.dart';class WatermarkPhoto extends StatefulWidget {static const String SAVE_DIR = 'tempImage';final double aspectRatio;final double pixelRatio;WatermarkPhoto({this.aspectRatio, this.pixelRatio});@override_WatermarkPhotoState createState() => _WatermarkPhotoState();
}class _WatermarkPhotoState extends State<WatermarkPhoto> with WidgetsBindingObserver {final GlobalKey _cameraKey = GlobalKey();CameraController _cameraController;String _time;String _address;TakeStatus _takeStatus = TakeStatus.preparing;XFile _curFile;Timer _timer;bool _isCapturing = false;@overridevoid initState() {super.initState();WidgetsBinding.instance.addObserver(this);AMapLocation.init(AMapLocationOption());_time = formatDate(DateTime.now(), [yyyy, '-', mm, '-' , dd, ' ', HH, ':', nn, ':', ss]);_address = '未知位置';_initCamera();}void _initCamera() async {try {_timer?.cancel();_timer = Timer.periodic(Duration(seconds: 1), (timer) {if (mounted) {setState(() {_time = formatDate(DateTime.now(), [yyyy, '-', mm, '-' , dd, ' ', HH, ':', nn, ':', ss]);});}});setState(() {_takeStatus = TakeStatus.preparing;});List cameras = await availableCameras();_cameraController = CameraController(cameras.first, ResolutionPreset.high,enableAudio: false,imageFormatGroup: ImageFormatGroup.jpeg,);_cameraController.addListener(() {if (mounted) setState(() {});});await _cameraController.initialize();if (mounted) {setState(() {_takeStatus = TakeStatus.taking;});}if (await checkLocationPermission()) {LocationInfo info = await AMapLocation.getLocation(true);if (info.isSuccess()) {String address = info.formattedAddress;if ((address == null || address.isEmpty) && (info.province != null)) {address = info.province + info.city + info.district;}setState(() {_address = address;});}}} on CameraException catch (e) {print(e);}}@overridevoid didChangeAppLifecycleState(AppLifecycleState state) {if (_cameraController == null || !_cameraController.value.isInitialized) {return;}if (state == AppLifecycleState.inactive) {_cameraController?.dispose();} else if (state == AppLifecycleState.resumed) {if (_cameraController != null) {_initCamera();}}}@overridevoid dispose() {WidgetsBinding.instance.removeObserver(this);_cameraController?.dispose();AMapLocation.destroy();_timer?.cancel();super.dispose();}@overrideWidget build(BuildContext context) {return Scaffold(backgroundColor: Colors.black,body: Stack(children: [_buildCameraArea(),_buildTopBar(),_buildAction(),],),);}Widget _buildCameraArea() {Widget area;if (_takeStatus == TakeStatus.confirm && _curFile != null) {area = Image.file(File(_curFile.path), fit: BoxFit.fitWidth,);} else if (_cameraController != null && _cameraController.value.isInitialized) {final double screenWidth = MediaQuery.of(context).size.width;area = ClipRect(child: OverflowBox(alignment: Alignment.center,child: FittedBox(fit: BoxFit.fitWidth,child: Container(width: screenWidth,height: screenWidth * _cameraController.value.aspectRatio,child: CameraPreview(_cameraController),))),);} else {area = Container(color: Colors.black,);}return Center(child: RepaintBoundary(key: _cameraKey,child: Stack(children: [AspectRatio(aspectRatio: widget.aspectRatio ?? 4 / 3,child: area,),Positioned(left: 10,right: 120,bottom: 10,child: Column(mainAxisSize: MainAxisSize.min,crossAxisAlignment: CrossAxisAlignment.start,children: [Text(_time ?? '', style: TextStyle(color: Colors.white, fontSize: 13),),Text(_address ?? '', style: TextStyle(color: Colors.white, fontSize: 13),),],)),],),),);}Widget _buildTopBar() {String flashIcon = 'assets/icon-flash-auto.png';if (_cameraController != null && _cameraController.value.isInitialized) {switch (_cameraController.value.flashMode) {case FlashMode.auto:flashIcon = 'assets/icon-flash-auto.png';break;case FlashMode.off:flashIcon = 'assets/icon-flash-off.png';break;case FlashMode.always:case FlashMode.torch:flashIcon = 'assets/icon-flash-on.png';break;}}if (_takeStatus == TakeStatus.confirm) {return Container();}return Positioned(top: MediaQuery.of(context).padding.top + 10,left: 10,right: 10,child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,children: [IconButton(color: Colors.white,icon: Icon(Icons.arrow_back, size: 32,),onPressed: () => Navigator.of(context).pop()),IconButton(color: Colors.white,icon: Image.asset(flashIcon, width: 32, height: 32,),onPressed: _toggleFlash)],));}Widget _buildAction() {Widget child;if (_takeStatus == TakeStatus.confirm) {child = Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,children: [OutlineButton(shape: CircleBorder(),color: Colors.black.withOpacity(0.5),padding: EdgeInsets.all(10),borderSide: BorderSide(color: Colors.grey),child: Image.asset('assets/icon-close.png', width: 24, height: 24,),onPressed: _cancel),OutlineButton(shape: CircleBorder(),color: Colors.black.withOpacity(0.5),padding: EdgeInsets.all(10),borderSide: BorderSide(color: Colors.grey),child: Image.asset('assets/icon-confirm.png', width: 24, height: 24,),onPressed: _confirm)],);} else {child = OutlineButton(shape: CircleBorder(),color: Colors.black.withOpacity(0.5),padding: EdgeInsets.all(8),borderSide: BorderSide(color: Colors.grey),child: Icon(Icons.camera, color: Colors.white, size: 48,),onPressed: _takePicture);}return Positioned(bottom: 50,left: 50,right: 50,child: child);}/// 切换闪光灯void _toggleFlash() {if (_cameraController == null) return;switch (_cameraController.value.flashMode) {case FlashMode.auto:_cameraController.setFlashMode(FlashMode.always);break;case FlashMode.off:_cameraController.setFlashMode(FlashMode.auto);break;case FlashMode.always:case FlashMode.torch:_cameraController.setFlashMode(FlashMode.off);break;}}/// 拍照void _takePicture() async {if (_cameraController == null || _cameraController.value.isTakingPicture) return;_timer?.cancel();XFile file = await _cameraController.takePicture();setState(() {_curFile = file;_takeStatus = TakeStatus.confirm;});}/// 取消。重新拍照void _cancel() {setState(() {_takeStatus = TakeStatus.preparing;});_cameraController?.dispose();_initCamera();}/// 确认。返回图片数据void _confirm() async {if (_isCapturing) return;_isCapturing = true;try {RenderRepaintBoundary boundary = _cameraKey.currentContext.findRenderObject();ui.Image image = await boundary.toImage(pixelRatio: widget.pixelRatio ?? 2.0);ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);Uint8List imgBytes = byteData.buffer.asUint8List();String basePath = await findSavePath(WatermarkPhoto.SAVE_DIR);File file = File('$basePath/${DateTime.now().millisecondsSinceEpoch}.jpg');file.writeAsBytesSync(imgBytes);Navigator.of(context).pop(file);} catch (e) {print(e);}_isCapturing = false;}
}enum TakeStatus {/// 准备中preparing,/// 拍摄中taking,/// 待确认confirm,/// 已完成done
}Future<File> takeWatermarkPhoto(BuildContext context, {double aspectRatio,double pixelRatio,
}) async {return await Navigator.of(context).push(PageRouteBuilder(opaque:false,pageBuilder: (BuildContext context, Animation<double> animation,Animation<double> secondaryAnimation) {return WatermarkPhoto(aspectRatio: aspectRatio, pixelRatio: pixelRatio);},transitionsBuilder: (BuildContext context,Animation<double> animation,Animation<double> secondaryAnimation,Widget child,) => FadeTransition(opacity: animation,child: child,),));
}
/// 获取文件存储路径
Future<String> findSavePath([ String basePath ]) async {final directory = Platform.isAndroid? await getExternalStorageDirectory(): await getApplicationDocumentsDirectory();if (basePath == null) {return directory.path;}String saveDir = path.join(directory.path, basePath);Directory root = Directory(saveDir);if (!root.existsSync()) {await root.create();}return saveDir;
}
Flutter 自定义水印拍照相机相关推荐
- 自定义水印相机(watercamera)
关于自定义水印相机(watercamera)的一点心得 本文为原创,转载请声明! 网上有很多自定义相机,但是跟我需要的项目真心不符,所以最后还是只能找了一个(git上面的例子:原本想链接地址的,结果没 ...
- android之利用surfaceView实现自定义水印相机
android之利用surfaceView实现自定义水印相机 知识点 1.自定义相机+预览相机 2.截屏拍照加水印 3.关于不使用intent来传输图片 4.关于大家说要demo的,因为这里是项目里头 ...
- android 美颜滤镜水印,水印滤镜相机
水印滤镜相机是一款拍照,录像,美艳,p图集一体的相机软件,有超多编辑模板供用户使用,帮助用户编辑最棒的照片,同时这是款免费安全的拍照软件,用户无需付费直接下载使用.一键式修改图片更为方便,软件界面简单 ...
- alertdialog 自定义样式回调选手_日志MIUI 10 9.8.7 内测更新资讯 小米8自定义水印...
「第60期」 - 不知不觉来到了第60期的「日志」 ... - 目前在试运行地把酷安@Yuming_Zh 「830」栏目资讯由周一到周五推送升级为每天推送.为打造一个读者互相交流的新栏目.期待各位的神 ...
- 自定义修改iPhone相机界面
很多情况下,我们需要自定义系统的相机拍照界面, 以下示例代码可以实现定制相机界面, 具体UI自己可以加入 - (void) setup: (UIView *) aView {//获取相机界面的view ...
- 【Flutter】Flutter 自定义字体 ( 下载 TTF 字体 | pubspec.yaml 配置字体资源 | 同步资源 | 全局应用字体 | 局部应用字体 )
文章目录 一.Flutter 自定义字体 1.ttf 字体文件 2.ttf 字体资源配置 3.获取字体 4.全局使用字体 5.局部使用字体 二.完整代码示例 三.相关资源 一.Flutter 自定义字 ...
- CSDN博客图片水印|自定义水印|去除水印
参考博文1:https://blog.csdn.net/stereohomology/article/details/54561782 参考博文2:https://blog.csdn.net/u011 ...
- java如何添加自定义的图片_java代码将图片加上自定义水印 -4
java代码将图片加上自定义水印,然后生成了新的图片 import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; ...
- Flutter 自定义组件实战之Cupertino(iOS)风格的复选框
继上一篇Flutter自定义组件的视频短课(视频地址: https://www.bilibili.com/video/BV1ap4y1U7UB/ )后,我们继续来聊自定义组件.视频中我为大家详解了Cu ...
最新文章
- 一起学习android图片四舍五入图片集资源 (28)
- nginx的启动初始化过程(一)
- Linux-Rsync命令参数详解
- hadoop之 Hadoop2.2.0中HDFS的高可用性实现原理
- Python 内置模块之 random
- 疑似Redmi K40新机获得3C认证:搭载联发科天玑1000+ 支持33W快充
- 217 - leetcode -存在重复元素 -数据结构类 先排序再操作
- 简单电话系统的电话数量分析
- 计算机广告制作专业,计算机广告制作专业介绍
- 单片机控制两个步进电机画圆_单片机控制的步进电机程序框图
- win7怎么设置开机密码_主编教您电脑开机密码怎么设置
- cdn/github_cdn加速配置
- rockchip eDP 配置
- 计算机英语教案模板,优秀全英文教案模板
- Akka默认20s超时修改配置
- Decision-making Strategy on Highway for Autonomous Vehicles using Deep Reinforcement Learning
- Linux_查看内存使用情况
- 一元线性回归与多元线性回归
- matlab中Add什么意思,add detail是什么意思
- HDMI 1.4 协议
热门文章
- 新“火眼金睛”:AI+遥感提升自然资源调查监管能力
- presto查询优化
- flannel网络的安装和删除
- 单片机反复进入休眠唤醒导致死机问题-辉芒微FMD 62F80X
- 分布式事务产生的原因
- 【深度学习】谷歌云GPU服务器创建与使用指南(二)
- Linux网络编程之实现服务器与客户端之间的通讯
- html 判断本地文件存在,javascript怎么判断文件是否存在?
- 【SSM - Spring篇01】spring详细概述,Spring体系结构,bean、property属性,Spring生命周期方法
- 关于Android 竖屏录制,在PC端播放被逆时针旋转了90度