文章目录

  • 前言
  • 一、相机预览
    • 1. 引入库
    • 2. 自定拍照状态
    • 3. 预览布局
  • 二、核心功能实现
    • 1. 拍照
    • 2. 确认时,区域截图
    • 3. 打开水印拍照
    • 4. 定位获取
  • 三、调用示例
  • 四、完整代码

前言

先说一下我这里的基本需求:

  1. 限制图片拍照比例 4:3
  2. 拍照时,需添加时间、地理位置的水印

然后看一下功能预览:

相机预览 生成水印图片

一、相机预览

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 自定义水印拍照相机相关推荐

  1. 自定义水印相机(watercamera)

    关于自定义水印相机(watercamera)的一点心得 本文为原创,转载请声明! 网上有很多自定义相机,但是跟我需要的项目真心不符,所以最后还是只能找了一个(git上面的例子:原本想链接地址的,结果没 ...

  2. android之利用surfaceView实现自定义水印相机

    android之利用surfaceView实现自定义水印相机 知识点 1.自定义相机+预览相机 2.截屏拍照加水印 3.关于不使用intent来传输图片 4.关于大家说要demo的,因为这里是项目里头 ...

  3. android 美颜滤镜水印,水印滤镜相机

    水印滤镜相机是一款拍照,录像,美艳,p图集一体的相机软件,有超多编辑模板供用户使用,帮助用户编辑最棒的照片,同时这是款免费安全的拍照软件,用户无需付费直接下载使用.一键式修改图片更为方便,软件界面简单 ...

  4. alertdialog 自定义样式回调选手_日志MIUI 10 9.8.7 内测更新资讯 小米8自定义水印...

    「第60期」 - 不知不觉来到了第60期的「日志」 ... - 目前在试运行地把酷安@Yuming_Zh 「830」栏目资讯由周一到周五推送升级为每天推送.为打造一个读者互相交流的新栏目.期待各位的神 ...

  5. 自定义修改iPhone相机界面

    很多情况下,我们需要自定义系统的相机拍照界面, 以下示例代码可以实现定制相机界面, 具体UI自己可以加入 - (void) setup: (UIView *) aView {//获取相机界面的view ...

  6. 【Flutter】Flutter 自定义字体 ( 下载 TTF 字体 | pubspec.yaml 配置字体资源 | 同步资源 | 全局应用字体 | 局部应用字体 )

    文章目录 一.Flutter 自定义字体 1.ttf 字体文件 2.ttf 字体资源配置 3.获取字体 4.全局使用字体 5.局部使用字体 二.完整代码示例 三.相关资源 一.Flutter 自定义字 ...

  7. CSDN博客图片水印|自定义水印|去除水印

    参考博文1:https://blog.csdn.net/stereohomology/article/details/54561782 参考博文2:https://blog.csdn.net/u011 ...

  8. java如何添加自定义的图片_java代码将图片加上自定义水印 -4

    java代码将图片加上自定义水印,然后生成了新的图片 import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; ...

  9. Flutter 自定义组件实战之Cupertino(iOS)风格的复选框

    继上一篇Flutter自定义组件的视频短课(视频地址: https://www.bilibili.com/video/BV1ap4y1U7UB/ )后,我们继续来聊自定义组件.视频中我为大家详解了Cu ...

最新文章

  1. 一起学习android图片四舍五入图片集资源 (28)
  2. nginx的启动初始化过程(一)
  3. Linux-Rsync命令参数详解
  4. hadoop之 Hadoop2.2.0中HDFS的高可用性实现原理
  5. Python 内置模块之 random
  6. 疑似Redmi K40新机获得3C认证:搭载联发科天玑1000+ 支持33W快充
  7. 217 - leetcode -存在重复元素 -数据结构类 先排序再操作
  8. 简单电话系统的电话数量分析
  9. 计算机广告制作专业,计算机广告制作专业介绍
  10. 单片机控制两个步进电机画圆_单片机控制的步进电机程序框图
  11. win7怎么设置开机密码_主编教您电脑开机密码怎么设置
  12. cdn/github_cdn加速配置
  13. rockchip eDP 配置
  14. 计算机英语教案模板,优秀全英文教案模板
  15. Akka默认20s超时修改配置
  16. Decision-making Strategy on Highway for Autonomous Vehicles using Deep Reinforcement Learning
  17. Linux_查看内存使用情况
  18. 一元线性回归与多元线性回归
  19. matlab中Add什么意思,add detail是什么意思
  20. HDMI 1.4 协议

热门文章

  1. 新“火眼金睛”:AI+遥感提升自然资源调查监管能力
  2. presto查询优化
  3. flannel网络的安装和删除
  4. 单片机反复进入休眠唤醒导致死机问题-辉芒微FMD 62F80X
  5. 分布式事务产生的原因
  6. 【深度学习】谷歌云GPU服务器创建与使用指南(二)
  7. Linux网络编程之实现服务器与客户端之间的通讯
  8. html 判断本地文件存在,javascript怎么判断文件是否存在?
  9. 【SSM - Spring篇01】spring详细概述,Spring体系结构,bean、property属性,Spring生命周期方法
  10. 关于Android 竖屏录制,在PC端播放被逆时针旋转了90度