前言
通过上一篇文章「如何优雅地画一张图」我们已经知道如何在画布里面绘画一张图了,这次我准备开一个系列讲解下手绘板的制作,可能包含:
手绘橡皮擦撤销重制重置图片导出命令模式
等功能。具体等到时候想到什么再写什么。
废话不多说,我们还是先来保证能够画个矩形:
class MyHomePage extends StatefulWidget {const MyHomePage({Key? key}) : super(key: key);@overrideState<MyHomePage> createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> {@overrideWidget build(BuildContext context) {return Container(color: Colors.white,child: CustomPaint(painter: MyPainter(),),);}}
class MyPainter extends CustomPainter {@overridevoid paint(Canvas canvas, Size size) {canvas.drawRect(const Rect.fromLTRB(50, 50, 200, 200), Paint());}@overridebool 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> {@overrideWidget 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.14285714285717I/flutter: onPanStart:开始移动,x:205.14285714285714,y:273.14285714285717I/flutter: onPanUpdate:移动,x:205.14285714285714,y:273.14285714285717I/flutter: onPanUpdate:移动,x:205.14285714285714,y:273.14285714285717I/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();@overrideWidget 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;@overridevoid 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);}}@overridebool shouldRepaint(covariant CustomPainter oldDelegate) {return true;}}
这里有一点要特别注意,那就是MyPainter(this.paintedBoardProvider): super(repaint: paintedBoardProvider);
,这是因为 PaintedBoardProvider 调用notifyListeners()
的时候,并不会像之前那样刷新 ChangeNotifierProvider 布局,而是直接刷新 MyPainter。(我们这里没有用 ChangeNotifierProvider 或者setState(() {});
去刷新布局,其实是个小优化,有时间可以讲下。)