前段时间参照今日相机有个需求,具体需求如下:
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<ImageEditPage> 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<HandWriteMarkEvent>().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: <Widget>[
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: <Widget>[
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<Null>(
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: <Widget>[
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: <Widget>[
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<String, Object> 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<Map<String, Object>> 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<DraggableLastWiget> {
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<HandReactLastBoard> {
List<Rectangular> _strokes = [];
List<TheEllipse> _theEllipse = [];
bool isClear = false;
@override
void initState() {
super.initState();
widget.boardController.bindContext(context);
widget.eventBusBiaoZhu.on<HandCavasXYEvent>().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<Rectangular> strokes = [];
List<TheEllipse> ellipses = [];
void bindContext(BuildContext context) {
_context = context;
}
void refRectStrokes(List<Rectangular> newValue) {
if (strokes != newValue) {
strokes = newValue;
}
notifyListeners();
}
void refEllipseStrokes(List<TheEllipse> 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<Rectangular> strokes;
final List<TheEllipse> 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<HandLineLastBoard> {
List<Stroke> _strokes = [];
bool isClear = false;
double starty = 0;
@override
void initState() {
super.initState();
widget.boardController.bindContext(context);
widget.eventBusLine.on<HandCavasXYEvent>().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<Stroke> strokes = [];
void bindContext(BuildContext context) {
_context = context;
}
Future<UI.Image> 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<Stroke> 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<Stroke> 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.图片失真问题(图片放到到真实大小图片和真实大小区域绘制相同区域是不一样的)。
以下是我实现的效果。有问题欢迎评论留言。