Flutter图片添加水印,矩形或文字标注,自定义涂鸦到底应该怎么做?

您所在的位置:网站首页 iphone7怎么在图片上涂鸦 Flutter图片添加水印,矩形或文字标注,自定义涂鸦到底应该怎么做?

Flutter图片添加水印,矩形或文字标注,自定义涂鸦到底应该怎么做?

2024-06-06 14:57| 来源: 网络整理| 查看: 265

前段时间参照今日相机有个需求,具体需求如下: 1.拍照或者相册选择图片在编辑时候可以添加一个自定义的水印(包含时间和定位信息); 2.能在图片上面绘制矩形或者椭圆; 3.能在图片上面编辑文字标注,文字标注区域可以拖动; 4.可以自定义涂鸦; Flutter也有很多库,但全网好像并没有此类的库,那就自己动手实现。因为公司项目,我并没有整理Demo出来。接下来我主要把自己的一些思路整理出来,也会放一些片段式的代码。

可能会遇到的问题: 拍照图片和相册图片编辑区域适配问题? *绘制区域到底由什么决定? *整个过程会经历网络图片到本地,再从本地编辑之后上传,上传后失真问题 *图片修改上传后与自己标注不成比例问题 *文字标注拖动及边界界定问题 *多个图层同时进行操作可能会遇到的问题 拍照入口

需要引入的包

image_picker: ^0.8.0+1 #拍照

拍照入口代码

final picker = ImagePicker(); var image = await picker.getImage(source: ImageSource.camera); if (image != null) { final bytes = await image.readAsBytes(); UI.decodeImageFromList(bytes, (image) { NavigatorUtil.push( mContext, ImageEditPage( uint8list: bytes, width: image.width, height: image.height, projectName: widget.pageModelContents.project.name, typeEventBus: typeEventBus, picInfomationModel: PicInfomationModel(1, pageModelContents: pageModelContents), )); }); }

简单描述下上面这段代码逻辑,调用手机相机拍照,获取到图片转Uint8List,并根据Uint8List获取图片的真实宽高,然后跳转了ImageEditPage,ImageEditPage中uint8list,width,height,是三个重要参数,后面需要用到,至于其他参数也是需求中逻辑需要。

相册图片上传后编辑入口 Image image = Image.network(picModel.url); image.image .resolve(new ImageConfiguration()) .addListener(new ImageStreamListener( (ImageInfo info, bool _) async { Uint8List uint8List = await NetWorkImageUtil.netWorkUint8ListImage( picModel.url); if (uint8List != null) { NavigatorUtil.push( mContext, ImageEditPage( uint8list: uint8List, width: info.image.width, height: info.image.height, projectName: widget.pageModelContents.project.name, typeEventBus: typeEventBus, picInfomationModel: PicInfomationModel(3, pageModelContents: pageModelContents, picModel: picModel), )); } }, ));

因为相册选择照片是多张的,比不太适合去添加水印,所以是添加完成之后可以编辑的,接下来我们来看ImageEditPage代码;

ImageEditPage import 'dart:typed_data'; import 'dart:ui' as UI; import 'package:event_bus/event_bus.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:yirui_flutter_app/abstracs/abstract_class.dart'; import 'package:yirui_flutter_app/dialog/handwrite/canvasremark_dialog.dart'; import 'package:yirui_flutter_app/dialog/handwrite/construction_dialog.dart'; import 'package:yirui_flutter_app/dialog/handwrite/handtext_dialog.dart'; import 'package:yirui_flutter_app/dialog/handwrite/handtuya_dialog.dart'; import 'package:yirui_flutter_app/dialog/handwrite/watermark_dialog.dart'; import 'package:yirui_flutter_app/event/handwrite_event.dart'; import 'package:yirui_flutter_app/event/picwaterupload_event.dart'; import 'package:yirui_flutter_app/model/picinfo_model.dart'; import 'package:yirui_flutter_app/util/adapt_util.dart'; import 'package:yirui_flutter_app/util/assetsload_util.dart'; import 'package:yirui_flutter_app/util/color_util.dart'; import 'package:yirui_flutter_app/util/handline_util.dart'; import 'package:yirui_flutter_app/util/handlinelast_util.dart'; import 'package:yirui_flutter_app/util/handractLast_util.dart'; import 'package:yirui_flutter_app/util/handract_util.dart'; import 'package:yirui_flutter_app/util/location_util.dart'; import 'package:yirui_flutter_app/util/screen_utils.dart'; import 'package:yirui_flutter_app/view/draggable_edit.dart'; import 'package:yirui_flutter_app/view/draggablelast_edit.dart'; class ImageEditPage extends StatefulWidget { final Uint8List uint8list; final int height; final int width; final String projectName; final PicInfomationModel picInfomationModel; final EventBus typeEventBus; ImageEditPage({ this.uint8list, this.height, this.width, this.projectName, this.picInfomationModel, this.typeEventBus, }); @override _ImageEditPageState createState() => _ImageEditPageState(); } class _ImageEditPageState extends State with OnLocationListener { ///可绘制区域背景真实高度 double canvasbg_height = 0; ///可绘制区域背景真实宽度 double canvasbg_width = 0; ///图片真实绘制高度 double pics_height = 0; ///图片真实绘制宽度 double pics_width = 0; ///地理位置 String address = null; ///默认选中水印 bool _selectDeflutWater = true; ///默认无文字标注 bool _selectDefaulttext = false; ///默认无图形标注 bool _defaultHandRect = false; ///默认无自定义涂鸦 bool selectDefaultTuYa = false; EventBus eventBus; EventBus eventBusBiaoZhu; EventBus eventBusLine; ///默认文字标注文案 String hittext = "暂无"; ///默认矩形椭圆无 默认不进行任何图形绘制 int selectReactType = 3; ///水印施工区域 String construction = "点我修改"; HandReactBoardController cosntrollerReact = HandReactBoardController(); HandLineBoardController cosntrollerLine = HandLineBoardController(); HandReactLastBoardController cosntrollerLastReact = HandReactLastBoardController(); HandLineBoardLastController cosntrollerLastLine = HandLineBoardLastController(); ScrollController thirdColumnController = ScrollController(); ScrollController secondedRowController = ScrollController(); GlobalKey _handglobalKey = new GlobalKey(); Offset draggLastoffset = Offset(0, 10); @override void initState() { // TODO: implement initState super.initState(); LocationUtil().onLocation(this, ""); if (eventBusBiaoZhu == null) { eventBusBiaoZhu = new EventBus(); } if (eventBusLine == null) { eventBusLine = new EventBus(); } if (eventBus == null) { eventBus = new EventBus(); eventBus.on().listen((event) { if (event.obj["type"] == 1) { if (mounted) { setState(() { _selectDeflutWater = event.obj["show"]; }); } } else if (event.obj["type"] == 2) { if (mounted) { setState(() { _selectDefaulttext = event.obj["show"]; }); } } else if (event.obj["type"] == 3) { if (mounted) { setState(() { hittext = event.obj["hittext"]; }); } } else if (event.obj["type"] == 4) { if (mounted) { setState(() { selectReactType = event.obj["tag"]; if (selectReactType == 1 || selectReactType == 2) { _defaultHandRect = true; } else { _defaultHandRect = false; } ///选择绘制矩形或者椭圆 则涂鸦层要影藏 if (_defaultHandRect) { selectDefaultTuYa = false; cosntrollerLine.clearBoard(); cosntrollerLastLine.clearBoard(); } }); cosntrollerReact.clearBoard(); cosntrollerLastReact.clearBoard(); } } else if (event.obj["type"] == 5) { if (mounted) { setState(() { selectDefaultTuYa = event.obj["show"]; ///选择涂鸦 则绘制椭圆和矩形要影藏 if (selectDefaultTuYa) { _defaultHandRect = false; selectReactType = 3; cosntrollerReact.clearBoard(); cosntrollerLastReact.clearBoard(); } }); cosntrollerLine.clearBoard(); cosntrollerLastLine.clearBoard(); } } else if (event.obj["type"] == 6) { setState(() { this.draggLastoffset = Offset(event.obj["x"], event.obj["y"]); }); } else if (event.obj["type"] == 7) { setState(() { construction = event.obj["hittext"]; }); } }); } } @override void dispose() { super.dispose(); if (eventBus != null) { eventBus.destroy(); eventBus = null; } if (eventBusBiaoZhu != null) { eventBusBiaoZhu.destroy(); eventBusBiaoZhu = null; } if (eventBusLine != null) { eventBusLine.destroy(); eventBusLine = null; } cosntrollerReact.dispose(); cosntrollerLine.dispose(); cosntrollerLastReact.dispose(); cosntrollerLastLine.dispose(); thirdColumnController.dispose(); secondedRowController.dispose(); } @override Widget build(BuildContext context) { ///计算图片距离顶部高度 double margin_tu_height = 0; ///计算图片距离左侧距离 double margin_tu_width = 0; ScreenUtils screenUtils = ScreenUtils.getInstance(); canvasbg_height = screenUtils.screenHeight - screenUtils.statusBarHeight - Adapt.px(140) * 2; canvasbg_width = screenUtils.screenWidth; double cs_height = (screenUtils.screenWidth * widget.height) / widget.width; if (cs_height >= canvasbg_height) { ///图片宽度大于等于背景高度,去缩放图片宽度 pics_height = canvasbg_height; pics_width = (pics_height * widget.width) / widget.height; margin_tu_height = screenUtils.statusBarHeight + Adapt.px(140); margin_tu_width = (screenUtils.screenWidth - pics_width) / 2; } else { ///图片宽度缩放到屏幕宽度,高度根据屏幕宽度等比缩放 pics_height = (screenUtils.screenWidth * widget.height) / widget.width; pics_width = screenUtils.screenWidth; margin_tu_height = (canvasbg_height - pics_height) / 2 + screenUtils.statusBarHeight + Adapt.px(140); margin_tu_width = 0; } ///水印宽高系数 double xi_w = ((Adapt.px(388) * widget.width) / pics_width) / Adapt.px(388); double xi_h = ((Adapt.px(254) * widget.height) / pics_height) / Adapt.px(254); return Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.black, body: Stack( children: [ ListView( controller: thirdColumnController, children: [ SingleChildScrollView( controller: secondedRowController, scrollDirection: Axis.horizontal, //horizontal child: Stack( children: [ RepaintBoundary( key: _handglobalKey, child: Container( height: double.parse(widget.height.toString()), width: double.parse(widget.width.toString()), child: Stack( children: [ Container( height: double.parse(widget.height.toString()), width: double.parse(widget.width.toString()), child: Image.memory(widget.uint8list, fit: BoxFit.cover, filterQuality: FilterQuality.high), ), Offstage( offstage: !_selectDeflutWater, child: Container( height: double.parse( widget.height.toString()), width: double.parse(widget.width.toString()), alignment: Alignment.bottomLeft, child: Container( width: Adapt.px(388) * xi_w, height: Adapt.px(254) * xi_h, margin: EdgeInsets.only( left: Adapt.px(10) * xi_w, bottom: Adapt.px(10) * xi_h), child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular( Adapt.px(15) * xi_w), child: Container( child: Column( children: [ Opacity( opacity: 0.8, child: Container( height: Adapt.px(54) * xi_h, color: ColorUtil.colorblue, ), ), Opacity( opacity: 0.7, child: Container( height: Adapt.px(200) * xi_h, color: ColorUtil .color2c2c2c, ), ) ], ), ), ), Container( width: double.infinity, child: Column( children: [ Container( width: Adapt.px(388) * xi_w, margin: EdgeInsets.only( top: Adapt.px(8) * xi_h), child: Row( children: [ Container( margin: EdgeInsets.only( left: Adapt.px(10) * xi_w, right: Adapt.px(10) * xi_w), child: Image.asset( "images/icon_yuan.png", width: Adapt.px(20) * xi_w, height: Adapt.px(20) * xi_h, excludeFromSemantics: true, gaplessPlayback: true, ), ), Flexible( child: Text( widget.projectName == null ? "暂无" : widget .projectName .toString(), style: TextStyle( fontSize: Adapt.px( 26) * xi_h, color: Colors .white, fontWeight: FontWeight .w600, decoration: TextDecoration .none), maxLines: 1, overflow: TextOverflow .ellipsis), ), ], ), ), Container( width: Adapt.px(388) * xi_w, margin: EdgeInsets.only( top: Adapt.px(10) * xi_h), child: Row( children: [ Container( margin: EdgeInsets.only( left: Adapt.px(10) * xi_w, right: Adapt.px(10) * xi_w), child: Text("施工区域:", style: TextStyle( fontSize: Adapt.px( 26) * xi_h, color: Colors .white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), Flexible( child: Text( construction, style: TextStyle( fontSize: Adapt.px( 26) * xi_h, color: Colors .white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), ], ), ), Container( width: Adapt.px(388) * xi_w, child: Row( children: [ Container( margin: EdgeInsets.only( left: Adapt.px(10) * xi_w, right: Adapt.px(10) * xi_w), child: Text("拍摄时间:", style: TextStyle( fontSize: Adapt.px( 26) * xi_h, color: Colors .white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), Flexible( child: Text( new DateTime.now() .toString() .substring( 0, 16), style: TextStyle( fontSize: Adapt.px( 26) * xi_h, color: Colors .white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), ], ), ), Container( width: Adapt.px(388) * xi_w, child: Row( crossAxisAlignment: CrossAxisAlignment .start, children: [ Container( alignment: Alignment.topLeft, margin: EdgeInsets.only( left: Adapt.px(10) * xi_w, right: Adapt.px(10) * xi_w), child: Text("拍摄位置:", style: TextStyle( fontSize: Adapt.px( 26) * xi_h, color: Colors .white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), Flexible( child: Text( address == null ? "定位中..." : address, style: TextStyle( fontSize: Adapt.px( 26) * xi_h, color: Colors .white, ), maxLines: 3, overflow: TextOverflow .ellipsis), ), ], ), ), ], ), ), ], ), ), )), Offstage( offstage: !_selectDefaulttext, child: Container( height: double.parse( widget.height.toString()), width: double.parse( widget.width.toString()), child: Stack( children: [ DraggableLastWiget( widgetColor: Colors.transparent, margin_tu_width: margin_tu_width, margin_tu_height: margin_tu_height, rc_width: Adapt.px(400), rc_height: Adapt.px(200), hittext: hittext == null ? "暂无" : hittext, xi_w: xi_w, xi_h: xi_h, draggLastoffset: draggLastoffset, ) ], ))), Offstage( offstage: !_defaultHandRect, child: Container( width: pics_width, height: pics_height, child: HandReactLastBoard( boardController: cosntrollerLastReact, paintWidth: 5, painColor: Colors.red, width: pics_width, height: pics_height, type: selectReactType, eventBusBiaoZhu: eventBusBiaoZhu, xi_w: xi_w, xi_h: xi_h, ), )), Offstage( offstage: !selectDefaultTuYa, child: Container( width: pics_width, height: pics_height, child: HandLineLastBoard( boardController: cosntrollerLastLine, paintWidth: 5, painColor: Colors.red, width: pics_width, height: pics_height, eventBusLine: eventBusLine, xi_w: xi_w, xi_h: xi_h, ), )) ], ), ), ), ///用于遮挡真实底层View Container( color: Colors.black, height: double.parse(widget.height.toString()), width: double.parse(widget.width.toString()), ) ], )), ], ), Container( child: Column( children: [ Container( height: Adapt.px(140), margin: EdgeInsets.only( top: ScreenUtils.getInstance().statusBarHeight, ), padding: EdgeInsets.only( left: Adapt.px(20), right: Adapt.px(20)), color: ColorUtil.color141414, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( onTap: () { Navigator.pop(context); }, child: Text('取消', style: TextStyle( fontSize: Adapt.px(36), color: Colors.white, )), ), Container( width: Adapt.px(130), height: Adapt.px(70), child: RaisedButton( color: Colors.blue, shape: RoundedRectangleBorder( borderRadius: new BorderRadius.circular(18.0), ), child: Text( '保存', style: TextStyle( fontSize: Adapt.px(30), color: Colors.white), ), onPressed: () { _savePicUrl(); }, ), ), ], ), ), Flexible( child: Container( alignment: Alignment.center, child: Stack( children: [ Container( width: pics_width, height: pics_height, child: Image.memory(widget.uint8list, fit: BoxFit.fitWidth, filterQuality: FilterQuality.high), ), Offstage( offstage: !_selectDeflutWater, child: GestureDetector( onTap: () { showDialog( context: context, //BuildContext对象 builder: (BuildContext context) { return GestureDetector( onTap: () {}, child: ConstructionDialog( eventBus: eventBus, ), ); }); }, child: Container( width: pics_width, height: pics_height, alignment: Alignment.bottomLeft, child: Container( width: Adapt.px(388), height: Adapt.px(254), margin: EdgeInsets.only( left: Adapt.px(10), bottom: Adapt.px(10)), child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(Adapt.px(15)), child: Container( child: Column( children: [ Opacity( opacity: 0.8, child: Container( height: Adapt.px(54), color: ColorUtil.colorblue, ), ), Opacity( opacity: 0.7, child: Container( height: Adapt.px(200), color: ColorUtil.color2c2c2c, ), ) ], ), ), ), Container( width: double.infinity, child: Column( children: [ Container( width: Adapt.px(388), margin: EdgeInsets.only( top: Adapt.px(8)), child: Row( children: [ Container( margin: EdgeInsets.only( left: Adapt.px(10), right: Adapt.px(10)), child: Image.asset( "images/icon_yuan.png", width: Adapt.px(20), height: Adapt.px(20), excludeFromSemantics: true, gaplessPlayback: true, ), ), Flexible( child: Text( widget.projectName == null ? "暂无" : widget.projectName .toString(), style: TextStyle( fontSize: Adapt.px(26), color: Colors.white, fontWeight: FontWeight.w600, decoration: TextDecoration .none), maxLines: 1, overflow: TextOverflow .ellipsis), ), ], ), ), Container( width: Adapt.px(388), margin: EdgeInsets.only( top: Adapt.px(10)), child: Row( children: [ Container( margin: EdgeInsets.only( left: Adapt.px(10), right: Adapt.px(10)), child: Text("施工区域:", style: TextStyle( fontSize: Adapt.px(26), color: Colors.white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), Flexible( child: Text(construction, style: TextStyle( fontSize: Adapt.px(26), color: Colors.white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), ], ), ), Container( width: Adapt.px(388), child: Row( children: [ Container( margin: EdgeInsets.only( left: Adapt.px(10), right: Adapt.px(10)), child: Text("拍摄时间:", style: TextStyle( fontSize: Adapt.px(26), color: Colors.white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), Flexible( child: Text( new DateTime.now() .toString() .substring(0, 16), style: TextStyle( fontSize: Adapt.px(26), color: Colors.white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), ], ), ), Container( width: Adapt.px(388), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( alignment: Alignment.topLeft, margin: EdgeInsets.only( left: Adapt.px(10), right: Adapt.px(10)), child: Text("拍摄位置:", style: TextStyle( fontSize: Adapt.px(26), color: Colors.white, ), maxLines: 1, overflow: TextOverflow .ellipsis), ), Flexible( child: Text( address == null ? "定位中..." : address, style: TextStyle( fontSize: Adapt.px(26), color: Colors.white, ), maxLines: 3, overflow: TextOverflow .ellipsis), ), ], ), ), ], ), ), ], ), ), ), )), Offstage( offstage: !_selectDefaulttext, child: Container( width: pics_width, height: pics_height, child: Stack( children: [ DraggableWiget( widgetColor: Colors.transparent, margin_tu_width: margin_tu_width, margin_tu_height: margin_tu_height, picHeight: pics_height, picWidth: pics_width, rc_width: Adapt.px(400), rc_height: Adapt.px(200), hittext: hittext == null ? "暂无" : hittext, eventBus: eventBus, ) ], ))), Offstage( offstage: !_defaultHandRect, child: Container( width: pics_width, height: pics_height, child: HandReactBoard( boardController: cosntrollerReact, paintWidth: 5, painColor: Colors.red, width: pics_width, height: pics_height, type: selectReactType, eventBusBiaoZhu: eventBusBiaoZhu), ), ), Offstage( offstage: !selectDefaultTuYa, child: Container( width: pics_width, height: pics_height, child: HandLineBoard( boardController: cosntrollerLine, paintWidth: 5, painColor: Colors.red, width: pics_width, height: pics_height, eventBusLine: eventBusLine), )) ], ), )), Container( height: Adapt.px(140), color: ColorUtil.color141414, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ GestureDetector( onTap: () { showModalBottomSheet( context: context, isDismissible: true, isScrollControlled: true, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), builder: (context) { return WatermarkPopupWindow( selectDeflutWater: _selectDeflutWater, eventBus: eventBus); }); }, child: Container( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( "images/icon_shuiyin.png", width: Adapt.px(60), height: Adapt.px(60), excludeFromSemantics: true, gaplessPlayback: true, ), Text('水印', style: TextStyle( fontSize: Adapt.px(32), color: Colors.white)) ], ), width: screenUtils.screenWidth / 4, ), ), GestureDetector( onTap: () { showModalBottomSheet( context: context, isDismissible: true, isScrollControlled: true, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), builder: (context) { return CanvasMarkPopupWindow( selectReactType: selectReactType, eventBus: eventBus); }); }, child: Container( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( "images/icon_biaozhu.png", width: Adapt.px(60), height: Adapt.px(60), excludeFromSemantics: true, gaplessPlayback: true, ), Text('标注', style: TextStyle( fontSize: Adapt.px(32), color: Colors.white)) ], ), width: screenUtils.screenWidth / 4, ), ), GestureDetector( onTap: () { showModalBottomSheet( context: context, isDismissible: true, isScrollControlled: true, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), builder: (context) { return HandTextPopupWindow( selectDefaulttext: _selectDefaulttext, eventBus: eventBus); }); }, child: Container( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( "images/icon_wenzi.png", width: Adapt.px(60), height: Adapt.px(60), excludeFromSemantics: true, gaplessPlayback: true, ), Text('文字', style: TextStyle( fontSize: Adapt.px(32), color: Colors.white)) ], ), width: screenUtils.screenWidth / 4, ), ), GestureDetector( onTap: () { showModalBottomSheet( context: context, isDismissible: true, isScrollControlled: true, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), builder: (context) { return HandTuYaPopupWindow( selectDefaultTuYa: selectDefaultTuYa, eventBus: eventBus); }); }, child: Container( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( "images/icon_tuya.png", width: Adapt.px(60), height: Adapt.px(60), excludeFromSemantics: true, gaplessPlayback: true, ), Text('涂鸦', style: TextStyle( fontSize: Adapt.px(32), color: Colors.white)) ], ), width: screenUtils.screenWidth / 4, ), ), ], ), ), ], ), ), ], )); } @override void onLocation(Map result) { if (result != null) { setState(() { address = result["address"]; }); } } _savePicUrl() async { EasyLoading.show(status: "上传中,请稍等..."); RenderRepaintBoundary repaintBoundary = _handglobalKey.currentContext.findRenderObject(); UI.Image image = await repaintBoundary.toImage(pixelRatio: 1.0); ByteData byteData = await image.toByteData(format: UI.ImageByteFormat.png); await AssetsLoadUtil.constant.imageByteFileUpload( byteData.buffer.asUint8List(), (List listAdress) { EasyLoading.dismiss(); if (listAdress != null) { if (widget.typeEventBus != null) { widget.typeEventBus .fire(PicWaterUploadEvent(listAdress, widget.picInfomationModel)); Navigator.pop(context); } } }); } } DraggableLastWiget import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:yirui_flutter_app/util/adapt_util.dart'; import 'package:yirui_flutter_app/util/color_util.dart'; import 'package:yirui_flutter_app/util/screen_utils.dart'; import 'package:yirui_flutter_app/dialog/handwrite/textinput_dialog.dart'; class DraggableLastWiget extends StatefulWidget { final Color widgetColor; ///计算图片距离顶部高度 final double margin_tu_height; ///计算图片距离左侧距离 final double margin_tu_width; ///矩形区域宽度 final double rc_width; ///矩形区域高度 final double rc_height; String hittext; final double xi_w; final double xi_h; final Offset draggLastoffset; DraggableLastWiget({ Key key, this.widgetColor, this.margin_tu_height, this.margin_tu_width, this.rc_width, this.rc_height, this.hittext, this.xi_w, this.xi_h, this.draggLastoffset, }) : super(key: key); @override _DraggableLastWigetState createState() => _DraggableLastWigetState(); } class _DraggableLastWigetState extends State { ScreenUtils screenUtils = null; @override void initState() { // TODO: implement initState super.initState(); screenUtils = ScreenUtils.getInstance(); } @override Widget build(BuildContext context) { return Positioned( left: widget.draggLastoffset.dx*widget.xi_w, top: widget.draggLastoffset.dy*widget.xi_h, child: Draggable( data: widget.widgetColor, child: Container( width: widget.rc_width*widget.xi_w, height: widget.rc_height*widget.xi_h, color: widget.widgetColor, child: Opacity( opacity: 0.7, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: EdgeInsets.only(top: Adapt.px(23)*widget.xi_h), child: Image.asset( "images/icon_yuan.png", width: Adapt.px(20)*widget.xi_w, height: Adapt.px(20)*widget.xi_h, excludeFromSemantics: true, gaplessPlayback: true, ), ), Flexible( child: Stack( children: [ Container( margin: EdgeInsets.only(top: Adapt.px(13)*widget.xi_h), child: Image.asset( "images/arrow_icon.png", width: Adapt.px(40)*widget.xi_w, height: Adapt.px(40)*widget.xi_h, excludeFromSemantics: true, gaplessPlayback: true, ), ), Container( child: Stack( children: [ Container( margin: EdgeInsets.only(left: Adapt.px(30)*widget.xi_w), decoration: new BoxDecoration( color: ColorUtil.color2c2c2c, borderRadius: BorderRadius.all( Radius.circular(Adapt.px(25)*widget.xi_w)), border: new Border.all( width: Adapt.px(5)*widget.xi_w, color: ColorUtil.color2c2c2c), ), ), Container( margin: EdgeInsets.only(left: Adapt.px(35)*widget.xi_w), child: Text( widget.hittext, style: TextStyle( fontSize: Adapt.px(28)*widget.xi_w, color: Colors.white), maxLines: 5, overflow: TextOverflow.ellipsis, ), ), ], )) ], )) ], ), ), ), feedback: Container(), )); } } HandReactLastBoard import 'dart:typed_data'; import 'dart:ui'; import 'dart:ui' as UI; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:yirui_flutter_app/event/base_event.dart'; import 'package:yirui_flutter_app/event/handbiaozhu_event.dart'; class HandReactLastBoard extends StatefulWidget { ///手写笔颜色 final Color painColor; ///手写笔宽度 final double paintWidth; ///手写笔控制器 final HandReactLastBoardController boardController; final double width; final double height; final int type; //绘制矩形还是绘制椭圆 final EventBus eventBusBiaoZhu; final double xi_w; final double xi_h; HandReactLastBoard({ Key key, this.painColor, this.paintWidth, @required this.boardController, this.width, this.height, this.type, this.eventBusBiaoZhu, this.xi_w, this.xi_h, }) : super(key: key); @override _HandReactLastBoardState createState() => _HandReactLastBoardState(); } class _HandReactLastBoardState extends State { List _strokes = []; List _theEllipse = []; bool isClear = false; @override void initState() { super.initState(); widget.boardController.bindContext(context); widget.eventBusBiaoZhu.on().listen((event) { if (event.obj["type"] == 2) { if (mounted) { DragUpdateDetails details = event.obj["obj"]; if (widget.type == 1) { setState(() { _strokes.last.updateStartX = details.localPosition.dx; _strokes.last.updateStartY = details.localPosition.dy; }); widget.boardController.refRectStrokes(_strokes); } else if (widget.type == 2) { setState(() { _theEllipse.last.updateStartX = details.localPosition.dx; _theEllipse.last.updateStartY = details.localPosition.dy; }); widget.boardController.refEllipseStrokes(_theEllipse); } } } else if (event.obj["type"] == 1) { double startX = event.obj["startX"]; double startY = event.obj["startY"]; if (widget.type == 1) { final newStroke = Rectangular( color: widget.painColor, width: widget.paintWidth, startX: startX, startY: startY, isClear: isClear, ); _strokes.add(newStroke); widget.boardController.refRectStrokes(_strokes); } else if (widget.type == 2) { final newStroke = TheEllipse( color: widget.painColor, width: widget.paintWidth, startX: startX, startY: startY, isClear: isClear, ); _theEllipse.add(newStroke); widget.boardController.refEllipseStrokes(_theEllipse); } } }); } @override Widget build(BuildContext context) { return CustomPaint( painter: BoardPainter( strokes: _strokes, theellipse: _theEllipse, type: widget.type, width: widget.width, height: widget.height, xi_w: widget.xi_w, xi_h: widget.xi_h), size: Size.infinite, ); } } class HandReactLastBoardController extends ChangeNotifier { BuildContext _context; List strokes = []; List ellipses = []; void bindContext(BuildContext context) { _context = context; } void refRectStrokes(List newValue) { if (strokes != newValue) { strokes = newValue; } notifyListeners(); } void refEllipseStrokes(List newtheellipse) { if (ellipses != newtheellipse) { ellipses = newtheellipse; } notifyListeners(); } void clearBoard() { strokes.clear(); ellipses.clear(); notifyListeners(); } } ///矩形方框 class Rectangular { final Color color; final double startX; final double startY; double updateStartX; double updateStartY; final bool isClear; final double width; Rectangular({ this.color = Colors.black, this.width = 4, this.isClear = false, this.startX = 0, this.startY = 0, this.updateStartX = 0, this.updateStartY = 0, }); } ///椭圆 class TheEllipse { final Color color; final double startX; final double startY; double updateStartX; double updateStartY; final bool isClear; final double width; TheEllipse({ this.color = Colors.black, this.width = 4, this.isClear = false, this.startX = 0, this.startY = 0, this.updateStartX = 0, this.updateStartY = 0, }); } class BoardPainter extends CustomPainter { final List strokes; final List theellipse; final int type; final double height; final double width; final double xi_w; final double xi_h; BoardPainter({ this.type, this.strokes, this.theellipse, this.height, this.width, this.xi_w, this.xi_h, }); @override void paint(Canvas canvas, Size size) { canvas.clipRect(Rect.fromLTWH(0, 0, width*xi_w, height*xi_h)); canvas.drawRect( Rect.fromLTWH(0, 0, width*xi_w, height*xi_h), Paint()..color = Colors.transparent, ); canvas.saveLayer(Rect.fromLTWH(0, 0, width*xi_w, height*xi_h), Paint()); if (type == 1) { ///绘制矩形 for (final stroke in strokes) { if (stroke.updateStartX != null && stroke.updateStartX > 0) { if (stroke.updateStartY != null && stroke.updateStartY > 0) { final paint = Paint() ..strokeWidth = stroke.width*xi_w ..color = stroke.isClear ? Colors.transparent : stroke.color ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke ..blendMode = stroke.isClear ? BlendMode.clear : BlendMode.srcOver; canvas.drawRect( Rect.fromLTWH( stroke.startX*xi_w, stroke.startY*xi_h, stroke.updateStartX*xi_w - stroke.startX*xi_w, stroke.updateStartY*xi_h - stroke.startY*xi_h), paint, ); } } } } else if (type == 2) { ///绘制椭圆 for (final theell in theellipse) { if (theell.updateStartX != null && theell.updateStartX > 0) { if (theell.updateStartY != null && theell.updateStartY > 0) { final paint = Paint() ..strokeWidth = theell.width*xi_w ..color = theell.isClear ? Colors.transparent : theell.color ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke ..blendMode = theell.isClear ? BlendMode.clear : BlendMode.srcOver; canvas.drawOval( Rect.fromPoints( Offset( theell.startX*xi_w, theell.startY*xi_h, ), Offset(theell.updateStartX*xi_w, theell.updateStartY*xi_h)), paint, ); } } } } canvas.restore(); } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } } HandLineLastBoard import 'dart:ui'; import 'dart:ui' as UI; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:yirui_flutter_app/event/base_event.dart'; import 'package:yirui_flutter_app/event/handbiaozhu_event.dart'; class HandLineLastBoard extends StatefulWidget { ///手写笔颜色 final Color painColor; ///手写笔宽度 final double paintWidth; ///手写笔控制器 final HandLineBoardLastController boardController; final double width; final double height; final EventBus eventBusLine; final double xi_w; final double xi_h; HandLineLastBoard({ Key key, this.painColor, this.paintWidth, @required this.boardController, this.width, this.height, this.eventBusLine, this.xi_w, this.xi_h, }) : super(key: key); @override _HandLineLastBoardState createState() => _HandLineLastBoardState(); } class _HandLineLastBoardState extends State { List _strokes = []; bool isClear = false; double starty = 0; @override void initState() { super.initState(); widget.boardController.bindContext(context); widget.eventBusLine.on().listen((event) { if (event.obj["type"] == 2) { if (mounted) { DragUpdateDetails details = event.obj["obj"]; setState(() { _strokes.last.path.lineTo( details.localPosition.dx*widget.xi_w, details.localPosition.dy*widget.xi_h - starty*widget.xi_h); }); widget.boardController.refStrokes(_strokes); } } else if (event.obj["type"] == 1) { double startX = event.obj["startX"]; double startY = event.obj["startY"]; final newStroke = Stroke( color: widget.painColor, width: widget.paintWidth, isClear: isClear, ); newStroke.path.moveTo(startX*widget.xi_w, startY*widget.xi_h); _strokes.add(newStroke); widget.boardController.refStrokes(_strokes); } }); } @override Widget build(BuildContext context) { return CustomPaint( painter: BoardPainter( strokes: _strokes, width: widget.width, height: widget.height, xi_w: widget.xi_w, xi_h: widget.xi_h), size: Size.infinite, ); } } class HandLineBoardLastController extends ChangeNotifier { BuildContext _context; List strokes = []; void bindContext(BuildContext context) { _context = context; } Future get uiImage { UI.PictureRecorder recorder = UI.PictureRecorder(); Canvas canvas = Canvas(recorder); BoardPainter painter = BoardPainter(); Size size = _context.size; painter.paint(canvas, size); return recorder .endRecording() .toImage(size.width.floor(), size.height.floor()); } void refStrokes(List newValue) { if (strokes != newValue) { strokes = newValue; } notifyListeners(); } void clearBoard() { strokes.clear(); notifyListeners(); } } class Stroke { final path = Path(); final Color color; final double width; final bool isClear; Stroke({ this.color = Colors.black, this.width = 4, this.isClear = false, }); } class BoardPainter extends CustomPainter { final List strokes; final double height; final double width; final double xi_w; final double xi_h; BoardPainter({ this.strokes, this.height, this.width, this.xi_w, this.xi_h, }); @override void paint(Canvas canvas, Size size) { canvas.clipRect(Rect.fromLTWH(0, 0, width*xi_w, height*xi_h)); canvas.drawRect( Rect.fromLTWH(0, 0, width*xi_w, height*xi_h), Paint()..color = Colors.transparent, ); canvas.saveLayer(Rect.fromLTWH(0, 0, width*xi_w, height*xi_h), Paint()); for (final stroke in strokes) { final paint = Paint() ..strokeWidth = stroke.width*xi_w ..color = stroke.isClear ? Colors.transparent : stroke.color ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke ..blendMode = stroke.isClear ? BlendMode.clear : BlendMode.srcOver; canvas.drawPath(stroke.path, paint); } canvas.restore(); } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } }

以上代码便是主体代码,接下来我解释下我的思路以及为什么要这么做,如果有好的方案欢迎评论。 可以看到我的绘制区域宽高一开始就固定了大小,那么图片宽高由图片真实宽高到绘制区域宽高去适配,缩放到一个相对比例的Widget,上下分为了三层,由下往上是真实图片大小区,遮挡层,操作区,由操作区操作的动作同步到真实图片大小区,最后上传时直接把真实大小图层区转图片上传,这时可能会好奇的问为什么不将操作好的Widget在点击保存时候再缩放到真实大小图层的图片上传,一开始我是这么做的,但Widget转图片过程是耗时的,体验很差。

RenderRepaintBoundary repaintBoundary = _handglobalKey.currentContext.findRenderObject(); UI.Image image = await repaintBoundary.toImage(pixelRatio: 1.0); ByteData byteData = await image.toByteData(format: UI.ImageByteFormat.png);

这一步是非常耗时的,为了减少这部分逻辑,只能在操作时候,相当于在看不到的view层进行模拟操作了所有动作,而保存实际避免了两个问题:1.图片转换过程中耗时问题 2.图片失真问题(图片放到到真实大小图片和真实大小区域绘制相同区域是不一样的)。 以下是我实现的效果。有问题欢迎评论留言。

device-2021-06-26-105821.png device-2021-06-26-105852.png device-2021-06-26-105949.png device-2021-06-26-110035.png device-2021-06-26-110110.png


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3