前段時間參照今日相機有個需求,具體需求如下:
1.拍照或者相冊選擇圖片在編輯時候可以添加一個自定義的水印(包含時間和定位信息);
2.能在圖片上面繪制矩形或者橢圓;
3.能在圖片上面編輯文字標注,文字標注區(qū)域可以拖動;
4.可以自定義涂鴉;
Flutter也有很多庫,但全網好像并沒有此類的庫,那就自己動手實現。因為公司項目,我并沒有整理Demo出來。接下來我主要把自己的一些思路整理出來,也會放一些片段式的代碼。
可能會遇到的問題:
- 拍照圖片和相冊圖片編輯區(qū)域適配問題?
*繪制區(qū)域到底由什么決定?
*整個過程會經歷網絡圖片到本地,再從本地編輯之后上傳,上傳后失真問題
*圖片修改上傳后與自己標注不成比例問題
*文字標注拖動及邊界界定問題
*多個圖層同時進行操作可能會遇到的問題
拍照入口
需要引入的包
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 {
///可繪制區(qū)域背景真實高度
double canvasbg_height = 0;
///可繪制區(qū)域背景真實寬度
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;
///水印施工區(qū)域
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("施工區(qū)域:",
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("施工區(qū)域:",
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;
///矩形區(qū)域寬度
final double rc_width;
///矩形區(qū)域高度
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;
}
}
以上代碼便是主體代碼,接下來我解釋下我的思路以及為什么要這么做,如果有好的方案歡迎評論。
可以看到我的繪制區(qū)域寬高一開始就固定了大小,那么圖片寬高由圖片真實寬高到繪制區(qū)域寬高去適配,縮放到一個相對比例的Widget,上下分為了三層,由下往上是真實圖片大小區(qū),遮擋層,操作區(qū),由操作區(qū)操作的動作同步到真實圖片大小區(qū),最后上傳時直接把真實大小圖層區(qū)轉圖片上傳,這時可能會好奇的問為什么不將操作好的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.圖片失真問題(圖片放到到真實大小圖片和真實大小區(qū)域繪制相同區(qū)域是不一樣的)。
以下是我實現的效果。有問題歡迎評論留言。




