本系列,将通过 Flutter 实现一个全平台的像素编辑器应用。源码见开源项目 【pix_editor】
本篇将完成如下功能:
[1]
. 展示方形网格。[2]
. 通过网格的坐标信息,为像素单元格着色。[3]
. 通过手势交互,在网格中编辑像素点。
大家可以在 [码上掘金] 上体验,由 Flutter 构建的 web 版:
1. 绘制网格
首先,准备一下绘制面板的配置信息,通过 PixEditorConfig
类承载数据。目前可以配置行数、列数,绘制名称、颜色等。下面是 5*5
网格 和 8*8
网格的绘制效果:
5*5 网格 | 8*8 网格 |
---|---|
class PixEditorConfig {
final int row; // 行
final int column; // 列
final String name; // 名称
final Color backgroundColor; // 背景色
final Color gridColor; // 网格颜色
final bool showGrid; // 网格颜色
PixEditorConfig( {
required this.row,
required this.showGrid,
required this.column,
required this.backgroundColor,
required this.name,
required this.gridColor,
});
}
网格通过 CustomPainter
来自定义绘制,如下所示 PixEditPainter 中持有绘制的配置信息,在 paint
方法中根据配置信息通过 Canvas 进行绘制。其中网格的绘制逻辑封装为 drawGrid
方法,可以通过 config.showGrid
配置数据,决定是否绘制网格:
class PixEditPainter extends CustomPainter {
final PixEditorConfig config;
PixEditPainter({required this.config});
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect( Offset.zero & size, Paint()..color = config.backgroundColor);
if(config.showGrid){
drawGrid(canvas, size);
}
}
@override
bool shouldRepaint(covariant PixEditPainter oldDelegate) {
return oldDelegate.config!=config;
}
}
drawGrid 中根据行列数计算出每格的宽高,再通过移动和添加直线的方式操作路径。最后通过绘制 path 来展示网格。
void drawGrid(Canvas canvas, Size size) {
Paint girdPaint = Paint()..style = PaintingStyle.stroke..color = config.gridColor;
Path path = Path();
double stepH = size.height / config.row;
for (int i = 0; i <= config.row; i++) {
path.moveTo(0, stepH * i);
path.relativeLineTo(size.width, 0);
}
double stepW = size.height / config.column;
for (int i = 0; i <= config.column; i++) {
path.moveTo(stepW * i, 0);
path.relativeLineTo(0, size.height);
}
canvas.drawPath(path, girdPaint);
}
2.根据坐标绘制像素
界面中网格的每格都有其对应的坐标,比如下面 5*5 网格
中坐标信息如下。我们希望做的就是通过坐标和颜色数据,为方格进行着色。下将对 (1,1)
坐标的网格着为蓝色:
这里将每个像素着色数据视为 PixCell
,包含颜色和坐标两个数据:
class PixCell {
final Color color;
final (int x, int y) position;
PixCell({
required this.color,
required this.position,
});
}
在绘制时过程中,需要依赖 PixCell 列表数据。所以画板 PixEditPainter 中增加 List<PixCell>
列表成员:
class PixEditPainter extends CustomPainter {
final PixEditorConfig config;
final List<PixCell> pixCells;
PixEditPainter({
required this.config,
required this.pixCells,
});
@override
bool shouldRepaint(covariant PixEditPainter oldDelegate) {
return oldDelegate.config != config || oldDelegate.pixCells != pixCells;
}
}
然后封装一个 drawPixCells
方法绘制像素点。像素点是一个矩形,通过 PixCell 坐标可以确定矩形,然后使用 canvas.drawRect
绘制即可。
void drawPixCells(Canvas canvas, Size size){
Paint cellPaint = Paint();
double stepH = size.height / config.row;
double stepW = size.height / config.row;
for (int i = 0; i < pixCells.length; i++) {
PixCell cell = pixCells[i];
double top = cell.position.$1 * stepW;
double left = cell.position.$2 * stepH;
Rect rect = Rect.fromLTWH(top , left, stepW, stepH);
canvas.drawRect(rect.deflate(-0.2), cellPaint..color = cell.color);
}
}
此时只要准备好 PixCell 数据列表,传递给画板,就可以为着色。比如下面准备了测试的列表数据:
5*5 网格 | 隐藏网格 |
---|---|
[
PixCell(color: Color(0xff5fc6f5), position: (0, 0)),
PixCell(color: Color(0xff5fc6f5), position: (0 ,1)),
PixCell(color: Color(0xff5fc6f5), position: (0, 2)),
PixCell(color: Color(0xff5fc6f5), position: (0, 3)),
PixCell(color: Color(0xff5fc6f5), position: (0, 4)),
PixCell(color: Color(0xff5fc6f5), position: (1, 0)),
PixCell(color: Color(0xff5fc6f5), position: (1, 2)),
PixCell(color: Color(0xff5fc6f5), position: (3, 2)),
PixCell(color: Color(0xff5fc6f5), position: (4, 3)),
PixCell(color: Color(0xff5fc6f5), position: (2, 3)),
PixCell(color: Color(0xff5fc6f5), position: (2, 1)),
PixCell(color: Color(0xff5fc6f5), position: (4, 1)),
],
3.手势交互维护像素列表数据
最终,我们将通过手势交互来对网格像素进行着色或取消着色。当单元格有像颜色时,点击取消颜色,否则进行着色:
通过 GestureDetector
的 onTapDown
回调,可以监听到按下事件,其中可以得到点击时的触点坐标。我们需要将触点坐标转化为网格坐标,此时需要画板的尺寸,以及配置信息。点击事件由下面的 _handleTapDown
来处理:
- 根据尺寸和和列数计算每格的宽高,然后通过触点计算落点在网格中的坐标。
- 校验
pixCells
中是否存在当前网格坐标。如果由则移除该点,否则添加一个 PixCell。 - 数据变化后,触发更新。
void _handleTapDown(TapDownDetails details, Size size, PixEditorConfig config) {
double stepH = size.height / config.row;
double stepW = size.height / config.row;
int x = details.localPosition.dx ~/ stepW;
int y = details.localPosition.dy ~/ stepH;
bool hasPix = pixCells.where((e) => e.position == (x, y)).isNotEmpty;
if (hasPix) {
pixCells.removeWhere((e) => e.position == (x, y));
} else {
pixCells.add(PixCell(color: const Color(0xff5fc6f5), position: (x, y)));
}
pixCells = List.of(pixCells);
setState(() {});
}
最后通过 LayoutBuilder
在构建过程中得到画板的区域,GestureDetector#onTapDown
回调触发 _handleTapDown
方法。CustomPaint
中使用 PixEditPainter
进行绘制:
到这里,第一版的 Flutter 像素编辑器就完成了,Flutter 的绘制能力可以应用于全平台。所以这个像素编辑器可以同时运行在 Android、iOS、Windows、MacOS、Linux、Web。目前只是一个非常简单的编辑像素功能,后续还会拓展更多的功能。敬请期待 ~
PS: 这不,github 的头像就有了