18

Flutter 绘制集录 | 随机对称图案

 4 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzI0NjU3MDA4NQ%3D%3D&%3Bmid=2247484609&%3Bidx=1&%3Bsn=8c372ed663362a13edf3b2d264240a0d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

关于本文画作

看到GitHub头像,有感而发。默认头像是一个5*5的格子,随机填充色块形成的图形

1[1]. 可指定每行(列)的格子个数,且为奇数
2[2]. 图形成左右对称
3[3]. 半侧的图像点随机出现随机个

YveEfa.jpg!mobile

效果展示

5*5 5*5 9*9 BNzU7zz.jpg!mobileNbeEv2m.jpg!mobileBJVnmiM.jpg!mobile 9*9 11*11 11*11 vYJbmiz.jpg!mobilezyyiIfN.jpg!mobilezqami2e.jpg!mobile

一、画布的栅格与坐标

1. 基本思路

如下: 将我们的白板想象成一个栅格( 当然你可以在纸上打打草稿,没必要画出来 ),这样就很容易看出关系。这时白板就变成了一个 平面坐标系 ,我们可以用一个 二维坐标点 描述一个位置。再绘制出来这个矩形。

UVVfyaE.jpg!mobile

现在创建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) 3mEfQju.jpg!mobilejqiUvab.jpg!mobileInIvuui.jpg!mobile

1class PortraitPainter extends CustomPainter {
2  Paint _paint;//画笔
3  final int blockCount = 5// 块数
4  final position = Position(11); //点位
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<Position> ,通过 遍历坐标集, 绘制矩形块 即可

多点 去线 IZBZfaV.jpg!mobile3YJbMn3.jpg!mobile

1final List<Position> positions = [
2  Position(10),
3  Position(21),
4  Position(01),
5  Position(02),
6  Position(13),
7  Position(24),
8  Position(30),
9  Position(21),
10  Position(41),
11  Position(42),
12  Position(33),
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<Position> 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<Position> 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 3qYniun.jpg!mobileqaEZ7nq.jpg!mobileFzAVRrB.jpg!mobile

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),可以通过边长取整来解决

留边距 小间隙 小间隙优化 qMFvUf3.jpg!mobile2IzABn.jpg!mobilebEriEn3.jpg!mobile

1class PortraitPainter extends CustomPainter {
2  Paint _paint;
3
4  final int blockCount;
5  final Color color;
6  final List<Position> positions;
7
8  final pd = 20.0;
9
10  PortraitPainter(this.positions,
11      {this.blockCount = 9this.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 ,并简单封装了一下。获取图片数据后,可以根据需求保存到本地成为图片,也可以发送到服务器中,作为用户头像。反正字节流在手,万事无忧。

ua2iQjF.jpg!mobile

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<Widget2ImageState>();
15    if (result != null)
16      return result;
17    throw FlutterError.fromParts(<DiagnosticsNode>[
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<Uint8List> loadImage() {
37    return _widget2Image(_globalKey);
38  }
39
40  Future<Uint8List> _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  }

本文到这来就接近尾声了,应该是蛮有意思的。其实根据坐标系,可以做出很多有意思的东西。比如并非一定是画矩形,也可以画圆、三角形、甚至是图片。

如果把栅格分的更细些,这就很像一个 像素世界 。基于此,做个俄罗斯方块或者贪吃蛇什么的应该也可以。 

最想说的一点是: 驱动视图显示的是背后的数据, 脑洞会让数据拥有无限可能


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK