【-Flutter绘制集录-】第一画: 随机对称点头像
零、前言
1. 关于FlutterUnit 绘制集录
本文隶属于FlutterUnit周边,项目地址: FlutterUnit FlutterUnit绘制集录已拉开序幕,此集录会收录一些有意思的绘制作品,或一些典型的绘制样例来让大家接触Flutter更广大的可能性。(
下面的黑框也是绘制出来的哦
)
The Chaos |
Random Portrait |
Triangular Mesh |
Hypnotic Squares |
---|---|---|---|
2.关于本文画作 相关源码见这里
看到GitHub头像,有感而发。默认头像是一个5*5的格子,随机填充色块形成的图形
[1]. 可指定每行(列)的格子个数,且为奇数
[2]. 图形成左右对称
[3]. 半侧的图像点随机出现随机个
效果展示
5*5 |
5*5 |
9*9 |
---|---|---|
9*9 |
11*11 |
11*11 |
---|---|---|
3.这有什么用?
[1]. 练习绘制能力
[2]. 练习操纵数据的能力
[3]. 将widget保存为图片,你能获得默认头像
[4]. 最重要的是,挺好玩的~
一、画布的栅格与坐标
1. 基本思路
如下: 将我们的白板想象成一个栅格(
当然你可以在纸上打打草稿,没必要画出来
),这样就很容易看出关系。这时白板就变成了一个平面坐标系
,我们可以用一个二维坐标点
描述一个位置。再绘制出来这个矩形。
现在创建Position类用于描述坐标位置。
class Position {
final int x;
final int y;
Position(this.x, this.y);
@override
String toString() {
return 'Position{x: $x, y: $y}';
}
}
复制代码
2. 从一个点开始
将一个
Position
对象和栅格中的一个矩形区域
对应起来Rect.fromLTWH
可以根据左上角坐标和矩形宽高绘制矩形
Position(1, 1) |
Position(4, 3) |
Position(3, 2) |
|
---|---|---|---|
class PortraitPainter extends CustomPainter {
Paint _paint;//画笔
final int blockCount = 5; // 块数
final position = Position(1, 1); //点位
PortraitPainter():
_paint = Paint()..color = Colors.blue;
@override
void paint(Canvas canvas, Size size) {
// 裁剪当前区域
canvas.clipRect(
Rect.fromPoints(Offset.zero, Offset(size.width, size.height)));
var perW = size.width / blockCount;
var perH = size.height / blockCount;
_drawBlock(perW, perH, canvas, position);
}
// 绘制块
void _drawBlock(double perW, double perH, Canvas canvas, Position position) {
canvas.drawRect(
Rect.fromLTWH(position.x * perW, position.y * perH, perW, perH), _paint);
}
@override
bool shouldRepaint(PortraitPainter oldDelegate) => true;
}
复制代码
3. 绘制多点
当你能绘制一个点时,这个问题就已经从
图像问题
转化为坐标问题
使用坐标集List
,通过遍历坐标集, 绘制矩形块
即可
多点 |
去线 |
---|---|
final List positions = [
Position(1, 0),
Position(2, 1),
Position(0, 1),
Position(0, 2),
Position(1, 3),
Position(2, 4),
Position(3, 0),
Position(2, 1),
Position(4, 1),
Position(4, 2),
Position(3, 3),
];
@override
void paint(Canvas canvas, Size size) {
//英雄所见...
// 遍历坐标集, 绘制块
positions.forEach((element) {
_drawBlock(perW, perH, canvas, element);
});
}
二、随机数和数据操作
上面已经完成了数据与图形的对应关系,达到了
数即形,形即数的数形合一
境界。 一般在画板类中接收数据,画板中仅进行绘制的相关操作,可以提取出需要DIY的变量。
1. 画板类:PortraitPainter
class PortraitPainter extends CustomPainter {
Paint _paint;
final int blockCount;
final Color color;
final List positions;
PortraitPainter(this.positions, {this.blockCount = 9,this.color=Colors.blue})
: _paint = Paint()..color = color;
@override
void paint(Canvas canvas, Size size) {
canvas.clipRect(
Rect.fromPoints(Offset.zero, Offset(size.width, size.height)));
var perW = size.width / blockCount;
var perH = size.height / blockCount;
positions.forEach((element) {
_drawBlock(perW, perH, canvas, element);
});
}
void _drawBlock(double dW, double dH, Canvas canvas, Position position) {
canvas.drawRect(
Rect.fromLTWH(position.x * dW, position.y * dH, dW, dH), _paint);
}
@override
bool shouldRepaint(PortraitPainter oldDelegate) => true;
}
复制代码
2.组件类:RandomPortrait
通过
CustomPaint
使用画板,这里为了方便演示,点击时会刷新重建图形 现在只需要按照需求完成坐标点的生成即可。
class RandomPortrait extends StatefulWidget {
@override
_RandomPortraitState createState() => _RandomPortraitState();
}
class _RandomPortraitState extends State<RandomPortrait> {
List positions = [];
Random random = Random();
final int blockCount = 9;
@override
Widget build(BuildContext context) {
_initPosition();
return GestureDetector(
onTap: () {
setState(() {});
},
child: CustomPaint(
painter: PortraitPainter(positions, blockCount: blockCount)));
}
void _initPosition() {
// TODO 生成坐标点集
}
}
复制代码
3.生成点集
思路是先
生成左半边的点
,然后遍历点,左侧非中间的点时,添加对称点。关于对称处理:
如果a点和b点关于x=c对称。
则 (a.x + b.x)/2 = c
即 b.x = 2*c - a.x
1 |
2 |
3 |
---|---|---|
void _initPosition() {
positions.clear(); // 先清空点集
// 左半边的数量 (随机)
int randomCount = 2 + random.nextInt(blockCount * blockCount ~/ 2 - 2);
// 对称轴
var axis = blockCount ~/ 2 ;
//添加左侧随机点
for (int i = 0; i < randomCount; i++) {
int randomX = random.nextInt(axis+ 1);
int randomY = random.nextInt(blockCount);
var position = Position(randomX, randomY);
positions.add(position);
}
//添加对称点
for (int i = 0; i < positions.length; i++) {
if (positions[i].x < blockCount ~/ 2) {
positions
.add(Position(2 * axis - positions[i].x, positions[i].y));
}
}
}
这样基本上就完成了,后面可以做些优化
4. 小优化
[1]. 可以在绘制时留些边距,这样好看些 [2]. 当格数为9*9时,由于除不尽,可能导致相连块的小间隙(下图2),可以通过边长取整来解决
留边距 |
小间隙 |
小间隙优化 |
---|---|---|
class PortraitPainter extends CustomPainter {
Paint _paint;
final int blockCount;
final Color color;
final List positions;
final pd = 20.0;
PortraitPainter(this.positions,
{this.blockCount = 9, this.color = Colors.blue})
: _paint = Paint()..color = color;
@override
void paint(Canvas canvas, Size size) {
canvas.clipRect(
Rect.fromPoints(Offset.zero, Offset(size.width, size.height)));
var perW = (size.width - pd * 2) / (blockCount);
var perH = (size.height - pd * 2) / (blockCount);
canvas.translate(pd, pd);
positions.forEach((element) {
_drawBlock(perW, perH, canvas, element);
});
}
void _drawBlock(double dW, double dH, Canvas canvas, Position position) {
canvas.drawRect(
Rect.fromLTWH(
position.x * dW.floor()*1.0,
position.y * dH.floor()*1.0,
dW.floor()*1.0,
dH.floor()*1.0), _paint);
}
@override
bool shouldRepaint(PortraitPainter oldDelegate) => true;
}
复制代码
三、canvas绘制保存为图片
可以通过很多方法来读取一个Widget对应的图片数据,这里我使用
RepaintBoundary
,并简单封装了一下。获取图片数据后,可以根据需求保存到本地成为图片,也可以发送到服务器中,作为用户头像。反正字节流在手,万事无忧。
1.Widget2Image组件
简单封装一下,简化Widget2Image的操作流程。
class Widget2Image extends StatefulWidget {
final Widget child;
final ui.ImageByteFormat format;
Widget2Image(
{@required this.child,
this.format = ui.ImageByteFormat.rawRgba});
@override
Widget2ImageState createState() => Widget2ImageState();
static Widget2ImageState of(BuildContext context) {
final Widget2ImageState result = context.findAncestorStateOfType();
if (result != null)
return result;
throw FlutterError.fromParts([
ErrorSummary(
'Widget2Image.of() called with a context that does not contain a Widget2Image.'
),
]);
}
}
class Widget2ImageState extends State<Widget2Image> {
final GlobalKey _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return RepaintBoundary(
key: _globalKey,
child: widget.child,
);
}
Future loadImage() {
return _widget2Image(_globalKey);
}
Future _widget2Image(GlobalKey key) async {
RenderRepaintBoundary boundary = key.currentContext.findRenderObject();
//获得 ui.image
ui.Image img = await boundary.toImage();
//获取图片字节
var byteData = await img.toByteData(format: widget.format);
Uint8List bits = byteData.buffer.asUint8List();
return bits;
}
}
复制代码
2. 使用 Widget2Image
@override
Widget build(BuildContext context) {
_initPosition();
return Widget2Image( // 使用
format: ImageByteFormat.png,
child: Builder( // 使用Builder,让上下文下沉一级
builder: (ctx) => GestureDetector(
onTap: () {
setState(() {});
},
onLongPress: () async { // 长按时执行获取图片方法
var bytes = await Widget2Image.of(ctx).loadImage();
// 获取到图片字节数据 ---- 之后可随意操作
final dir = await getTemporaryDirectory();
final dest = path.join(dir.path, "widget.png");
await File(dest).writeAsBytes(bytes);
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("图片已保存到:$dest")));
},
child: CustomPaint(
painter: PortraitPainter(positions, blockCount: blockCount)),
),
));
}
复制代码
本文到这来就接近尾声了,应该是蛮有意思的。其实根据坐标系,可以做出很多有意思的东西。比如并非一定是画矩形,也可以画圆、三角形、甚至是图片。 如果把栅格分的更细些,这就很像一个
像素世界
。基于此,做个俄罗斯方块或者贪吃蛇什么的应该也可以。 最想说的一点是:驱动视图显示的是背后的数据, 脑洞会让数据拥有无限可能
。
最后欢迎大家多多支持 FlutterUnit
@张风捷特烈 2020.10.11 未允禁转
~ END ~
- 《深入理解C# 3.x的新特性》博文系列汇总
- 十一国庆节 之 “变量与函数同名时,会输出谁?”
- 挖坑无止境,来看看这个《this的指向》
- T-SQL Enhancement in SQL Server 2005[上篇]
- 初学js钻太深,不太好
- Linux shell 程序设计3——命令行程序
- Linux shell 程序设计2——bash的内置命令
- T-SQL Enhancement in SQL Server 2005[下篇]
- JS原型,a和b是不是失散多年的兄弟?
- Linux shell 程序设计1——安装及入门
- 偶遇--《坑新人--前端专用面试题》
- 简单的说下,(function(){...})() 与 (function(){...}()) 有什么区别?
- ASP.NET Process Model之二:ASP.NET Http Runtime Pipeline[上篇]
- Shell常用命令小结
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 在Laravel中实现使用AJAX动态刷新部分页面
- laravel 自定义常量的两种方案
- Thinkphp页面跳转设置跳转等待时间的操作
- Laravel 创建可以传递参数 Console服务的例子
- laravel实现查询最后执行的一条sql语句的方法
- 解决laravel groupBy 对查询结果进行分组出现的问题
- laravel批量生成假数据的方法
- 对laravel in 查询的使用方法详解
- PHP使用redis位图bitMap 实现签到功能
- thinkphp5+layui实现的分页样式示例
- Laravel实现搜索的时候分页并携带参数
- PHP7.3.10编译安装教程
- Laravel-添加后台模板AdminLte的实现方法
- PHP7 安装event扩展的实现方法
- 在laravel框架中使用model层的方法