SVG 转 Vector Drawable 的注意事项
SVG 转 Vector Drawable 的注意事项
Android 开发里用 Vector Drawable 替代多套 PNG 已经是老生常谈了,但真把设计师给的 SVG 往项目里一丢,编译报错、运行时崩溃、渲染错位的问题从来没少过。这篇文章想聊的不是"为什么要用 Vector",而是"怎么把 SVG 干净利落地转成能用的 Vector Drawable",以及那些工具链里藏着的坑。
先搞清楚 Vector Drawable 不是什么完整 SVG 子集
很多人以为 Vector Drawable 就是 SVG 换个后缀名,这是最大的误解。Android 的 Vector Drawable 格式从 API 21 开始引入,设计目标很明确:只覆盖图标和简单插画场景,绝不是要做一个移动端 SVG 渲染引擎。这意味着大量 SVG 特性直接被砍掉,或者做了语义替换。
最直观的例子是渐变。SVG 的 <linearGradient> 和 <radialGradient> 在 Vector Drawable 里直到 API 24 才支持,而且语法被改成了 <gradient> 标签,属性名也变了。如果你拿着一个带渐变的 SVG 直接用工具转换,很多转换器会默默把渐变扁平化成纯色,或者干脆跳过这部分路径,生成一个看起来完全不对的文件。更隐蔽的是 mask 和 clip-path,Vector Drawable 至今不支持这些特性,复杂遮罩的 SVG 转过来基本要重构。
路径数据本身也有差异。SVG 的 path 指令支持科学计数法(比如 1.23e-4),Vector Drawable 的 android:pathData 在旧版本解析器里会直接抛异常。还有 SVG 里常见的 viewBox 和 preserveAspectRatio 的多种组合,Vector Drawable 只保留了 viewportWidth/viewportHeight 和 android:viewport 的简化映射,某些非均匀缩放场景下行为并不一致。
这些差异决定了"转换"不是单纯的格式翻译,而是一个有损的适配过程。理解这一点之后,再看各种工具的选择和配置,才能知道它们在什么环节做了妥协。
官方工具 SVG2Vector 的真实表现
Android Gradle Plugin 内置的 SVG2Vector 是大多数人最先接触的转换方式。它集成在构建流程里,你把 .svg 文件往 res/drawable/ 或者 res/drawable-anydpi/ 里一放,AGP 自动在编译时生成 .xml。这个路径从 Android Studio 3.0 左右开始稳定,到现在 AGP 8.x 一直在迭代。
它的好处是无缝。不用额外装软件,团队协作时也不会出现"我本地转了文件你那边没更新"的问题。但问题也出在这个"自动"上——它失败了不会阻塞构建,而是生成一个残缺的 Vector Drawable,或者把不支持的元素直接丢弃。你只有运行时看到图标缺了一块,或者看 build warning 才能发现。
AGP 8.1.0 之后,SVG2Vector 的日志稍微友好了一点,会在 build output 里列出被忽略的元素。但处理方式依然是静默降级,不是报错终止。我个人觉得这种设计对 CI 环境很不友好,你很难在自动化流程里检测"这个 SVG 转得干不干净"。
具体的不支持清单可以查官方文档,但有几个高频踩坑点值得单独说。SVG 的 <text> 元素会被完全忽略,如果你的图标里嵌了文字,转出来就是空白。<use> 和 <symbol> 的引用关系不会被展开,很多从设计系统里导出的 SVG 喜欢复用路径定义,转过来直接失效。滤镜效果(<filter>)全部丢弃,阴影、模糊这些在 Material Design 图标里很常见。
一个我实际遇到的 case:设计师从 Figma 导出一个带 drop-shadow 的 SVG,SVG2Vector 编译通过,运行时看图标边缘发虚,以为是缩放问题,调了半天 android:scaleType,最后发现是阴影没了,和背景色混在一起导致的视觉错位。这种"静默失败"最浪费时间。
Android Studio 的 Vector Asset Studio 还值得用吗
Android Studio 自带的 Vector Asset Studio 在菜单里藏得很深:File > New > Vector Asset,或者右键 drawable 目录。它本质上是个 GUI 包装,底层调用的还是 SVG2Vector,但多了两个实用功能:一是可以在线搜索 Material Icons 直接导入,二是能调整生成的 Vector 的 width、height 和 viewport 尺寸。
这个工具在 Android Studio 4.x 时代有个很烦的 bug:导入某些 SVG 时会报 "Error parsing SVG" 但没有任何具体信息。原因是它依赖的 Batik 解析器对 SVG 的命名空间处理比较严格,有些从 Sketch 或 Adobe Illustrator 导出的 SVG 会在根元素带一堆冗余的 namespace 声明,触发解析失败。 workaround 是手动打开 SVG 文件删掉 xmlns:* 里用不到的那些,或者过一遍 SVGO 清理。
Android Studio Giraffe 和 Hedgehog 版本之后,这个工具的位置和交互做了调整,但核心逻辑没变。它适合快速导入单个图标,或者从 Material Icons 库挑一个现成的。不适合批量处理,也不适合复杂 SVG 的精细调整——没有预览渐变支持情况的功能,也没有路径级别的编辑。
我现在的用法是:简单图标直接走 SVG2Vector 自动转换,稍微复杂一点的先丢进 Vector Asset Studio 看看预览效果,如果预览里已经缺东西了,就知道这条路走不通,得换工具或者手动改。
第三方工具:SVG to Vector Drawable Converter 的取舍
Niklas Baudy 写的 SVG to Vector Drawable Converter(GitHub 上搜 nhaarman/svg-to-vector-drawable 能找到)是个老工具了,但到现在还在维护。它是个命令行工具,基于 Kotlin 实现,核心卖点是"比官方工具支持更多 SVG 特性"。
实际测试下来,它对渐变的处理确实比 SVG2Vector 好一些,会把 SVG 的 <linearGradient> 转成 API 24 兼容的 <gradient> 标签,并加上 android:fillType 之类的适配属性。但代价是生成的 XML 文件里会硬编码 android:apiLevel="24" 之类的限制,如果你需要向下兼容到 API 21,还得手动删掉这些或者用 vectorDrawables.useSupportLibrary = true 走 AppCompat 的兼容路径。
这个工具的另一个问题是路径数据的精度处理。它默认会对浮点数做截断,减少 XML 体积,但某些精细图标截断后会出现肉眼可见的锯齿。配置参数里可以调 floatPrecision,但文档里没明说默认值是多少,得翻源码或者试错。
我去年在一个项目里批量转了一批 200 多个图标,用这个工具跑了一遍,大概 15% 的文件需要手动修复。主要是渐变方向映射错误,和某些 Arc 指令(SVG 的 A 命令)转成 C 贝塞尔曲线时的精度损失。对于大批量转换的场景,这个比例算能接受,但别指望全自动。
在线工具:SVGOMG 和特定转换器的陷阱
SVGOMG(jakearchibald.github.io/svgomg)是 Jake Archibald 做的 SVG 优化工具,不是专门为 Android 设计的,但在转换流程里很有用。它的核心功能是压缩 SVG:删除冗余节点、合并路径、简化 transform、把绝对坐标改成相对坐标等等。这些优化对最终转成的 Vector Drawable 体积也有帮助。
但有个大坑:SVGOMG 的某些优化选项会改变 SVG 的语义,而 Vector Drawable 的转换器对这些变化更敏感。最典型的例子是 "Prefer viewBox to width/height" 这个选项。SVG 里 viewBox 和 width/height 共同决定坐标系映射,Vector Drawable 的 viewportWidth/viewportHeight 只对应 width/height,如果优化后把 width/height 删了,转换器可能拿到错误的尺寸基准。
还有一个隐蔽选项叫 "Round/rewrite transforms",会把嵌套的 <g transform="..."> 矩阵乘法展开到路径数据里。这在 SVG 渲染器里没问题,但 Vector Drawable 的 android:pathData 有长度限制(虽然文档没明确说上限,但实际测试超过一定长度后部分设备会解析失败),展开后的路径可能直接超长。
我的建议是:用 SVGOMG 做清理时,关掉 "Prefer viewBox to width/height" 和 "Round/rewrite transforms" 这两个选项,只保留删除注释、合并颜色、简化数值精度这些安全的优化。清理完再进专门的 Vector Drawable 转换器。
另一个在线工具是 a-student.github.io/SvgToVectorDrawableConverter,这个工具的特点是会把转换过程可视化,列出哪些元素被支持、哪些被忽略。但它已经好几年没更新,对较新的 SVG 特性(比如 CSS 变量、某些滤镜)识别不准确,而且生成的代码风格比较老旧,不会用 android:fillColor 的 theme attribute 适配。偶尔应急查一下可以,不建议进正式工作流。
付费工具:Vector Asset Studio 的替代方案
Shapeshifter(github.com/alexjlockwood/shapeshifter)是 Alex Lockwood 做的工具,现在主要形态是 Web 版本(shapeshifter.design),完全免费。它定位是"SVG 和 Vector Drawable 的桥梁",支持双向转换,还能做动画路径的插值。
这个工具对开发者的价值在于:它会把 SVG 的每个元素映射到 Vector Drawable 的对应结构,并在界面上标出"不支持"的部分。比如一个 SVG 用了 <mask>,Shapeshifter 的预览区会直接显示遮罩效果,但导出为 Vector Drawable 时会提示 "Masks are not supported in Vector Drawable format",让你明确知道要手动处理。
Shapeshifter 的 path 编辑器也很实用。Vector Drawable 的 android:pathData 是字符串形式,手动改很痛苦,Shapeshifter 提供了节点级别的编辑,可以拖拽调整曲线控制点,实时看效果。这比在 Android Studio 里改 XML 再编译看效果快得多。
但它也有局限。Web 版本的性能处理不了特别复杂的 SVG,几千个节点以上的文件会卡死。动画功能(AVD 导出)虽然强大,但生成的 AnimatedVectorDrawable XML 很冗长,手工维护困难。另外它不支持批量处理,一次只能操作一个文件。
我目前的 workflow 是:复杂单图标用 Shapeshifter 做预览和微调,确认转换效果后再导出;简单图标批量走命令行工具。两者互补。
手动干预的几种典型场景
工具再智能,也覆盖不了所有 case。以下几种情况我遇到过多次,基本需要手动改 SVG 或者改生成的 Vector XML。
文字转路径。前面说过 Vector Drawable 不支持 <text>,如果图标里嵌了文字,必须在设计端就转成轮廓路径。Figma 里选文字层右键 "Outline stroke",Sketch 里 "Convert to Outlines",Illustrator 里 "Create Outlines"。注意转完之后检查字间距,有些字体轮廓化后会有微小重叠,在 Vector Drawable 的 fillType="evenOdd" 下可能出现镂空。
渐变的手动映射。SVG 的 <linearGradient x1="0%" y1="0%" x2="100%" y2="0%"> 转成 Vector Drawable 的 <gradient android:type="linear" android:startX="0" android:startY="0" android:endX="24" android:endY="0">,这里的坐标是相对于 viewport 的绝对值,不是百分比。如果 SVG 的 viewBox 和最终 Vector 的 viewport 不一致,渐变方向会错。必须手动按比例换算。
Clip-path 的替代方案。Vector Drawable 不支持 clip-path,但可以用 android:fillType="evenOdd" 配合路径的绕向来做简单遮罩。复杂 clip 的话,只能让设计师重新画,或者把遮罩效果栅格化成 PNG 再嵌入(但这就失去 Vector 的意义了)。我通常的做法是:评估这个 clip 是否核心视觉效果,如果是就保留为 PNG,如果不是就让设计师简化。
Transform 的展开顺序。SVG 的 transform 是矩阵后乘(从右到左执行),Vector Drawable 的 android:group 里的 android:rotation、android:scaleX/scaleY、android:translateX/translateY 是分别指定的,没有明确的执行顺序文档。实际测试下来,系统渲染器是先 scale 再 rotate 再 translate,和 SVG 的默认行为不完全一致。如果 SVG 里用了复合 transform,最好手动展开成嵌套的 <group>,避免顺序歧义。
版本兼容的暗礁:AppCompat 和 VectorDrawableCompat
很多人以为 vectorDrawables.useSupportLibrary = true 就万事大吉了,能兼容到 API 14。但实际上 VectorDrawableCompat 的兼容是有限度的,它只兼容 Vector Drawable 的核心特性,API 24 以上的新特性(渐变、stroke 的复杂属性等)在旧系统上要么降级要么崩溃。
具体说,渐变在 API 23 及以下会被忽略,变成透明填充。如果你的图标依赖渐变来区分层次,旧设备上直接消失。android:trimPathStart/trimPathEnd 的动画属性在兼容库里有实现,但性能比原生差很多,低端机上掉帧明显。
还有一个坑是 android:tint 和 theme attribute 的交互。VectorDrawableCompat 对 ?attr/colorControlNormal 这类 theme 属性的解析在旧版本 AppCompat 里有 bug,某些情况下会缓存第一次解析的颜色值,切换深色模式时不刷新。这个 bug 在 AppCompat 1.4.0 之后修复,但如果你的项目还卡在老版本,可能遇到。
我的建议是:如果项目 minSdk 还在 21 或 23,转 SVG 时主动避开渐变,用纯色分层。非要渐变的话,准备一套 fallback 的纯色版本,用 drawable-v24 资源目录做版本隔离。别指望兼容库魔法解决一切。
性能考量:Vector Drawable 不是免费午餐
Vector Drawable 的渲染开销经常被低估。它在 CPU 端解析 path 数据,生成 tessellation 后的三角形网格,再送 GPU 渲染。对于简单图标这个开销可以忽略,但复杂路径在低端机上能占到几毫秒的帧时间。
一个具体的优化点:减少 path 的节点数。SVG 从设计软件导出时经常带大量冗余节点,比如一条直线被拆成十几个点,或者曲线控制点过度拟合。用 SVGOMG 的 "Simplify paths" 或者手动在矢量编辑器里优化,能显著减少 Vector Drawable 的解析时间。
另一个点是避免过度使用 android:pathData 的嵌套 clip 和复杂 fill rule。evenOdd 和 nonZero 的 fill type 在某些 GPU 驱动上有已知 bug,虽然罕见,但遇到就是黑屏或闪退。保守做法是优先用 nonZero,只在必须镂空的地方用 evenOdd。
批量加载 Vector Drawable 也有坑。如果在 RecyclerView 的每个 item 里都用一个不同的复杂 Vector,滑动时会频繁解析和 tessellate,造成卡顿。解决方案是启用 VectorDrawableCompat 的缓存,或者把常用图标预加载为 Bitmap 缓存。但这就又回到"是不是该直接用 PNG"的权衡了。
一个完整的转换 workflow 示例
结合上面的工具和经验,我现在的实际操作流程大概是:
拿到设计师的 SVG 后,先丢进 Shapeshifter 看预览。如果提示有不支持元素,评估是否核心效果。如果是文字,打回给设计师转路径。如果是渐变,看 minSdk 是否支持,不支持就协商改纯色或者准备 fallback。
简单图标(单色、无渐变、无特殊效果)直接走 SVG2Vector 自动编译,不额外处理。编译后看 build warning,确认没有静默丢弃元素。
复杂图标在 Shapeshifter 里做精细调整,确认 viewport 比例、渐变映射、path 节点数都合理后,导出 XML 手动放进项目。不依赖自动转换,避免意外。
批量图标(比如一次性导入 50 个)先用 SVGOMG 做保守清理,再用命令行工具批量转换,最后抽样检查几个,确认没有系统性问题。
所有图标在低端测试机上跑一遍,看渲染性能和视觉效果。特别是深色模式、不同缩放级别、动画场景(如果有)。
这个流程不追求全自动,但每个环节的可控性比较高,出问题能快速定位是哪个工具、哪个配置导致的。
几个具体版本的已知问题
最后列几个我实际踩过、有版本号可考的坑,供排查时参考:
Android Gradle Plugin 7.0.0 到 7.2.0 之间,SVG2Vector 对 SVG 的 stroke-dasharray 处理有误,会把虚线样式转错,生成实心线条。7.3.0 修复。
AppCompat 1.3.0 之前,VectorDrawableCompat 在某些国产 ROM 上解析 android:fillType 会 NPE,原因是这些 ROM 改了 Resources 的实现。1.3.0 加了防御性判断。
Android 13(API 33)上有个系统 bug,某些带 android:gradient 的 Vector Drawable 在硬件加速关闭时渲染为黑色。Google Issue Tracker 里有记录,workaround 是强制硬件加速或者避免在该场景用渐变。
Shapeshifter 的 Web 版本在 Safari 上有性能问题,复杂 SVG 会触发浏览器内存限制崩溃。用 Chrome 或 Firefox 稳定很多。
这些细碎的问题没有统一文档,基本靠 GitHub issue、Stack Overflow 的零散讨论,和自己踩坑积累。写这篇文章也是想把这部分经验结构化,减少后来者的重复踩坑时间。
工具链永远在变,Android 15 的预览版里 Vector Drawable 的渐变支持似乎有扩展迹象,但正式文档还没更新。保持关注官方 release note,同时不盲目追新,稳定可预期的行为比"可能支持的特性"更有价值。