零、前言
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<Position>
,通过遍历坐标集, 绘制矩形块
即可
多点 | 去线 |
---|---|
final List<Position> 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<Position> 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<Position> 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<Position> 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<Widget2ImageState>();
if (result != null)
return result;
throw FlutterError.fromParts(<DiagnosticsNode>[
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<Uint8List> loadImage() {
return _widget2Image(_globalKey);
}
Future<Uint8List> _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 ~