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

Flutter中如何在CustomPainter Canvas分层绘制SVG

Flutter中Canvas分层渲染SVG并实现图层交互

核心思路

将SVG中的每个<g>标签作为独立图层,解析提取每个图层的SVG片段与初始变换参数,通过CustomPainter在Canvas上分层绘制,结合手势监听实现图层选中、拖拽、缩放操作。

步骤实现

1. 解析SVG提取图层数据

使用xml包解析SVG字符串,提取所有<g>元素,为每个图层创建数据模型存储状态(位置、缩放比例、原始SVG内容等)。

首先添加依赖:

dependencies:
  xml: ^6.5.0
  flutter_svg: ^2.0.9

定义图层模型:

class SvgLayer {
  final String id; // 唯一标识(原SVG的id重复,用索引生成)
  final String svgContent; // 单个图层的独立SVG片段
  Offset position; // 拖拽偏移量
  double scale; // 缩放比例
  final Matrix4 initialTransform; // 图层原始变换矩阵

  SvgLayer({
    required this.id,
    required this.svgContent,
    required this.initialTransform,
    Offset? position,
    double? scale,
  })  : position = position ?? Offset.zero,
        scale = scale ?? 1.0;
}

解析SVG的工具方法:

List<SvgLayer> parseSvgLayers(String svgString) {
  final document = XmlDocument.parse(svgString);
  final svgElement = document.findElements('svg').first;
  final gElements = svgElement.findElements('g').toList();

  return gElements.asMap().entries.map((entry) {
    final index = entry.key;
    final gElement = entry.value;
    
    // 解析transform属性为Matrix4
    final transformAttr = gElement.getAttribute('transform');
    Matrix4 initialTransform = Matrix4.identity();
    if (transformAttr != null && transformAttr.startsWith('matrix(')) {
      final values = transformAttr
          .replaceAll('matrix(', '')
          .replaceAll(')', '')
          .split(',')
          .map((e) => double.parse(e.trim()))
          .toList();
      if (values.length == 6) {
        initialTransform = Matrix4.fromList([
          values[0], values[1], 0, 0,
          values[2], values[3], 0, 0,
          0, 0, 1, 0,
          values[4], values[5], 0, 1,
        ]);
      }
    }
    
    // 生成单个图层的独立SVG片段
    final svgContent = '''
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="${svgElement.getAttribute('viewBox')}">
  ${gElement.innerXml}
</svg>
''';
    return SvgLayer(
      id: 'layer_$index',
      svgContent: svgContent,
      initialTransform: initialTransform,
    );
  }).toList();
}

2. 自定义Painter分层渲染

使用flutter_svg将每个图层的SVG转为Picture,结合图层的位置、缩放参数在Canvas上分层绘制:

class LayeredSvgPainter extends CustomPainter {
  final List<SvgLayer> layers;
  final String? selectedLayerId;

  LayeredSvgPainter({required this.layers, this.selectedLayerId});

  @override
  void paint(Canvas canvas, Size size) {
    for (final layer in layers) {
      final pictureProvider = PictureProvider.string(layer.svgContent);
      pictureProvider.resolve(ImageConfiguration()).addListener(
        ImageStreamListener((info, _) {
          final picture = info.picture;
          if (picture != null) {
            canvas.save();
            // 应用原始变换
            canvas.transform(layer.initialTransform.storage);
            // 应用拖拽与缩放
            canvas.translate(layer.position.dx, layer.position.dy);
            canvas.scale(layer.scale);
            // 绘制SVG内容
            canvas.drawPicture(picture);
            // 选中图层绘制蓝色边框标识
            if (layer.id == selectedLayerId) {
              final rect = picture.bounds;
              final paint = Paint()
                ..color = Colors.blue
                ..style = PaintingStyle.stroke
                ..strokeWidth = 2;
              canvas.drawRect(rect, paint);
            }
            canvas.restore();
          }
        }),
      );
    }
  }

  @override
  bool shouldRepaint(covariant LayeredSvgPainter oldDelegate) {
    return layers != oldDelegate.layers || selectedLayerId != oldDelegate.selectedLayerId;
  }
}

3. 实现手势交互

用GestureDetector监听触摸事件,处理图层选中、拖拽、缩放逻辑:

class LayeredSvgEditor extends StatefulWidget {
  final String svgString;

  const LayeredSvgEditor({super.key, required this.svgString});

  @override
  State<LayeredSvgEditor> createState() => _LayeredSvgEditorState();
}

class _LayeredSvgEditorState extends State<LayeredSvgEditor> {
  late List<SvgLayer> _layers;
  String? _selectedLayerId;
  Offset _dragStart = Offset.zero;
  double _initialScale = 1.0;

  @override
  void initState() {
    super.initState();
    _layers = parseSvgLayers(widget.svgString);
  }

  // 触摸点命中检测
  String? _hitTestLayer(Offset localPosition) {
    for (final layer in _layers.reversed) {
      final rect = Rect.fromLTWH(
        layer.initialTransform.getTranslation().x + layer.position.dx,
        layer.initialTransform.getTranslation().y + layer.position.dy,
        300 * layer.scale,
        300 * layer.scale,
      );
      if (rect.contains(localPosition)) {
        return layer.id;
      }
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      // 点击选中图层
      onTapUp: (details) {
        final hitLayerId = _hitTestLayer(details.localPosition);
        setState(() => _selectedLayerId = hitLayerId);
      },
      // 拖拽图层
      onPanStart: (details) {
        if (_selectedLayerId != null) _dragStart = details.localPosition;
      },
      onPanUpdate: (details) {
        if (_selectedLayerId != null) {
          final delta = details.localPosition - _dragStart;
          setState(() {
            final layer = _layers.firstWhere((l) => l.id == _selectedLayerId);
            layer.position += delta;
            _dragStart = details.localPosition;
          });
        }
      },
      // 缩放图层
      onScaleStart: (details) {
        if (_selectedLayerId != null) {
          _initialScale = _layers.firstWhere((l) => l.id == _selectedLayerId).scale;
        }
      },
      onScaleUpdate: (details) {
        if (_selectedLayerId != null) {
          setState(() {
            final layer = _layers.firstWhere((l) => l.id == _selectedLayerId);
            layer.scale = _initialScale * details.scale;
          });
        }
      },
      child: CustomPaint(
        painter: LayeredSvgPainter(
          layers: _layers,
          selectedLayerId: _selectedLayerId,
        ),
        size: const Size(300, 300),
      ),
    );
  }
}

使用示例

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const svgString = '''
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="300" height="300" viewBox="312.07142857142856 203.98000000000002 233.02999999999997 122.98027586206888" xml:space="preserve"><defs></defs>
<desc>Create with Free Logo Design v1.0</desc>
<defs>
</defs>
<defs></defs><rect x="0" y="0" width="100%" height="100%" fill="rgba(0, 0, 0, 0)"></rect>
<g transform="matrix(1 0 0 1 469.77 247.73)" id="Layer_1">
<polygon style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(29,29,27); fill-rule: nonzero; opacity: 1;" points="-50,-43.21 36.43,43.21 50,43.21 -36.41,-43.21 -50,-43.21 "></polygon>
</g>
<g transform="matrix(1 0 0 1 494.6 247.73)" id="Layer_1">
<polygon style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(29,29,27); fill-rule: nonzero; opacity: 1;" points="-50,-43.21 36.43,43.21 50,43.21 -36.42,-43.21 -50,-43.21 "></polygon>
</g>
<g transform="matrix(1 0 0 1 361.98 237.81)" id="Layer_1">
<polygon style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(29,29,27); fill-rule: nonzero; opacity: 1;" points="7.97,-18.1 7.97,14 3.47,18.09 -7.97,18.09 -7.97,-18.1 7.97,-18.1 "></polygon>
</g>
<g transform="matrix(1 0 0 1 403.87 247.81)" id="Layer_1">
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(29,29,27); fill-rule: nonzero; opacity: 1;" transform=" translate(-115.3, -127.06)" d="M 119.05 83.73 L 81.37 118.06 L 65.44 132.59 L 24 170.38 L 174.09 170.38 L 206.59 170.17 Z M 174.05 160.73000000000002 L 48.76 160.73000000000002 L 76.87 135.11 L 81.37 131.01000000000002 L 118.78 97 L 183.35 160.76 Z" stroke-linecap="round"></path>
</g>
<g transform="matrix(1 0 0 1 405.2 259.14)" id="Layer_1">
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(227,6,19); fill-rule: nonzero; opacity: 1;" transform=" translate(-116.63, -138.39)" d="M 103.13 124.89 L 103.13 151.89 L 130.13 151.89 L 130.13 124.88999999999999 Z M 120.5 142.26 L 112.73 142.26 L 112.73 134.48999999999998 L 120.5 134.48999999999998 Z" stroke-linecap="round"></path>
</g>
<g transform="matrix(1 0 0 1 371.45 309.98)" style="" id="Layer_1">
        <text xml:space="preserve" font-family="Roboto-Bold, Roboto" font-size="29.16" font-style="normal" font-weight="700" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(227,6,19); fill-rule: nonzero; opacity: 1; white-space: pre;"><tspan x="-56.7" y="9.16">company</tspan></text>
</g>
</svg>
''';
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: LayeredSvgEditor(svgString: svgString),
        ),
      ),
    );
  }
}

注意事项

  • 原SVG中所有<g>的id均为Layer_1,解析时通过索引生成了唯一id用于图层区分
  • 命中检测的边界计算做了简化,实际场景可缓存每个SVG Picture的精确边界,结合变换矩阵计算更准确的可点击区域
  • 若需调整图层层级,可在SvgLayer中添加zIndex字段,渲染时按zIndex排序绘制

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 15:57:26