手绘板的制作——手绘(1)
前言
- 手绘
- 橡皮擦
- 撤销
- 重制
- 重置
- 图片导出
- 命令模式
等功能。具体等到时候想到什么再写什么。
废话不多说,我们还是先来保证能够画个矩形:
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint(
painter: MyPainter(),
),
);
}
}
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(const Rect.fromLTRB(50, 50, 200, 200), Paint());
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
drawPath
为什么要先了解 drawPath ?而不是手势?这是因为手绘其实就是根据手指的移动进行绘制,而这个绘制就是用 drawPath 来实现的,只要学会了 drawPath,后续根据手指的移动进行 path 的制作,然后再进行绘制即可。
我们来看下其 API:
void drawPath(Path path, Paint paint)
关于 path 的时候有很多种方式,这里就不进行详解了,我们目前只需要用到 void moveTo(double x, double y)
和 void lineTo(double x, double y)
,等后续用到其它的再进行额外说明。
- moveTo:将绘制点移动到某个位置。
- lineTo:将当前绘制点与目标绘制点进行链接,并且将目标绘制点设置为当前绘制点。
然后我们通过一个简单的示例来看看效果:
final path = Path()
..moveTo(150, 30)
..lineTo(25, 60)
..lineTo(70, 100)
..lineTo(100, 50);
canvas.drawPath(path, Paint());
em...没有抗锯齿和默认填充了,我们来改改 Paint:
final paint = Paint()
..isAntiAlias = true
..style = PaintingStyle.stroke;
final path = Path()
..moveTo(150, 30)
..lineTo(25, 60)
..lineTo(70, 100)
..lineTo(100, 50);
canvas.drawPath(path, paint);
这就跟我们想象中的差不多了。
手势
在写代码前,我们先梳理下逻辑:
- 将每次的完整手势流程存储为一个 path。如何定义是一个完整的手势流程?那就是手指从按下、到移动、到抬起,就是一次完整的手势流程。
- 按下操作,其实就是记录 path 的
moveTo()
。 - 移动操作,其实就是记录 path 的
lineTo()
。 - 抬起操作,其实就是标识本次手势流程结束了。
下面我们来看下,具体代码该怎么实现。
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: GestureDetector(
onPanDown: (details){
print("onPanDown:刚按下,x:${details.localPosition.dx},y:${details.localPosition.dy}");
},
onPanStart: (details){
print("onPanStart:开始移动,x:${details.localPosition.dx},y:${details.localPosition.dy}");
},
onPanUpdate: (details){
print("onPanUpdate:移动,x:${details.localPosition.dx},y:${details.localPosition.dy}");
},
onPanEnd: (details){
print("onPanDown:移动结束");
},
child: CustomPaint(
painter: MyPainter(),
),
),
);
}
}
日志输出:
I/flutter: onPanDown:刚按下,x:205.14285714285714,y:273.14285714285717
I/flutter: onPanStart:开始移动,x:205.14285714285714,y:273.14285714285717
I/flutter: onPanUpdate:移动,x:205.14285714285714,y:273.14285714285717
I/flutter: onPanUpdate:移动,x:205.14285714285714,y:273.14285714285717
I/flutter: onPanDown:移动结束
其实就是完成 onPanDown、onPanStart、onPanUpdate、onPanEnd 的回调书写,不过由于 onPanDown 和 onPanStart 功能较为相近,并且 onPanStart 明确为移动开始就回调,所以我们后续就只使用 onPanStart,不使用 onPanDown。
下面我们新建一个类来存储当前绘画相关信息:
class Stroke {
final path = Path(); // 绘画路径
Color color; // 画笔颜色
double width; // 画笔粗细
Stroke({
this.color = Colors.black,
this.width = 3,
});
}
然后新建一个 ChangeNotifier 来存储绘画的相关操作,同时也便于后续更新,因为我们每次绘画其实都是需要刷新画布,将最新效果绘画出来:
class PaintedBoardProvider extends ChangeNotifier {
// 存储绘画数据
final List<Stroke> _strokes = [];
List<Stroke> get strokes => _strokes;
// 颜色
var color = Colors.greenAccent;
// 笔画宽度
double paintWidth = 3;
/// 移动开始时
void onStart(DragStartDetails details) {
double startX = details.localPosition.dx;
double startY = details.localPosition.dy;
final newStroke = Stroke(
color: color,
width: paintWidth,
);
newStroke.path.moveTo(startX, startY);
_strokes.add(newStroke);
}
/// 移动
void onUpdate(DragUpdateDetails details) {
_strokes.last.path
.lineTo(details.localPosition.dx, details.localPosition.dy);
notifyListeners();
}
}
将 GestureDetector 与 PaintedBoardProvider 进行关联,同时也把 PaintedBoardProvider 传递给 MyPainter,因为绘画时需要用到 PaintedBoardProvider 的数据,同时刷新时也需要用到 PaintedBoardProvider。
class _MyHomePageState extends State<MyHomePage> {
final PaintedBoardProvider _paintedBoardProvider = PaintedBoardProvider();
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: GestureDetector(
onPanStart: (details){
_paintedBoardProvider.onStart(details);
},
onPanUpdate: (details){
_paintedBoardProvider.onUpdate(details);
},
onPanEnd: (details){
print("onPanDown:移动结束");
},
child: CustomPaint(
painter: MyPainter(_paintedBoardProvider),
),
),
);
}
}
class MyPainter extends CustomPainter {
MyPainter(this.paintedBoardProvider)
: super(repaint: paintedBoardProvider);
final PaintedBoardProvider paintedBoardProvider;
@override
void paint(Canvas canvas, Size size) {
// 获取绘画数据进行绘画
for (final stroke in paintedBoardProvider.strokes) {
final paint = Paint()
..strokeWidth = stroke.width
..color = stroke.color
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
canvas.drawPath(stroke.path, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
这里有一点要特别注意,那就是 MyPainter(this.paintedBoardProvider): super(repaint: paintedBoardProvider);
,这是因为 PaintedBoardProvider 调用 notifyListeners()
的时候,并不会像之前那样刷新 ChangeNotifierProvider 布局,而是直接刷新 MyPainter。(我们这里没有用 ChangeNotifierProvider 或者 setState(() {});
去刷新布局,其实是个小优化,有时间可以讲下。)
转载自:https://juejin.cn/post/7107102398476189732