You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter如何创建可交互像素画?

实现可交互像素画布的两种可行方案

方案一:使用Flutter Canvas(推荐)

Canvas是处理这类需求的最优选择,尤其是针对中大型尺寸图片,性能优势明显。核心逻辑如下:

  1. 加载并解析像素数据:通过ImageProvider加载图片,获取ui.Image实例后,提取RGBA格式的原始像素字节数据。
  2. 绘制放大像素网格:在CustomPainter的paint方法中,按设定的放大倍数逐个绘制像素矩形,精准对应原始图片的每个像素位置。
  3. 处理触摸交互:用GestureDetector监听触摸事件,将屏幕坐标转换为原始图片的像素坐标,修改对应像素颜色后触发重绘。

核心代码示例

class PixelCanvas extends StatefulWidget {
  final String imagePath;
  final int pixelSize; // 单个像素放大后的尺寸,如20px

  const PixelCanvas({super.key, required this.imagePath, required this.pixelSize});

  @override
  State<PixelCanvas> createState() => _PixelCanvasState();
}

class _PixelCanvasState extends State<PixelCanvas> {
  ui.Image? _image;
  Uint8List? _pixelData;
  int? _imgWidth;
  int? _imgHeight;

  @override
  void initState() {
    super.initState();
    _loadImage();
  }

  Future<void> _loadImage() async {
    final imageStream = AssetImage(widget.imagePath).resolve(const ImageConfiguration());
    final completer = Completer<ui.Image>();
    void listener(ImageInfo info, bool syncCall) {
      completer.complete(info.image);
      imageStream.removeListener(listener);
    }
    imageStream.addListener(listener);
    _image = await completer.future;
    _imgWidth = _image!.width;
    _imgHeight = _image!.height;
    
    // 获取RGBA格式的像素字节数据
    final byteData = await _image!.toByteData(format: ui.ImageByteFormat.rawRgba);
    _pixelData = byteData?.buffer.asUint8List();
    setState(() {});
  }

  // 将触摸坐标转换为原始像素坐标
  Offset? _convertToPixelPos(Offset globalPos) {
    if (_imgWidth == null || _imgHeight == null) return null;
    final renderBox = context.findRenderObject() as RenderBox;
    final localPos = renderBox.globalToLocal(globalPos);
    final pixelX = (localPos.dx / widget.pixelSize).floor();
    final pixelY = (localPos.dy / widget.pixelSize).floor();
    if (pixelX >= 0 && pixelX < _imgWidth! && pixelY >=0 && pixelY < _imgHeight!) {
      return Offset(pixelX.toDouble(), pixelY.toDouble());
    }
    return null;
  }

  // 修改指定像素的颜色
  void _updatePixelColor(Offset pixelPos, Color newColor) {
    if (_pixelData == null || _imgWidth == null) return;
    final index = (pixelPos.y * _imgWidth! + pixelPos.x).toInt() * 4;
    if (index + 3 >= _pixelData!.length) return;
    setState(() {
      _pixelData![index] = newColor.red;
      _pixelData![index+1] = newColor.green;
      _pixelData![index+2] = newColor.blue;
      _pixelData![index+3] = newColor.alpha;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_image == null || _pixelData == null) {
      return const Center(child: CircularProgressIndicator());
    }
    return GestureDetector(
      onTapDown: (details) {
        final pixelPos = _convertToPixelPos(details.globalPosition);
        if (pixelPos != null) {
          // 示例:点击后将像素改为红色
          _updatePixelColor(pixelPos, Colors.red);
        }
      },
      child: CustomPaint(
        size: Size(
          _imgWidth! * widget.pixelSize.toDouble(),
          _imgHeight! * widget.pixelSize.toDouble(),
        ),
        painter: _PixelPainter(
          pixelData: _pixelData!,
          imgWidth: _imgWidth!,
          imgHeight: _imgHeight!,
          pixelSize: widget.pixelSize,
        ),
      ),
    );
  }
}

class _PixelPainter extends CustomPainter {
  final Uint8List pixelData;
  final int imgWidth;
  final int imgHeight;
  final int pixelSize;
  final Paint _paint = Paint();

  _PixelPainter({
    required this.pixelData,
    required this.imgWidth,
    required this.imgHeight,
    required this.pixelSize,
  });

  @override
  void paint(Canvas canvas, Size size) {
    for (int y = 0; y < imgHeight; y++) {
      for (int x = 0; x < imgWidth; x++) {
        final index = (y * imgWidth + x) * 4;
        final color = Color.fromARGB(
          pixelData[index+3],
          pixelData[index],
          pixelData[index+1],
          pixelData[index+2],
        );
        _paint.color = color;
        canvas.drawRect(
          Rect.fromLTWH(
            x * pixelSize.toDouble(),
            y * pixelSize.toDouble(),
            pixelSize.toDouble(),
            pixelSize.toDouble(),
          ),
          _paint,
        );
      }
    }
  }

  @override
  bool shouldRepaint(_PixelPainter oldDelegate) {
    return pixelData != oldDelegate.pixelData;
  }
}

方案优势

  • 性能优异,不会因大量组件导致布局卡顿,适合处理高分辨率图片
  • 直接操作原始像素数据,交互逻辑精准可控
  • 支持扩展复杂绘制效果(如像素边框、渐变填充等)

方案二:使用Container组件(仅适合小尺寸图片)

如果图片尺寸极小(如12×12、20×20),可以用Container快速实现,核心逻辑:

  1. 解析图片每个像素的颜色值,生成二维颜色数组
  2. 通过GridView渲染每个像素对应的Container
  3. 给每个Container绑定点击事件,修改对应像素颜色

核心代码示例

class PixelGrid extends StatefulWidget {
  final String imagePath;
  final int pixelSize;

  const PixelGrid({super.key, required this.imagePath, required this.pixelSize});

  @override
  State<PixelGrid> createState() => _PixelGridState();
}

class _PixelGridState extends State<PixelGrid> {
  List<List<Color>>? _pixelColors;
  int? _imgWidth;
  int? _imgHeight;

  @override
  void initState() {
    super.initState();
    _loadPixelColors();
  }

  Future<void> _loadPixelColors() async {
    final imageStream = AssetImage(widget.imagePath).resolve(const ImageConfiguration());
    final completer = Completer<ui.Image>();
    void listener(ImageInfo info, bool syncCall) {
      completer.complete(info.image);
      imageStream.removeListener(listener);
    }
    imageStream.addListener(listener);
    final image = await completer.future;
    _imgWidth = image.width;
    _imgHeight = image.height;
    
    final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
    final pixelData = byteData?.buffer.asUint8List();
    if (pixelData == null) return;

    _pixelColors = List.generate(_imgHeight!, (y) {
      return List.generate(_imgWidth!, (x) {
        final index = (y * _imgWidth! + x) * 4;
        return Color.fromARGB(
          pixelData[index+3],
          pixelData[index],
          pixelData[index+1],
          pixelData[index+2],
        );
      });
    });
    setState(() {});
  }

  void _updatePixelColor(int x, int y, Color newColor) {
    if (_pixelColors == null) return;
    setState(() {
      _pixelColors![y][x] = newColor;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_pixelColors == null) {
      return const Center(child: CircularProgressIndicator());
    }
    return GridView.builder(
      physics: const NeverScrollableScrollPhysics(),
      shrinkWrap: true,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: _imgWidth!,
        childAspectRatio: 1,
      ),
      itemCount: _imgWidth! * _imgHeight!,
      itemBuilder: (context, index) {
        final x = index % _imgWidth!;
        final y = index ~/ _imgWidth!;
        return GestureDetector(
          onTap: () {
            _updatePixelColor(x, y, Colors.red);
          },
          child: Container(
            width: widget.pixelSize.toDouble(),
            height: widget.pixelSize.toDouble(),
            color: _pixelColors![y][x],
            // 可选:添加像素边框增强视觉效果
            decoration: BoxDecoration(
              border: Border.all(color: Colors.black.withOpacity(0.1), width: 0.5),
            ),
          ),
        );
      },
    );
  }
}

方案优缺点

  • 优点:实现简单,无需自定义Painter,新手易上手
  • 缺点:图片尺寸超过几十×几十时,会生成大量Container组件,导致布局和渲染卡顿,性能极差

总结建议

  • 小尺寸图片(如12×12)可选择Container方案快速实现
  • 中大型图片必须使用Canvas方案保证性能
  • 无论哪种方案,都要通过ui.Image获取原始像素数据,不要用文件大小推算像素数量——图片编码格式不同,文件大小和像素数没有直接对应关系

内容的提问来源于stack exchange,提问作者Coder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 06:24:47