32、矢量图与SVG:VectorDrawable使用、AnimatedVectorDrawable、SVG转Vector
说实话,做Android开发这么多年,我踩过最大的坑之一就是图片适配。早期项目里塞了几百张PNG,不同分辨率各一套,APK体积直接飙到30多M。后来全面转向矢量图,APK瘦身效果立竿见影。今天咱们就聊聊Android里的矢量图方案——VectorDrawable。
为什么Android需要矢量图?
你想想看,一张100x100的图标,放到300dpi的屏幕上,PNG得准备三套图。而矢量图呢?一个XML文件搞定所有分辨率。它不依赖像素,而是通过数学路径来描述图形。说白了,就是一套坐标点+绘制指令。
我在项目中遇到过最典型的场景:App需要支持平板和手机,图标在不同屏幕密度下都得清晰。用PNG的话,光图标资源就得翻三倍。换成VectorDrawable后,一个文件走天下,省心多了。
- 文件极小:一个复杂图标通常只有1-3KB
- 无损缩放:任意尺寸都不会模糊
- 支持动画:配合AnimatedVectorDrawable实现流畅过渡
- 减少APK体积:尤其适合多语言、多屏幕适配
VectorDrawable基础用法
先看一个最简单的例子。一个红色的圆形,用VectorDrawable怎么写?
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FF0000"
android:pathData="M24,24m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"/>
</vector>
这里有两个关键属性:viewportWidth/Height定义的是逻辑坐标系,width/height是实际显示尺寸。pathData里的指令跟SVG的d属性几乎一样。M是移动到,m是相对移动,a是弧线。嗯,这里要注意:viewport的数值不一定要跟实际尺寸一致,但比例要匹配,否则图形会变形。
我个人习惯把viewport设成100的整数倍,这样pathData里的坐标好算。比如图标设计在100x100的网格上,直接抄坐标就行。
pathData指令速查表
| 指令 | 含义 | 示例 |
|---|---|---|
| M / m | 移动到(绝对/相对) | M10,10 |
| L / l | 画直线 | L20,30 |
| C / c | 三次贝塞尔曲线 | C10,20 30,40 50,60 |
| Q / q | 二次贝塞尔曲线 | Q15,25 40,50 |
| A / a | 弧线 | A10,10 0 0,1 20,20 |
| Z / z | 闭合路径 | Z |
SVG转VectorDrawable
实际开发中,我们很少手写pathData。设计师给的一般是SVG文件。怎么转成Android能用的VectorDrawable?
我推荐三种方式:
- Android Studio内置工具:右键res/drawable → New → Vector Asset → Local SVG。最省事,但有些SVG特性不支持(比如渐变、滤镜)。
- 在线转换工具:比如svg2vector.com。适合批量转换,但要注意版权问题,别把敏感设计图传上去。
- 命令行工具:用svgsync或自己写脚本。适合CI/CD流程,自动化处理。
我曾经接手过一个老项目,里面混用了PNG和SVG,设计师给的是Sketch源文件。我写了个Python脚本,批量导出SVG再转成VectorDrawable,一天搞定了原来需要一周的适配工作。
- 使用了CSS样式而非内联属性
- 包含位图嵌入(比如base64图片)
- 使用了复杂的滤镜或蒙版
- 文本元素(Android不支持SVG文本转路径)
AnimatedVectorDrawable:让图标动起来
静态矢量图只是开胃菜,真正的硬菜是动画。AnimatedVectorDrawable可以让你对矢量图的路径、颜色、旋转等属性做动画。
它需要三个文件配合:
- VectorDrawable静态图
- ObjectAnimator动画定义
- AnimatedVectorDrawable将它们关联起来
看个例子:一个箭头从右向左移动的动画。
第一步:定义静态矢量图 arrow.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:name="arrow"
android:fillColor="#333333"
android:pathData="M8,24 L40,24 M32,16 L40,24 L32,32"/>
</vector>
第二步:定义动画 animator/arrow_anim.xml
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="translateX"
android:valueFrom="0"
android:valueTo="-20"
android:duration="500"
android:repeatCount="infinite"
android:repeatMode="reverse"/>
第三步:关联起来 drawable/animated_arrow.xml
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/arrow">
<target
android:name="arrow"
android:animation="@animator/arrow_anim"/>
</animated-vector>
然后在代码里这样用:
val iv = findViewById<ImageView>(R.id.iv_arrow)
iv.setImageResource(R.drawable.animated_arrow)
val anim = iv.drawable as AnimatedVectorDrawable
anim.start()
为什么动画要拆成三个文件?说白了,这是Android的设计哲学——关注点分离。静态图归静态图,动画归动画,组合归组合。这样你可以复用同一个矢量图,配上不同的动画效果。
- fillColor / strokeColor:颜色渐变
- translateX / translateY:平移
- scaleX / scaleY:缩放
- rotation:旋转
- trimPathStart / trimPathEnd:路径裁剪(类似进度条效果)
- pathData:路径变形(需要起始和结束路径的指令数一致)
知识体系结构图
下面这张图梳理了本章的核心脉络,从矢量图基础到动画实现,再到实际开发中的工具链:
实际项目中的避坑指南
讲几个我踩过的坑,希望能帮你省点时间。
坑一:路径变形动画的指令数必须一致
我曾经做一个加载动画,想让一个圆形渐变成对勾。结果动画跑起来直接崩了。查了半天才发现,圆形的pathData用了4条贝塞尔曲线,对勾只用了3条直线。指令数不一样,AnimatedVectorDrawable直接抛异常。解决办法:让设计师把两个图形的路径拆成相同数量的指令段。
坑二:VectorDrawable在低版本上的兼容性
VectorDrawable从API 21开始原生支持。如果你要兼容API 19以下,得用AppCompat库。但注意,AppCompat的VectorDrawable不支持所有特性,比如android:fillAlpha在某些版本上无效。我的建议是:如果minSdk低于21,尽量用简单的纯色矢量图,别玩透明度渐变。
坑三:过度使用矢量图导致UI卡顿
矢量图虽然文件小,但渲染开销比PNG大。因为每次绘制都要重新计算路径。如果你在列表里用了大量复杂的矢量图,滑动时可能会掉帧。我一般遵循这个原则:图标类(路径少于20条)用矢量图,复杂插画(路径超过50条)用WebP或PNG。
- 避免在RecyclerView中频繁启动AnimatedVectorDrawable
- 复杂矢量图(超过100条路径)考虑用位图代替
- 动画时长控制在300ms-600ms之间,太短看不清,太长显得拖沓
- 使用
android:fillType="evenOdd"处理镂空图形时,注意渲染顺序
总结一下
VectorDrawable是Android开发中处理图标的利器。它让APK瘦身、适配简单、还能做流畅动画。但也不是万能的——复杂图形、低版本兼容、性能开销都是需要权衡的点。
我个人建议:图标、按钮、简单装饰图用矢量图;照片、复杂插画、渐变丰富的图形用位图。两者结合,才是最优解。
好了,矢量图这块就聊到这儿。记住一点:工具是死的,思路是活的。别为了用矢量图而用矢量图,合适才是最好的。