Flutter 像素编辑器#01 | 像素网格

3,099 阅读4分钟

本系列,将通过 Flutter 实现一个全平台的像素编辑器应用。源码见开源项目 【pix_editor】

本篇将完成如下功能:

  • [1]. 展示方形网格。
  • [2]. 通过网格的坐标信息,为像素单元格着色。
  • [3]. 通过手势交互,在网格中编辑像素点。

95.gif

大家可以在 [码上掘金] 上体验,由 Flutter 构建的 web 版:


1. 绘制网格

首先,准备一下绘制面板的配置信息,通过 PixEditorConfig 类承载数据。目前可以配置行数、列数,绘制名称、颜色等。下面是 5*5 网格 和 8*8 网格的绘制效果:

5*5 网格8*8网格
image.pngimage.png
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) 坐标的网格着为蓝色:

image.png

这里将每个像素着色数据视为 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 网格隐藏网格
image.pngimage.png
[
  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.手势交互维护像素列表数据

最终,我们将通过手势交互来对网格像素进行着色或取消着色。当单元格有像颜色时,点击取消颜色,否则进行着色:

95.gif

通过 GestureDetectoronTapDown 回调,可以监听到按下事件,其中可以得到点击时的触点坐标。我们需要将触点坐标转化为网格坐标,此时需要画板的尺寸,以及配置信息。点击事件由下面的 _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 进行绘制:

image.png


到这里,第一版的 Flutter 像素编辑器就完成了,Flutter 的绘制能力可以应用于全平台。所以这个像素编辑器可以同时运行在 Android、iOS、Windows、MacOS、Linux、Web。目前只是一个非常简单的编辑像素功能,后续还会拓展更多的功能。敬请期待 ~

PS: 这不,github 的头像就有了

image.png