-
Flutter实现给图片添加涂鸦功能
这篇文章主要介绍了利用Flutter实现给图片添加涂鸦功能,文中通过代码示例给大家讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
简介
先来张图,看一下最终效果
关闭和确定
关闭和确定功能对应的是界面左下角叉号,和右下角对钩,关闭按钮仅做取消当次涂鸦,读者可自行设置点击后功能,也可自行更改相应UI。选择功能点击后会执行一段把当前涂鸦后的图片合成并保存到本地的操作。具体请看示例代码。
颜色选择
颜色选择功能可选择和标识当前涂鸦颜色,和指示当前涂鸦颜色的选中状态(以白色外圈标识)。切换颜色后下一次涂鸦即会使用新的颜色。
撤销功能
撤销功能可撤销最近的一次涂鸦。如没有涂鸦时显示置灰的撤销按钮。
清除功能
清除功能可清除所有涂鸦,如当前没有任何涂鸦时显示置灰的清除按钮。
涂鸦图片的放大和缩小
可双指滑动切换涂鸦放大缩小的效果。
放大缩小后按照新的线条粗细继续涂鸦
涂鸦放大或缩小后,涂鸦线条会随之放大和缩小,此时如果继续涂鸦,则新涂鸦显示的粗细程度与放大或缩小后的线条粗细程度保持一致。
保存涂鸦图片到本地。
flutter涂鸦后的图片可合成新图片并保存到本地路径。
代码介绍
涂鸦颜色选择组件。
主要是显示为可配置的圆点和外圈
circle_ring_widget.dart
import 'package:flutter/material.dart';
class CircleRingWidget extends StatelessWidget {
late bool isShowRing;
late Color dotColor;
CircleRingWidget(this.isShowRing,this.dotColor, {super.key});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: CircleAndRingPainter(isShowRing,dotColor),
size: const Size(56.0, 81.0), // 调整尺寸大小
);
}
}
class CircleAndRingPainter extends CustomPainter {
late bool isShowRing;
late Color dotColor;
CircleAndRingPainter(this.isShowRing,this.dotColor);
@override
void paint(Canvas canvas, Size size) {
Paint circlePaint = Paint()
..color = dotColor // 设置圆点的颜色
..strokeCap = StrokeCap.round
..strokeWidth = 1.0;
Paint ringPaint = Paint()
..color = Colors.white // 设置圆环的颜色
..strokeCap = StrokeCap.round
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
Offset center = size.center(Offset.zero);
// 画一个半径为10的圆点
canvas.drawCircle(center, 13.0, circlePaint);
if(isShowRing){
// 画一个半径为20的圆环
canvas.drawCircle(center, 18.0, ringPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
存储颜色和食指划过点的数据类
color_offset.dart
import 'dart:ui';
class ColorOffset{
late Color color;
late List<Offset> offsets=[];
ColorOffset(Color color,List<Offset> offsets){
this.color=color;
this.offsets.addAll(offsets);
}
}
具体涂鸦点的绘制
此处是具体绘制涂鸦点的自定义view。大家是不是觉得哇,好简单呢。两个循环一嵌套,瞬间所有涂鸦就都出来了。其实做这个功能时,我参考了其他各种涂鸦控件,但是总觉得流程非常复杂。难以理解。原因是他们的颜色和点在数据层面都是混合到一起的,而且还得判断哪里是新画的涂鸦线条,来回控制。用这个demo的结构,相信各位读者一看就能知道里面的思路
doodle_painter.dart
import 'package:flutter/cupertino.dart';
import 'color_offset.dart';
class DoodleImagePainter extends CustomPainter {
late Map<int,ColorOffset> newPoints;
DoodleImagePainter(this.newPoints);
@override
void paint(Canvas canvas, Size size) {
newPoints.forEach((key, value) {
Paint paint = _getPaint(value.color);
for(int i=0;i<value.offsets.length - 1;i++){
//最后一个画点,其他画线
if(i==value.offsets.length-1){
canvas.drawCircle(value.offsets[i], 2.0, paint);
}else{
canvas.drawLine(value.offsets[i], value.offsets[i + 1], paint);
}
}
});
}
Paint _getPaint(Color color){
return Paint()
..color = color
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
涂鸦主要界面代码
包含整体涂鸦数据的构建
包含涂鸦图片的合成和本地存储
包含涂鸦颜色列表的自定义
包含涂鸦原图片的放大缩小
包含撤销一步和清屏功能
下面这些就是整体涂鸦相关功能代码,其中一些资源图片未提供,请根据需要自己去设计处获取。
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../player_base_control.dart';
import 'circle_ring_widget.dart';
import 'color_offset.dart';
import 'doodle_painter.dart';
class DoodleWidget extends PlayerBaseControllableWidget {
final String snapShotPath;
final ValueChanged<bool>? completed;
const DoodleWidget(super.controller,
{super.key, required this.snapShotPath, this.completed});
@override
State<StatefulWidget> createState() => _DoodleWidgetState();
}
class _DoodleWidgetState extends State<DoodleWidget> {
Map<int, ColorOffset> newPoints = {};
List<Offset> points = [];
int lineIndex = 0;
GlobalKey globalKey = GlobalKey();
int currentSelect = 0;
final double maxScale = 3.0;
final double minScale = 1.0;
List<Color> colors = const [
Color(0xffff0000),
Color(0xfffae03d),
Color(0xff6f52ff),
Color(0xffffffff),
Color(0xff000000)
];
TransformationController controller = TransformationController();
double realScale = 1.0;
Offset realTransLocation = Offset.zero;
late Image currentImg;
bool isSaved = false;
@override
void initState() {
currentImg = Image.memory(File(widget.snapShotPath).readAsBytesSync());
controller.addListener(() {
///获取矩阵里面的缩放具体值
realScale = controller.value.entry(0, 0);
///获取矩阵里面的位置偏移量
realTransLocation = Offset(controller.value.getTranslation().x,
controller.value.getTranslation().y);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraint) {
return InteractiveViewer(
panEnabled: false,
scaleEnabled: true,
maxScale: maxScale,
minScale: minScale,
transformationController: controller,
onInteractionStart: (ScaleStartDetails details) {
// print("--------------onInteractionStart执行了 dx=${details.focalPoint.dx} dy=${details.focalPoint.dy}");
},
onInteractionUpdate: (ScaleUpdateDetails details) {
if (details.pointerCount == 1) {
/// 获取 x,y 拿到值后进行缩放偏移等换算
var x = details.focalPoint.dx;
var y = details.focalPoint.dy;
var point = Offset(
_getScaleTranslateValue(x, realScale, realTransLocation.dx),
_getScaleTranslateValue(
y, realScale, realTransLocation.dy));
setState(() {
points.add(point);
newPoints[lineIndex] =
ColorOffset(colors[currentSelect], points);
});
}
},
onInteractionEnd: (ScaleEndDetails details) {
// print("onInteractionEnd执行了");
if (points.length > 5) {
newPoints[lineIndex] =
ColorOffset(colors[currentSelect], points);
lineIndex++;
}
// newPoints.addAll({lineIndex:ColorOffset(colors[currentSelect],points)});
//清空原数组
points.clear();
},
child: RepaintBoundary(
key: globalKey,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned.fill(child: currentImg),
Positioned.fill(
child:
CustomPaint(painter: DoodleImagePainter(newPoints))),
],
),
),
);
})),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _bottomActions(),
)
],
);
}
double _getScaleTranslateValue(
double current, double scale, double translate) {
return current / scale - translate / scale;
}
Widget _bottomActions() {
return Container(
height: 81,
color: const Color(0xaa17161f),
child: Row(
children: [
/// 关闭按钮
SizedBox(
width: 95,
height: 81,
child: Center(
child: GestureDetector(
onTap: () {
widget.completed?.call(false);
},
child: Image.asset(
"images/icon_close_white.webp",
width: 30,
height: 30,
scale: 3,
package: "koo_daxue_record_player",
),
),
),
),
const VerticalDivider(
thickness: 1,
indent: 15,
endIndent: 15,
color: Colors.white,
),
Row(
children: _colorListWidget(),
),
Expanded(child: Container()),
/// 退一步按钮
SizedBox(
width: 66,
height: 81,
child: GestureDetector(
onTap: () {
setState(() {
if (lineIndex > 0) {
lineIndex--;
newPoints.remove(lineIndex);
}
});
},
child: Center(
child: Image.asset(
lineIndex == 0
? "images/icon_undo.webp"
: "images/icon_undo_white.webp",
width: 30,
height: 30,
scale: 3,
package: "koo_daxue_record_player",
)),
),
),
/// 清除按钮
SizedBox(
width: 66,
height: 81,
child: Center(
child: GestureDetector(
onTap: () {
setState(() {
lineIndex = 0;
newPoints.clear();
});
},
child: Image.asset(
lineIndex == 0
? "images/icon_clear_doodle.webp"
: "images/icon_clear_doodle_white.webp",
width: 30,
height: 30,
scale: 3,
package: "koo_daxue_record_player",
),
)),
),
const VerticalDivider(
thickness: 1,
indent: 15,
endIndent: 15,
color: Colors.white,
),
/// 确定按钮
SizedBox(
width: 85,
height: 81,
child: Center(
child: GestureDetector(
onTap: () {
if (isSaved) return;
isSaved = true;
if (newPoints.isEmpty) {
widget.completed?.call(false);
return;
}
saveDoodle(widget.snapShotPath).then((value) {
if (value) {
widget.completed?.call(true);
} else {
widget.completed?.call(false);
}
});
},
child: Image.asset(
"images/icon_finish_white.webp",
width: 30,
height: 30,
scale: 3,
package: "koo_daxue_record_player",
),
)),
)
],
),
);
}
List<Widget> _colorListWidget() {
List<Widget> widgetList = [];
for (int i = 0; i < colors.length; i++) {
Color color = colors[i];
widgetList.add(GestureDetector(
onTap: () {
setState(() {
currentSelect = i;
});
},
child: CircleRingWidget(i == currentSelect, color),
));
}
return widgetList;
}
Future<bool> saveDoodle(String imgPath) async {
try {
RenderRepaintBoundary boundary =
globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
ui.Image image = await boundary.toImage(pixelRatio: 3.0);
ByteData? byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
// 保存图片到文件
File imgFile = File(imgPath);
await imgFile.writeAsBytes(pngBytes);
return true;
} catch (e) {
return false;
}
}
}
复制AI辅助
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../player_base_control.dart';
import 'circle_ring_widget.dart';
import 'color_offset.dart';
import 'doodle_painter.dart';
class DoodleWidget extends PlayerBaseControllableWidget {
final String snapShotPath;
final ValueChanged<bool>? completed;
const DoodleWidget(super.controller,
{super.key, required this.snapShotPath, this.completed});
@override
State<StatefulWidget> createState() => _DoodleWidgetState();
}
class _DoodleWidgetState extends State<DoodleWidget> {
Map<int, ColorOffset> newPoints = {};
List<Offset> points = [];
int lineIndex = 0;
GlobalKey globalKey = GlobalKey();
int currentSelect = 0;
final double maxScale = 3.0;
final double minScale = 1.0;
List<Color> colors = const [
Color(0xffff0000),
Color(0xfffae03d),
Color(0xff6f52ff),
Color(0xffffffff),
Color(0xff000000)
];
TransformationController controller = TransformationController();
double realScale = 1.0;
Offset realTransLocation = Offset.zero;
late Image currentImg;
bool isSaved = false;
@override
void initState() {
currentImg = Image.memory(File(widget.snapShotPath).readAsBytesSync());
controller.addListener(() {
///获取矩阵里面的缩放具体值
realScale = controller.value.entry(0, 0);
///获取矩阵里面的位置偏移量
realTransLocation = Offset(controller.value.getTranslation().x,
controller.value.getTranslation().y);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraint) {
return InteractiveViewer(
panEnabled: false,
scaleEnabled: true,
maxScale: maxScale,
minScale: minScale,
transformationController: controller,
onInteractionStart: (ScaleStartDetails details) {
// print("--------------onInteractionStart执行了 dx=${details.focalPoint.dx} dy=${details.focalPoint.dy}");
},
onInteractionUpdate: (ScaleUpdateDetails details) {
if (details.pointerCount == 1) {
/// 获取 x,y 拿到值后进行缩放偏移等换算
var x = details.focalPoint.dx;
var y = details.focalPoint.dy;
var point = Offset(
_getScaleTranslateValue(x, realScale, realTransLocation.dx),
_getScaleTranslateValue(
y, realScale, realTransLocation.dy));
setState(() {
points.add(point);
newPoints[lineIndex] =
ColorOffset(colors[currentSelect], points);
});
}
},
onInteractionEnd: (ScaleEndDetails details) {
// print("onInteractionEnd执行了");
if (points.length > 5) {
newPoints[lineIndex] =
ColorOffset(colors[currentSelect], points);
lineIndex++;
}
// newPoints.addAll({lineIndex:ColorOffset(colors[currentSelect],points)});
//清空原数组
points.clear();
},
child: RepaintBoundary(
key: globalKey,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned.fill(child: currentImg),
Positioned.fill(
child:
CustomPaint(painter: DoodleImagePainter(newPoints))),
],
),
),
);
})),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _bottomActions(),
)
],
);
}
double _getScaleTranslateValue(
double current, double scale, double translate) {
return current / scale - translate / scale;
}
Widget _bottomActions() {
return Container(
height: 81,
color: const Color(0xaa17161f),
child: Row(
children: [
/// 关闭按钮
SizedBox(
width: 95,
height: 81,
child: Center(
child: GestureDetector(
onTap: () {
widget.completed?.call(false);
},
child: Image.asset(
"images/icon_close_white.webp",
width: 30,
height: 30,
scale: 3,
package: "koo_daxue_record_player",
),
),
),
),
const VerticalDivider(
thickness: 1,
indent: 15,
endIndent: 15,
color: Colors.white,
),
Row(
children: _colorListWidget(),
),
Expanded(child: Container()),
/// 退一步按钮
SizedBox(
width: 66,
height: 81,
child: GestureDetector(
onTap: () {
setState(() {
if (lineIndex > 0) {
lineIndex--;
newPoints.remove(lineIndex);
}
});
},
child: Center(
child: Image.asset(
lineIndex == 0
? "images/icon_undo.webp"
: "images/icon_undo_white.webp",
width: 30,
height: 30,
scale: 3,
package: "koo_daxue_record_player",
)),
),
),
/// 清除按钮
SizedBox(
width: 66,
height: 81,
child: Center(
child: GestureDetector(
onTap: () {
setState(() {
lineIndex = 0;
newPoints.clear();
});
},
child: Image.asset(
lineIndex == 0
? "images/icon_clear_doodle.webp"
: "images/icon_clear_doodle_white.webp",
width: 30,
height: 30,
scale: 3,
package: "koo_daxue_record_player",
),
)),
),
const VerticalDivider(
thickness: 1,
indent: 15,
endIndent: 15,
color: Colors.white,
),
/// 确定按钮
SizedBox(
width: 85,
height: 81,
child: Center(
child: GestureDetector(
onTap: () {
if (isSaved) return;
isSaved = true;
if (newPoints.isEmpty) {
widget.completed?.call(false);
return;
}
saveDoodle(widget.snapShotPath).then((value) {
if (value) {
widget.completed?.call(true);
} else {
widget.completed?.call(false);
}
});
},
child: Image.asset(
"images/icon_finish_white.webp",
width: 30,
height: 30,
scale: 3,
package: "koo_daxue_record_player",
),
)),
)
],
),
);
}
List<Widget> _colorListWidget() {
List<Widget> widgetList = [];
for (int i = 0; i < colors.length; i++) {
Color color = colors[i];
widgetList.add(GestureDetector(
onTap: () {
setState(() {
currentSelect = i;
});
},
child: CircleRingWidget(i == currentSelect, color),
));
}
return widgetList;
}
Future<bool> saveDoodle(String imgPath) async {
try {
RenderRepaintBoundary boundary =
globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
ui.Image image = await boundary.toImage(pixelRatio: 3.0);
ByteData? byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
// 保存图片到文件
File imgFile = File(imgPath);
await imgFile.writeAsBytes(pngBytes);
return true;
} catch (e) {
return false;
}
}
}
以上就是Flutter实现给图片添加涂鸦功能的详细内容,更多关于Flutter给图片添加涂鸦的资料请关注
原文链接:https://juejin.cn/post/7327501508659216424