聊个Android开发里很常见、但每次做都让人犹豫一会儿的需求手头drawable目录下有一堆Vector Asset的XML矢量图怎么把它们转成SVG格式。这东西本身不复杂难点在于大部分人只知道Android Studio支持SVG转Vector却不知道反过来应该怎么做或者试了几个在线工具发现转出来根本不是想要的效果。先说清楚这个内容解决了什么问题。Vector Drawable是Android专用的矢量图格式XML结构走的是res/drawable目录SVG则是通用的Web矢量图标准。当你需要在网页上展示同一套图标、把资源交付给其他非Android端的设计稿、或者想用跨平台绘图工具继续加工时就必须把.xml转成.svg。这篇内容适合所有Android开发者、兼顾一点前端协作需求的人读我从原理讲到实操脚本再把坑给你列出来照着做基本就能跑通。1. 为什么需要把Vector Drawable转成SVG1.1 两种格式的关系与差异很多人以为Vector Drawable就是Android版的SVG从语法血缘上确实如此。Android的Vector Drawable借鉴了SVG的Path语法pathData里的M、L、C、A这些指令和SVG的d属性完全同源所以理论上转换是可行的这也是后面所有方案成立的基础。但两者在XML结构上是有出入的。Vector Drawable用的是vector根标签SVG用的是svg根标签Vector的长度单位是dpviewport用viewportWidth/viewportHeightSVG则用普通数值和viewBox0 0 24 24Path的填充色叫fillColorSVG里叫fill描边属性也是类似strokeColor对应stroke。甚至颜色值格式都不同Android的十六进制颜色是#AARRGGBBAlpha通道在最前面而SVG/CSS标准里是#RRGGBBAAAlpha在后面。这些差异单个看都不大但合在一起就导致了你不能简单地改个后缀名当SVG用必须做属性层面的翻译。1.2 真实场景驱使我见过最典型的需求来自一个做跨平台系统的团队。设计师在Android Studio里画好了一套Material风格图标导出成Vector XML后扔给了开发但后来Web端也要用同一套图标设计那边又不想重新画一遍。这时候如果没有转换工具就只能让设计师用设计工具逐个人肉描一遍既浪费时间又难以保证视觉一致。还有一种情况是图表库。很多代码绘图库支持SVGPath但不支持Android的Vector格式你想把一个icon变成动画路径就得先把它转成标准的SVG路径数据。再比如你要把图标贴到文档、PPT、演示稿里SVG自然比drawable里的XML好用得多。所以这个需求的本质是Android生态有一套自己的矢量描述但生态之外的主流标准是SVG中间差一个桥。1.3 转换为什么不是一键搞定既然Path语法都同源为什么不做个一键转换工具因为Android在Vector Drawable里加了几个SVG没有的标签和属性。比如group标签用于分组变换里面有rotation、scaleX、scaleY、translateX、translateY、pivotX、pivotY这些控件属性SVG的g标签虽然也能做变换但语法是transformrotate(...) scale(...) translate(...)而且旋转默认绕坐标原点跟Android里默认绕pivot点旋转不一致。再比如android:trimPathStart、android:trimPathEnd、android:trimPathOffsetAndroid用来做路径描边动画的SVG里没有直接的等价物。还有android:autoMirrored、android:tint这些SVG的机制也跟它对不上。所以转换的重点就落在了两点一是标签和属性的映射二是group变换语义的翻译。搞懂这两点你就看穿了这个问题的本质之后不管用什么工具心里都有数。2. 三条路线选哪个取决于你的场景2.1 手动改XML属性适合单文件应急如果只是临时要一个图标最快的办法是手动把XML的属性名一个一个改掉。大概步骤就是把vector开标签改成svg加上SVG的命名空间xmlns然后把android:width改成widthviewportWidth合并到viewBox属性里Path里的pathData改成dfillColor改成fill。文件里如果只有一个path不涉及group变换这样改完基本就能用了。这个方案最大的问题是容易漏。颜色值如果带Alpha位就涉及#AARRGGBB转#RRGGBBAA的问题一个图标两三个Path还顶得住一旦源文件有几十个Path手动改一定会出乱子。所以它只适合应急不适合作为固定工作流。2.2 脚本批量处理适合图标库迁移当你手里有上百个drawable XML要转的时候手动方案就彻底失效了。这时候必须写脚本用程序去解析XML、遍历节点、翻译属性。脚本方案的好处除了批量还在于可复现——源文件更新后重新跑一遍脚本就行不至于每次都不一样。我用Python写过一个通用的转换脚本后面会贴完整代码。这个方案对有编程基础的人来说是最可控的把转换逻辑握在自己手里遇到特殊情况可以随时打补丁。2.3 在线工具与IDE内置功能适合快速验证如果不想写代码还有两条路可以走一是用在线转换网站把XML粘贴进去点一下生成SVG二是用新版Android Studio的预览面板打开Vector XML文件后预览区有一个复制为SVG的按钮。这两者的共同特点是快但都有明显局限。在线工具普遍存在两个问题一是对group标签的处理很粗暴很多直接把pivot点忽略了导致旋转后的位置偏掉二是对新版本Android系统添加的fillType等属性支持不全遇到evenOdd填充规则会转错。Android Studio内置的复制SVG功能我在新版里用过简单图标没问题但只要涉及group嵌套、多个Path叠加导出的SVG也偶尔出现样式异常。所以我的建议是快速验证用这些工具正式迁移用脚本。3. 手写一个转换脚本核心实现与参数详解3.1 属性映射清单脚本的核心是一张映射表。根节点上需要处理的属性有这些Vector DrawableSVG说明android:width24dpwidth24去掉dp后缀android:height24dpheight24同上android:viewportWidthviewBox与viewportHeight合并android:viewportHeightviewBox格式为0 0 vw vhPath节点属性Vector DrawableSVG说明android:pathDatad路径数据基本原样拷贝android:fillColorfill注意Alpha位顺序android:strokeColorstroke同上android:strokeWidthstroke-width数值原样拷贝android:fillAlphafill-opacity数值一般0-1android:strokeAlphastroke-opacity同上android:strokeLineCapstroke-linecap值butt/round/square一致android:strokeLineJoinstroke-linejoin值miter/round/bevel一致android:fillTypefill-ruleevenOdd转evenoddnonZero转nonzero特别注意颜色格式。Android里的#FF000000是不透明的黑色但在SVG里你写fill#FF000000就完蛋了SVG解析器会当成RGB来读你会得到一个偏蓝或偏紫的颜色。正确做法是把#AARRGGBB拆开Alpha放到末尾拼成#RRGGBBAA。这个细节是我一开始最容易忽略的普通转换工具没处理好就会造成图标颜色整体漂移。3.2 group标签的矩阵变换处理这个部分是最容易出错的地方。Vector Drawable的group支持这样的写法group android:rotation15 android:pivotX12 android:pivotY12 android:translateX4 android:translateY5 android:scaleX1.5 android:scaleY0.8 path ... / /group语义是先平移到pivotX/pivotY再旋转rotation度再缩放scaleX/scaleY最后再平移到translateX/translateY。用矩阵表示大概是T translate(translateX pivotX, translateY pivotY) * rotate(rotation) * scale(scaleX, scaleY) * translate(-pivotX, -pivotY)注意这里的顺序不能乱因为矩阵乘法不满足交换律。对应到SVG的transform属性可以这么组合transformtranslate(txpivotX, typivotY) rotate(rotation) scale(scaleX, scaleY) translate(-pivotX, -pivotY)如果你的Vector文件里group还嵌套了groupSVG的g标签也支持嵌套天然可以保留这种层级关系。脚本里我建议直接保留嵌套结构不要急着把矩阵合并成一个这样最后生成的SVG人眼也容易读后续要改也好改。如果想进一步精简可以把嵌套的所有变换合并成一个6参数矩阵matrix(a b c d e f)但可读性会差很多。从数学上讲直接叠加矩阵会生成一个完全等价的单矩阵很多人会为了“干净”选择这么做。我的态度是能不合并就不合并保留嵌套的g标签让SVG的结构跟原始XML保持一一对应出问题好排查。3.3 完整转换脚本解读下面是我在生产里用过的一套脚本支持递归处理嵌套group支持颜色格式转换支持批量目录处理。Python3直接跑就行不需要第三方依赖就用标准库xml.etree.ElementTree。#!/usr/bin/env python3 # -*- coding: utf-8 -*- Vector Drawable XML 转 SVG 脚本 用法: python vector2svg.py input.xml output.svg python vector2svg.py drawable_dir output_dir # 批处理目录 import os import re import sys import xml.etree.ElementTree as ET SVG_NAMESPACE http://www.w3.org/2000/svg # color: 把 #AARRGGBB 转成 #RRGGBBAA # 如果是 #RRGGBB 则保持原样; 如果是 color/xxx 或 ?attr/xxx 则原样输出并打印警告 def convert_color(value): if value is None: return None value value.strip() # 资源引用无法解析原样保留 if value.startswith() or value.startswith(?): print(f[WARN] 无法解析资源引用: {value}, filesys.stderr) return value m re.fullmatch(r#([0-9a-fA-F]{8}), value) if m: hex_str m.group(1) a hex_str[0:2] rgb hex_str[2:8] return # rgb a return value def convert_viewbox(root): vw root.get(android:viewportWidth, 24) vh root.get(android:viewportHeight, 24) return f0 0 {vw} {vh} def convert_group(group_elem, out_parent): 递归处理 group 和 path g ET.SubElement(out_parent, g) transforms [] tx float(group_elem.get(android:translateX, 0)) ty float(group_elem.get(android:translateY, 0)) pivot_x float(group_elem.get(android:pivotX, 0)) pivot_y float(group_elem.get(android:pivotY, 0)) rotation float(group_elem.get(android:rotation, 0)) scale_x float(group_elem.get(android:scaleX, 1)) scale_y float(group_elem.get(android:scaleY, 1)) # 与 Android 的变换语义保持一致 if tx ! 0 or ty ! 0 or pivot_x ! 0 or pivot_y ! 0: transforms.append(ftranslate({tx pivot_x} {ty pivot_y})) if rotation ! 0: transforms.append(frotate({rotation})) if scale_x ! 1 or scale_y ! 1: transforms.append(fscale({scale_x} {scale_y})) if pivot_x ! 0 or pivot_y ! 0: transforms.append(ftranslate({-pivot_x} {-pivot_y})) if transforms: g.set(transform, .join(transforms)) for child in group_elem: if child.tag.endswith(group): convert_group(child, g) elif child.tag.endswith(path): path ET.SubElement(g, path) for attr, svg_attr in PATH_ATTR_MAP.items(): val child.get(attr) if val is None: continue if svg_attr in (fill, stroke): val convert_color(val) path.set(svg_attr, val) def convert_file(xml_path, svg_path): tree ET.parse(xml_path) root tree.getroot() if not root.tag.endswith(vector): print(f[SKIP] {xml_path} 不是 Vector Drawable, filesys.stderr) return False svg_root ET.Element(svg) svg_root.set(xmlns, SVG_NAMESPACE) # 宽高: 去掉 dp / dip 等单位 w root.get(android:width, 24dp) h root.get(android:height, 24dp) w re.sub(r(dp|dip|px)$, , w.strip()) h re.sub(r(dp|dip|px)$, , h.strip()) svg_root.set(width, w) svg_root.set(height, h) svg_root.set(viewBox, convert_viewbox(root)) for child in root: if child.tag.endswith(group): convert_group(child, svg_root) elif child.tag.endswith(path): path ET.SubElement(svg_root, path) for attr, svg_attr in PATH_ATTR_MAP.items(): val child.get(attr) if val is None: continue if svg_attr in (fill, stroke): val convert_color(val) path.set(svg_attr, val) tree_out ET.ElementTree(svg_root) ET.indent(tree_out, space ) tree_out.write(svg_path, encodingutf-8, xml_declarationTrue) print(f[OK] {xml_path} - {svg_path}) return True PATH_ATTR_MAP { android:pathData: d, android:fillColor: fill, android:strokeColor: stroke, android:strokeWidth: stroke-width, android:fillAlpha: fill-opacity, android:strokeAlpha: stroke-opacity, android:strokeLineCap: stroke-linecap, android:strokeLineJoin: stroke-linejoin, android:strokeMiterLimit: stroke-miterlimit, android:fillType: fill-rule, } def main(): if len(sys.argv) 3: print(用法: python vector2svg.py input.xml|input_dir output.svg|output_dir) sys.exit(1) src sys.argv[1] dst sys.argv[2] if os.path.isdir(src): os.makedirs(dst, exist_okTrue) for fname in sorted(os.listdir(src)): if not fname.endswith(.xml): continue xml_path os.path.join(src, fname) svg_name os.path.splitext(fname)[0] .svg svg_path os.path.join(dst, svg_name) convert_file(xml_path, svg_path) else: convert_file(src, dst) if __name__ __main__: main()脚本核心逻辑分三块第一块是属性翻译把android:开头的属性名换成SVG的标准名第二块是颜色修正把Android的Alpha前置格式换成SVG的后置格式第三块是递归遍历group和path都按原始层级输出到SVG的g和path。ET.indent是Python 3.9以后才有的方法如果你的Python版本较老可以手动换行缩进或者干脆不缩进直接输出一行SVG浏览器照样能解析。3.4 运行结果验证用脚本跑一个简单的Vector文件输入是Material Design里常见的圆形填充加内圆图标vector xmlns:androidhttp://schemas.android.com/apk/res/android android:width24dp android:height24dp android:viewportWidth24.0 android:viewportHeight24.0 path android:fillColor#FF000000 android:pathDataM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z/ /vector转换之后是svg xmlnshttp://www.w3.org/2000/svg width24 height24 viewBox0 0 24.0 24.0 path dM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z fill#000000FF/ /svg注意这里的fill从#FF000000变成了#000000FF这就是颜色Alpha位置修正的效果。SVG里这段代码可以正常渲染为一个黑色圆形加内圆孔。如果你在浏览器里打开发现整个图标颜色变了第一反应应该就是查颜色值有没有做Alpha位转换。这是转换正确性最隐蔽、也最常见的坎。4. 实战中的坑与排查技巧4.1 特有属性的取舍前面提到android:trimPathStart、android:trimPathEnd、android:trimPathOffset这三个属性是做描边动画用的。在Vector里它们的作用是控制Path的可见部分相当于只绘制路径的一部分。SVG没有直接等价物硬要转的话可以用stroke-dasharray配合stroke-dashoffset逼近但语义不完全一致而且要对路径长度做估算非常麻烦。我的建议是分两种情况处理如果这个属性在你项目里只是静态使用值是非默认值比如0到1之间那你得评估一下目标SVG的使用场景一般用CSS动画的stroke-dashoffset去模拟是可以的如果它本来就是配合ValueAnimator做动画用的直接在转换时忽略SVG端单独开发动画逻辑。不要为了追求“转换后跟原文件完全一样”而卡在这里矢量图的动画语义本身就带有平台特性接受转换后需要微调才是务实态度。还有一个属性是android:fillType。这个属性在部分老Android版本上不支持但在新版本和很多设计工具里很常见。它控制填充规则nonZero是默认的非零环绕规则evenOdd是奇偶规则。转换时我通常是值为nonZero时可以不输出fill-rule因为SVG默认就是nonzero。值为evenOdd时必须输出fill-ruleevenodd否则图案的镂空区域会错误填充。4.2 编码、路径与中文注释处理文件编码时踩过一个不怎么起眼但很坑的问题ET.parse在解析XML时如果XML文件带utf-8的BOM头或者Windows环境下文件编码不是UTF-8会抛出不可读的解析错误。我建议在所有脚本入口处统一加一句with open(xml_path, r, encodingutf-8-sig) as f: tree ET.parse(f)如果XML里有中文注释但文件声明的是ISO-8859-1或没有声明编码解析也是必炸的。统一用utf-8-sig打开不管是BOM还是无BOM都能兼容。另外注意SVG的Path数据中坐标符号的解析。Android的pathData允许在M指令之后省略空格直接写M12.5-3.2这种形式解析器要求能识别数字与符号边界。我们用脚本转换时相当于把pathData原样拷贝到d属性这个倒不会出错问题出在如果你在这个过程中引入了解析库去做路径数据清洗反而可能把合法的负号边界弄坏。所以路径数据我强烈建议原样拷贝不做任何格式化。4.3 批量转换时的高频问题批量处理drawable目录时最常见的报错是某个XML文件根标签不叫vector。比如目录里混入了selector文件、layer-list甚至普通的shape文件。脚本里我已经做了跳过处理但生产环境中更稳妥的做法是先把目录里所有XML文件统计一遍确认哪些是Vector哪些不是这样不会漏转。还有一类问题是命名冲突。drawable里的文件名全是小写加下划线比如ic_arrow_back.xml转成ic_arrow_back.svg没问题但如果同一个目录下有同名文件脚本的覆盖行为可能需要你预先确认。我习惯在批量转换前把输出目录清空避免混入旧文件造成误判。5. 备选方案的实操细节5.1 Android Studio预览窗口的用法如果你手边只有一个图标要转而且你的Android Studio版本比较新可以这样操作打开res/drawable下的Vector XML文件编辑器底部的Preview窗口会自动渲染出矢量图形窗口右上角有一个类似“Copy SVG”的按钮点击后SVG文本就进剪贴板了新建一个.svg文件粘贴保存即可。这个方法转换出来的SVG在简单场景下效果很好因为它是IDE内部利用同一套解析机制生成的能够正确处理大部分group变换。但它有几个限制一是不会处理color资源引用因为IDE根本没有解析资源上下文颜色直接表现为默认黑色二是不能批量处理所以只适合单文件应急文件多的时候效率远不如脚本。5.2 在线工具的试用经验在线转换站我也试过很多个整体感受是“能用但不敢全信”。网页版工具的接入方式通常是粘贴XML、导出SVG生成速度确实快但验证过几个带group旋转的图标导出之后图形位置明显跑偏。原因就是前面提到的pivot变换语义没有处理正确它只是简单地做了rotate的前缀处理丢失了translate(pivot)与translate(-pivot)的补偿操作。还有一个通病是对资源引用的处理。如果你的Vector里写的是android:fillColorcolor/ic_launcher_background在线工具百分之百处理不了因为它没有你项目的res/values配置。最后要么变成黑色要么变成一条报错信息。所以在线工具适合验证语法、不适合作为正式的批量生产力工具这话我在团队里反复说过几次。6. 我的实操体会最后分享一个个人经验。这套转换流程我最早是给某个跨平台系统的图标库做的一开始也是拿在线工具凑合后来实在受不了它转完的图标位置偏移才下决心写脚本。写完之后才发现脚本本身并不复杂真正花时间的反而是处理颜色顺序、pivot变换语义这些边界情况。但从那以后我们再也没为“Vector转SVG”这件事头疼过源文件更新了就重新跑一遍脚本几分钟出全部结果而且转出来的SVG能直接进Chrome、进Figma、进各类文档工具。如果只让我给一条建议那就是先搞懂Vector和SVG的属性映射关系比搜一百个转换工具都管用。一旦你理解了颜色要调Alpha位置、group变换要补pivot语义、trimPath系列SVG不支持你就能判断任何转换工具靠不靠谱也能在半小时内自己写一个适合自己项目的转换器。另外一个可以扩展的方向是把脚本反向改一下SVG转Vector也能做思路完全对称只是属性映射反过来。我已经把正向和反向逻辑都固化在了自己常用的一个工具脚本里有需要的时候改个参数就能两边互通整个图标管理流程就很顺了。