Flutter 绘制集录 | 随机对称图案
关于本文画作
看到GitHub头像,有感而发。默认头像是一个5*5的格子,随机填充色块形成的图形
1[1]. 可指定每行(列)的格子个数,且为奇数
2[2]. 图形成左右对称
3[3]. 半侧的图像点随机出现随机个
效果展示
5*5 | 5*5 | 9*9 |
---|---|---|
9*9 | 11*11 | 11*11 |
---|---|---|
一、画布的栅格与坐标
1. 基本思路
如下: 将我们的白板想象成一个栅格( 当然你可以在纸上打打草稿,没必要画出来
),这样就很容易看出关系。这时白板就变成了一个 平面坐标系
,我们可以用一个 二维坐标点
描述一个位置。再绘制出来这个矩形。
现在创建Position类用于描述坐标位置。
1class Position {
2 final int x;
3 final int y;
4
5 Position(this.x, this.y);
6
7 @override
8 String toString() {
9 return 'Position{x: $x, y: $y}';
10 }
11}
2. 从一个点开始
将一个 Position
对象和 栅格中的一个矩形区域
对应起来
Rect.fromLTWH
可以根据左上角坐标和矩形宽高绘制矩形
Position(1, 1) | Position(4, 3) | Position(3, 2) |
---|---|---|
1class PortraitPainter extends CustomPainter {
2 Paint _paint;//画笔
3 final int blockCount = 5; // 块数
4 final position = Position(1, 1); //点位
5
6 PortraitPainter():
7 _paint = Paint()..color = Colors.blue;
8
9 @override
10 void paint(Canvas canvas, Size size) {
11 // 裁剪当前区域
12 canvas.clipRect(
13 Rect.fromPoints(Offset.zero, Offset(size.width, size.height)));
14
15 var perW = size.width / blockCount;
16 var perH = size.height / blockCount;
17 _drawBlock(perW, perH, canvas, position);
18 }
19
20 // 绘制块
21 void _drawBlock(double perW, double perH, Canvas canvas, Position position) {
22 canvas.drawRect(
23 Rect.fromLTWH(position.x * perW, position.y * perH, perW, perH), _paint);
24 }
25
26 @override
27 bool shouldRepaint(PortraitPainter oldDelegate) => true;
28}
3. 绘制多点
当你能绘制一个点时,这个问题就已经从 图像问题
转化为 坐标问题
使用坐标集 List
,通过 遍历坐标集, 绘制矩形块
即可
多点 | 去线 |
---|---|
1final List positions = [
2 Position(1, 0),
3 Position(2, 1),
4 Position(0, 1),
5 Position(0, 2),
6 Position(1, 3),
7 Position(2, 4),
8 Position(3, 0),
9 Position(2, 1),
10 Position(4, 1),
11 Position(4, 2),
12 Position(3, 3),
13];
14
15@override
16void paint(Canvas canvas, Size size) {
17 //英雄所见...
18 // 遍历坐标集, 绘制块
19 positions.forEach((element) {
20 _drawBlock(perW, perH, canvas, element);
21 });
22}
二、随机数和数据操作
上面已经完成了数据与图形的对应关系,达到了 数即形,形即数的数形合一
境界。
一般在画板类中接收数据,画板中仅进行绘制的相关操作,可以提取出需要DIY的变量。
1. 画板类:PortraitPainter
1class PortraitPainter extends CustomPainter {
2 Paint _paint;
3
4 final int blockCount;
5 final Color color;
6 final List positions;
7
8 PortraitPainter(this.positions, {this.blockCount = 9,this.color=Colors.blue})
9 : _paint = Paint()..color = color;
10
11 @override
12 void paint(Canvas canvas, Size size) {
13 canvas.clipRect(
14 Rect.fromPoints(Offset.zero, Offset(size.width, size.height)));
15
16 var perW = size.width / blockCount;
17 var perH = size.height / blockCount;
18
19 positions.forEach((element) {
20 _drawBlock(perW, perH, canvas, element);
21 });
22 }
23
24 void _drawBlock(double dW, double dH, Canvas canvas, Position position) {
25 canvas.drawRect(
26 Rect.fromLTWH(position.x * dW, position.y * dH, dW, dH), _paint);
27 }
28
29 @override
30 bool shouldRepaint(PortraitPainter oldDelegate) => true;
31}
2.组件类:RandomPortrait
通过 CustomPaint
使用画板,这里为了方便演示,点击时会刷新重建图形
现在只需要按照需求完成坐标点的生成即可。
1class RandomPortrait extends StatefulWidget {
2 @override
3 _RandomPortraitState createState() => _RandomPortraitState();
4}
5
6class _RandomPortraitState extends State<RandomPortrait> {
7 List positions = [];
8 Random random = Random();
9 final int blockCount = 9;
10
11 @override
12 Widget build(BuildContext context) {
13 _initPosition();
14 return GestureDetector(
15 onTap: () {
16 setState(() {});
17 },
18 child: CustomPaint(
19 painter: PortraitPainter(positions, blockCount: blockCount)));
20 }
21
22 void _initPosition() {
23 // TODO 生成坐标点集
24 }
25}
3.生成点集
思路是先 生成左半边的点
,然后遍历点,左侧非中间的点时,添加对称点。关于对称处理:
1如果a点和b点关于x=c对称。
2则 (a.x + b.x)/2 = c
3即 b.x = 2*c - a.x
1 | 2 | 3 |
---|---|---|
1 void _initPosition() {
2 positions.clear(); // 先清空点集
3
4 // 左半边的数量 (随机)
5 int randomCount = 2 + random.nextInt(blockCount * blockCount ~/ 2 - 2);
6 // 对称轴
7 var axis = blockCount ~/ 2 ;
8 //添加左侧随机点
9 for (int i = 0; i < randomCount; i++) {
10 int randomX = random.nextInt(axis+ 1);
11 int randomY = random.nextInt(blockCount);
12 var position = Position(randomX, randomY);
13 positions.add(position);
14 }
15 //添加对称点
16 for (int i = 0; i < positions.length; i++) {
17 if (positions[i].x < blockCount ~/ 2) {
18 positions
19 .add(Position(2 * axis - positions[i].x, positions[i].y));
20 }
21 }
22 }
这样基本上就完成了,后面可以做些优化
4. 小优化
[1]. 可以在绘制时留些边距,这样好看些
[2]. 当格数为9*9时,由于除不尽,可能导致相连块的小间隙(下图2),可以通过边长取整来解决
留边距 | 小间隙 | 小间隙优化 |
---|---|---|
1class PortraitPainter extends CustomPainter {
2 Paint _paint;
3
4 final int blockCount;
5 final Color color;
6 final List positions;
7
8 final pd = 20.0;
9
10 PortraitPainter(this.positions,
11 {this.blockCount = 9, this.color = Colors.blue})
12 : _paint = Paint()..color = color;
13
14 @override
15 void paint(Canvas canvas, Size size) {
16 canvas.clipRect(
17 Rect.fromPoints(Offset.zero, Offset(size.width, size.height)));
18
19 var perW = (size.width - pd * 2) / (blockCount);
20 var perH = (size.height - pd * 2) / (blockCount);
21
22 canvas.translate(pd, pd);
23 positions.forEach((element) {
24 _drawBlock(perW, perH, canvas, element);
25 });
26 }
27
28 void _drawBlock(double dW, double dH, Canvas canvas, Position position) {
29 canvas.drawRect(
30 Rect.fromLTWH(
31 position.x * dW.floor()*1.0,
32 position.y * dH.floor()*1.0,
33 dW.floor()*1.0,
34 dH.floor()*1.0), _paint);
35 }
36
37 @override
38 bool shouldRepaint(PortraitPainter oldDelegate) => true;
39}
三、canvas绘制保存为图片
可以通过很多方法来读取一个Widget对应的图片数据,这里我使用 RepaintBoundary
,并简单封装了一下。获取图片数据后,可以根据需求保存到本地成为图片,也可以发送到服务器中,作为用户头像。反正字节流在手,万事无忧。
1.Widget2Image组件
简单封装一下,简化Widget2Image的操作流程。
1class Widget2Image extends StatefulWidget {
2 final Widget child;
3 final ui.ImageByteFormat format;
4
5 Widget2Image(
6 {@required this.child,
7 this.format = ui.ImageByteFormat.rawRgba});
8
9 @override
10 Widget2ImageState createState() => Widget2ImageState();
11
12
13 static Widget2ImageState of(BuildContext context) {
14 final Widget2ImageState result = context.findAncestorStateOfType();
15 if (result != null)
16 return result;
17 throw FlutterError.fromParts([
18 ErrorSummary(
19 'Widget2Image.of() called with a context that does not contain a Widget2Image.'
20 ),
21 ]);
22 }
23}
24
25class Widget2ImageState extends State<Widget2Image> {
26 final GlobalKey _globalKey = GlobalKey();
27
28 @override
29 Widget build(BuildContext context) {
30 return RepaintBoundary(
31 key: _globalKey,
32 child: widget.child,
33 );
34 }
35
36 Future loadImage() {
37 return _widget2Image(_globalKey);
38 }
39
40 Future _widget2Image(GlobalKey key) async {
41 RenderRepaintBoundary boundary = key.currentContext.findRenderObject();
42 //获得 ui.image
43 ui.Image img = await boundary.toImage();
44 //获取图片字节
45 var byteData = await img.toByteData(format: widget.format);
46 Uint8List bits = byteData.buffer.asUint8List();
47 return bits;
48 }
49}
2. 使用 Widget2Image
1 @override
2 Widget build(BuildContext context) {
3 _initPosition();
4 return Widget2Image( // 使用
5 format: ImageByteFormat.png,
6 child: Builder( // 使用Builder,让上下文下沉一级
7 builder: (ctx) => GestureDetector(
8 onTap: () {
9 setState(() {});
10 },
11 onLongPress: () async { // 长按时执行获取图片方法
12 var bytes = await Widget2Image.of(ctx).loadImage();
13
14 // 获取到图片字节数据 ---- 之后可随意操作
15 final dir = await getTemporaryDirectory();
16 final dest = path.join(dir.path, "widget.png");
17 await File(dest).writeAsBytes(bytes);
18 Scaffold.of(context)
19 .showSnackBar(SnackBar(content: Text("图片已保存到:$dest")));
20 },
21 child: CustomPaint(
22 painter: PortraitPainter(positions, blockCount: blockCount)),
23 ),
24 ));
25 }
本文到这来就接近尾声了,应该是蛮有意思的。其实根据坐标系,可以做出很多有意思的东西。比如并非一定是画矩形,也可以画圆、三角形、甚至是图片。
如果把栅格分的更细些,这就很像一个 像素世界
。基于此,做个俄罗斯方块或者贪吃蛇什么的应该也可以。
最想说的一点是: 驱动视图显示的是背后的数据, 脑洞会让数据拥有无限可能
。