演示:
代码:
import 'dart:ui';
import 'package:flutter/material.dart' hide Image;
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kq_flutter_widgets/widgets/animate/stack.dart';
import 'package:kq_flutter_widgets/widgets/button/kq_small_button.dart';
import 'package:kq_flutter_widgets/widgets/update/update_view.dart';
///抠图软件原型
class DrawPathTest extends StatefulWidget {
const DrawPathTest({super.key});
@override
State<StatefulWidget> createState() => DrawPathTestState();
}
class DrawPathTestState extends State<DrawPathTest> {
///是否绑定左右操作点,即操作一个点,另一个点自动计算
static bool isBind = true;
///击中范围半径
static double hitRadius = 5;
///绘制区域state持有
UpdateViewState? state;
///背景图
Image? _image;
///历史步骤存储
KqStack stackHistory = KqStack();
///回收站步骤存储
KqStack stackRecycleBin = KqStack();
///绘制步骤集合
List<Step> drawList = [];
///手指按下时点击的控制点的位置缓存
Step? hitControlStep;
///手指按下时点击的画线点的位置缓存
Step? hitDrawStep;
///闭合绘制完成状态,不再添加点
bool drawFinish = false;
@override
void initState() {
super.initState();
_load("https://c-ssl.duitang.com/uploads/item/201903/19/20190319001325_bjvzi.jpg")
.then((value) {
_image = value;
update();
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: LayoutBuilder(builder: (c, lc) {
return Container(
color: Colors.white60,
child: Listener(
onPointerDown: (v) {
Offset src = v.localPosition;
///判断是否hit
hitDrawStep = _isHitDrawPoint(src);
if (!drawFinish) {
if (hitDrawStep != null && hitDrawStep!.isFirst) {
_add(src, isLast: true);
drawFinish = true;
} else {
hitControlStep = _isHitControlPoint(src);
hitControlStep ??= _add(src);
}
update();
} else {
hitControlStep = _isHitControlPoint(src);
}
},
onPointerMove: (v) {
if (hitDrawStep != null) {
_update(hitDrawStep!, v.localPosition);
update();
} else if (hitControlStep != null) {
_update(hitControlStep!, v.localPosition);
update();
}
},
child: UpdateView(
build: (UpdateViewState state) {
this.state = state;
return CustomPaint(
size: Size(lc.maxWidth, lc.maxHeight),
painter: TestDraw(_image, drawList),
);
},
),
),
);
}),
),
Row(
children: [
SizedBox(width: 20.r),
Expanded(
child: KqSmallButton(
title: "撤销",
onTap: (disabled) {
_undo();
update();
},
),
),
SizedBox(width: 20.r),
Expanded(
child: KqSmallButton(
title: "重做",
onTap: (disabled) {
_redo();
update();
},
),
),
SizedBox(width: 20.r),
Expanded(
child: KqSmallButton(
title: "选择",
onTap: (disabled) {
_select();
update();
},
),
),
SizedBox(width: 20.r),
Expanded(
child: KqSmallButton(
title: "反选",
onTap: (disabled) {
_invert();
update();
},
),
),
SizedBox(width: 20.r),
Expanded(
child: KqSmallButton(
title: "删除",
onTap: (disabled) {
_delete();
update();
},
),
),
SizedBox(width: 20.r),
],
),
SizedBox(height: 20.r),
],
);
}
///更新绘制区域
update() {
state?.update();
}
///添加点
Step _add(Offset offset, {bool isLast = false}) {
Step step = Step(offset, offset, offset);
step.isLast = isLast;
if (drawList.isEmpty) {
step.isFirst = true;
}
//添加到历史
stackHistory.push(step);
//添加到绘制列表
drawList.add(step);
//清除垃圾箱
stackRecycleBin.clear();
return step;
}
///判断是否点击在控制点上
Step? _isHitControlPoint(Offset src) {
for (Step step in drawList) {
if (_distance(step.pointRight, src) < hitRadius) {
step.hitPointType = PointType.pointRight;
return step;
} else if (_distance(step.pointLeft, src) < hitRadius) {
step.hitPointType = PointType.pointLeft;
return step;
}
}
return null;
}
///判断是否点击在连接点上
Step? _isHitDrawPoint(Offset src) {
for (Step step in drawList) {
if (_distance(step.point, src) < hitRadius) {
step.hitPointType = PointType.point;
return step;
}
}
return null;
}
///更新点信息
_update(Step hitStep, Offset target) {
if (hitStep.hitPointType == PointType.pointRight) {
hitStep.pointRight = target;
if (isBind) {
hitStep.pointLeft = hitStep.point.scale(2, 2) - target;
}
} else if (hitStep.hitPointType == PointType.pointLeft) {
hitStep.pointLeft = target;
if (isBind) {
hitStep.pointRight = hitStep.point.scale(2, 2) - target;
}
} else if (hitStep.hitPointType == PointType.point) {
hitStep.pointLeft = hitStep.pointLeft - hitStep.point + target;
hitStep.pointRight = hitStep.pointRight - hitStep.point + target;
hitStep.point = target;
}
}
///两点距离
double _distance(Offset one, Offset two) {
return (one - two).distance;
}
///撤销、后退
_undo() {
Step? step = stackHistory.pop();
if (step != null) {
drawList.remove(step);
stackRecycleBin.push(step);
}
}
///重做、前进
_redo() {
Step? step = stackRecycleBin.pop();
if (step != null) {
drawList.add(step);
stackHistory.push(step);
}
}
///选择、完成
_select() {}
///反选、完成
_invert() {}
///删除
_delete() {}
///加载图片
Future<Image> _load(String url) async {
ByteData data = await NetworkAssetBundle(Uri.parse(url)).load(url);
Codec codec = await instantiateImageCodec(data.buffer.asUint8List());
FrameInfo fi = await codec.getNextFrame();
return fi.image;
}
}
class TestDraw extends CustomPainter {
static double width = 260;
static double width1 = 50;
static double height1 = 100;
///绘制集合
final List<Step> draw;
///背景图片
final Image? image;
Step? tempStep;
Step? tempFirstStep;
TestDraw(this.image, this.draw);
@override
void paint(Canvas canvas, Size size) {
///绘制背景
if (image != null) {
canvas.drawImageRect(
image!,
Rect.fromLTRB(
0,
0,
image!.width.toDouble(),
image!.height.toDouble(),
),
Rect.fromLTRB(
width1,
height1,
width + width1,
width * image!.height / image!.width + height1,
),
Paint(),
);
}
if (draw.isNotEmpty) {
///构建画点与点之间的连线的path
Path path = Path();
///绘制点和线
for (int i = 0; i < draw.length; i++) {
Step step = draw[i];
if (!step.isLast) {
canvas.drawCircle(step.point, 4.r, Paint()..color = Colors.red);
canvas.drawCircle(
step.pointLeft, 4.r, Paint()..color = Colors.purple);
canvas.drawCircle(
step.pointRight, 4.r, Paint()..color = Colors.purple);
///画控制点和连线点之间的线段
canvas.drawLine(
step.point,
step.pointLeft,
Paint()
..color = Colors.green
..style = PaintingStyle.stroke);
canvas.drawLine(
step.point,
step.pointRight,
Paint()
..color = Colors.green
..style = PaintingStyle.stroke);
}
///构建画点与点之间的连线的path
if (step.isLast) {
if (tempFirstStep != null && tempStep != null) {
path.cubicTo(
tempStep!.pointRight.dx,
tempStep!.pointRight.dy,
tempFirstStep!.pointLeft.dx,
tempFirstStep!.pointLeft.dy,
tempFirstStep!.point.dx,
tempFirstStep!.point.dy,
);
}
} else {
//处理初始点
if (step.isFirst) {
tempFirstStep = step;
path.moveTo(step.point.dx, step.point.dy);
}
if (tempStep != null) {
path.cubicTo(
tempStep!.pointRight.dx,
tempStep!.pointRight.dy,
step.pointLeft.dx,
step.pointLeft.dy,
step.point.dx,
step.point.dy,
);
}
}
tempStep = step;
}
if (draw.length >= 2) {
canvas.drawPath(
path,
Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 1.5,
);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class Step {
///线条连接点
Offset point;
///右控制点
Offset pointRight;
///左控制点(起始点没有左控制点的)
Offset pointLeft;
///是否选中了点的类型
PointType hitPointType = PointType.pointRight;
///是否是第一个控制点
bool isFirst = false;
///是否是最后一个控制点
bool isLast = false;
Step(
this.point,
this.pointRight,
this.pointLeft,
);
}
///点类型
enum PointType {
///线条连接点
point,
///右控制点
pointRight,
///左控制点
pointLeft
}
stack代码:
///栈,先进后出
class KqStack<T> {
final List<T> _stack = [];
///入栈
push(T obj) {
_stack.add(obj);
}
///出栈
T? pop() {
if (_stack.isEmpty) {
return null;
} else {
return _stack.removeLast();
}
}
///栈长度
length() {
return _stack.length;
}
///清除栈
clear() {
_stack.clear();
}
}
主要思路:
更具手指点击屏幕的位置,记录点击的位置,并生成绘制点和两个控制点,手指拖动控制点时,动态刷新控制点位置,然后利用flutter绘制机制,在canvas上根据点的位置和控制点的位置绘制三阶贝塞尔曲线,实现钢笔工具效果。具体实现可以看代码,有注释,逻辑应该还算清晰。