200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > 手绘板的制作——手绘(1)

手绘板的制作——手绘(1)

时间:2022-10-02 23:48:33

相关推荐

手绘板的制作——手绘(1)

前言

通过上一篇文章「如何优雅地画一张图」我们已经知道如何在画布里面绘画一张图了,这次我准备开一个系列讲解下手绘板的制作,可能包含:

手绘橡皮擦撤销重制重置图片导出命令模式

等功能。具体等到时候想到什么再写什么。

废话不多说,我们还是先来保证能够画个矩形:

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(() {});去刷新布局,其实是个小优化,有时间可以讲下。)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。