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
相关产品推荐
相关产品推荐

