首页 / 资讯中心 / 文章详情

Android Vector Drawable 从入门到实战:XML语法、SVG转换与动画避坑指南

Android Vector Drawable 从入门到实战:XML语法、SVG转换与动画避坑指南 ★ FEATURED ARTICLE
做 Android 开发这几年我见过太多为一个图标同时导出 mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi 五张切图的场景。文件夹里堆满了一排 PNG换一版设计稿就要全部重出一次漏掉任何一个分辨率总会在某台旧设备上看到毛边。后来项目里逐步把图标资源切到 vector矢量图之后这件事才算真正省下来。vector 本质上是用一段 XML 描述图形的轮廓和填充与分辨率无关任意尺寸下边缘都保持清晰。这篇文章想把 vector 的入门知识一次性讲透它到底是什么、XML 语法怎么读、怎么从 SVG 转换、怎么做动画、上线前要避开哪些坑。适合天天跟资源与适配打交道的客户端开发也适合想把设计稿直接转成代码资源的视觉同学。看完之后你可以亲手把一个图标写成 XML并且知道在低版本系统上怎么不踩雷。1. 从位图思维切换到矢量思维Vector Drawable 解决了什么问题1.1 位图和矢量图的底层差异位图和矢量图最本质的区别是存储信息的方式。位图存的是一个个像素点的颜色放大之后像素点本身不会变多边缘于是出现锯齿和马赛克矢量图存的是几何形状的数学描述比如“从 A 点沿一条贝塞尔曲线画到 B 点”或者“这个闭合区域用黑色填充”缩放时只是重新计算坐标边缘始终平滑。拿画画类比位图是用马赛克拼出画面矢量图是用一支无限细的画笔沿着数学公式画曲线。在 Android 里vector 对应 android.graphics.drawable.VectorDrawable它本身是一种 Drawable可以塞进 ImageView 的 src也可以作为 View 的背景。它和 PNG 最大的不同在于PNG 在编译时就是静态像素vector 在运行时由渲染引擎根据虚拟坐标动态绘制。这个差异带来两个直接结果一是 APK 体积更小二是任意密度不糊。2dp 和 200dp 显示同一个矢量图标清晰度完全一致。1.2 Android 对矢量图的支持现状经常有人说 Android 矢量图兼容麻烦这句话对一半。从 Android 5.0API 21开始framework 原生支持 VectorDrawable 和 AnimatedVectorDrawable也就是说 5.0 以上的系统直接引用 vector 资源不会出问题。那 5.0 之前呢靠兼容库。早期的 V4 支持库里就有 VectorDrawableCompat后来统一进入 AndroidXAppCompat 系列控件在加载资源时会自动把 vector 交给兼容实现处理。现在的工程新创建时 minSdk 基本都是 21 以上很多团队直接放弃旧版本兼容。如果项目还在支持 5.0 以下的老设备也不是不能用两条路一条是配置 vectorDrawables.useSupportLibrary true让 AppCompat 控件帮忙解析另一条是在构建时把 vector 转成位图牺牲一点体积换全局兼容。实际项目里最常见的做法是两者结合既保留 vector 源文件又对系统级场景做兜底。1.3 用矢量图解决掉的工程痛点第一个痛点是适配成本。以前一个图标五套图设计师每改一版五套图都要重新导漏导一套就会在某个分辨率上露馅。换成 vector 之后设计师只需要导出一份 SVG开发转成 XML所有密度全覆盖。第二个痛点是变色。按钮在不同状态需要不同颜色的图标位图时代要么出多份彩图要么在代码里处理染色vector 只需要 android:tint 一个属性代码里 DrawableCompat.setTint 也能动态换色做皮肤切换非常轻松。第三个痛点是动画。位图做形变动画基本要预置多帧序列图空间占用极大vector 动画可以在运行时变换路径数据APK 里仍然是那一份 XML。这三点叠加让我现在看到新图标的第一反应就是“能不能写成一个 vector”。2. 认识 Vector 的本质XML 标签与 pathData 语法拆解2.1 最小可运行的 vector 文件长什么样先看一个最简单的例子。假设我要画一个播放按钮的三角形文件放在 res/drawable/ic_play.xmlvector xmlns:androidhttp://schemas.android.com/apk/res/android android:width24dp android:height24dp android:viewportWidth24 android:viewportHeight24 path android:fillColor#FF000000 android:pathDataM8,5 L8,19 L19,12 Z / /vector渲染出来就是一个从上边出发、向右下延伸、闭合的黑色三角形。这里有两组尺寸要理解透width 和 height 决定这个 Drawable 在屏幕上实际占多大单位是 dpviewportWidth 和 viewportHeight 不直接参与屏幕渲染它只定义一个虚拟坐标系。比如上面 viewport 是 24x24那么 pathData 里的坐标就按 0 到 24 的范围设计系统最终会把虚拟坐标等比映射到实际宽高上。这个映射关系很像看设计稿设计师以 24 的网格画图标开发设置物理显示大小两者的比值就是缩放因子。改动 width 不影响 pathData只影响缩放这正是 vector 适配密度的原理。2.2 pathData 指令逐条拆解pathData 是 vector 里最核心、也最容易劝退新手的属性。它的语法和 SVG 中 path 元素的 d 属性几乎一致本质是一连串绘图指令。每条指令由一个英文字母开头后面跟数字。字母大写表示绝对坐标小写表示相对坐标。我把开发里最常用的指令整理成速查表指令含义作用Mmoveto把画笔移动到某个坐标不画线Llineto画一条直线到指定坐标Hhorizontal lineto画水平直线Vvertical lineto画垂直直线Ccubic bezier画三次贝塞尔曲线需要 3 个点Ssmooth cubic平滑三次贝塞尔需要 2 个点Qquadratic bezier画二次贝塞尔曲线需要 2 个点Tsmooth quadratic平滑二次贝塞尔需要 1 个点Aarc画圆弧Zclosepath闭合路径回到起点以 L 为例“M8,5 L8,19”就是先把画笔移动到坐标(8,5)再画一条直线到(8,19)。字母和数字、数字和数字之间可以用空格或逗号分隔所以“M8,5L8,19Z”这种紧凑写法也合法只是可读性差。大小写很关键。大写 M 是“移动到绝对位置”小写 m 是“相对当前点移动多少距离”。比如上一段终点是(8,19)接着写“l5,0”实际是从(8,19)向右偏移 5到达(13,19)。SVG 转 vector 时常会出现大量小写指令不用慌语义一样只是坐标系不同。2.3 group 和 clip-path 的配合使用vector 资源里不只有 path还有 group。group 是一个分组容器可以把多个 path 包起来统一做变换支持的属性有 scaleX、scaleY、rotation、pivotX、pivotY、translateX、translateY。如果你的图标是复杂组合比如“播放按钮外面套一个圆环”把圆环和三角形分别放进不同 group后续做动画或者局部调整就能独立控制。clip-path 则用于裁剪它本身不是可见图形而是定义一块裁剪区域区域内可见、区域外透明。比如做一个“只显示上半部分的心形图标”可以先定义一个矩形裁剪路径再画心形。clip-path 的 pathData 必须闭合否则裁剪结果不可预期。很多人刚接触 group 时觉得它多余把所有 path 平铺在 vector 根节点下也照样显示。但一旦进入动画章节group 的价值就非常明显缩放和旋转动画大多作用在 group 上而不是单个 path 上。所以我的建议是一开始就养成分组习惯哪怕暂时只有一个 group。3. 从 SVG 到 Vector创建资源与接入项目的完整过程3.1 创建 vector 资源的三种途径第一种是纯手工写 XML。对简单几何图形可行对复杂图标效率极低而且 pathData 写错很难肉眼察觉。第二种是借助现成工具转换。常规做法是从设计稿导出 SVG再用转换工具变成 vector XML。这里要提前提醒不是所有 SVG 都能完美转换。SVG 支持渐变、滤镜、文本、蒙版而 Android vector 只支持 fillColor、strokeColor、strokeWidth 这类基础的描边与填充。原始 SVG 里一旦用了线性渐变转换后往往丢色或者变成一段无效路径文本大概率会变成一堆无法阅读的外轮廓。第三种是在开发工具里通过资源菜单的 Vector Asset 入口创建支持从图片导入也支持从内置图标库选择。这个入口底层也是 SVG 转 vector目的是让开发快速拿到一个能用的资源而不是替代设计师的整套流程。3.2 SVG 与 Vector 的映射关系SVG 的路径和 vector 的 path 非常像但细节差异要留意。SVG 坐标单位不确定vector 必须在 viewportWidth/viewportHeight 里定义坐标系SVG 用 stroke、fill 等属性vector 对应 strokeColor、strokeWidth、fillColorSVG 支持 CSS class 和 style 继承vector 不支持所有属性必须显式写在 XML 标签上。转换流程通常是打开 SVG去掉非路径元素统一坐标到 viewport 大小转成 XML再人工检查 pathData。我习惯转完以后在预览里放大 10 倍看边缘有没有断线再缩到 16dp 看有没有糊成一团。这一步能避开很多“代码没错但看起来就是不对”的玄学问题。3.3 在布局和代码中使用 vector最常规的用法是作为 ImageView 的图片资源ImageView android:layout_width48dp android:layout_height48dp android:srcdrawable/ic_play /代码里获取 drawable 再设置也一样自然Drawable drawable ContextCompat.getDrawable(context, R.drawable.ic_play); imageView.setImageDrawable(drawable);如果使用的是 AppCompatImageView 这类兼容控件库会在底层自动拦截 vector 的加载。但如果目标是原生 ImageView 且系统低于 API 21直接引用 vector 就可能触发异常这通常是兼容问题的高发点。还有一类用法是给控件设置背景比如按钮的 selector。做法是让 selector 里的 item 指向 vector 文件同样支持按状态切换。要注意的是vector 背景的宽高如果依赖控件宽高而控件又是 wrap_content最终尺寸会按 vector 自身的 width 和 height 计算容易出现“背景不铺满”的情况。3.4 Gradle 兼容配置与兜底思路对于还要支持 API 21 以下的老项目Gradle 里必须开启兼容库支持在 defaultConfig 中配置android { defaultConfig { vectorDrawables.useSupportLibrary true } }开启之后旧系统上的兼容控件会尝试用支持库的实现去解析 vector。但注意这个开关不会让系统全局所有位置的 vector 都可用比如通知栏图标、桌面快捷方式这类系统界面里的 Drawable仍然走系统侧解析逻辑低版本上不保证显示正确。所以需要一条兜底思路构建时把 vector 转成 PNG。部分团队会采用自动化方案在构建阶段把 drawable 下的 vector 资源按密度生成 PNG 一并打包代价是 APK 体积变大收益是任何系统场景都不会失手。我的建议是minSdk 不低于 21 就全面使用 vectorminSdk 低于 21 时业务图标用 vector 配合兼容库通知栏、快捷方式这类系统场景手动备一份 PNG 兜底。4. 让静态图标动起来Vector 动画的三种玩法4.1 属性动画对单一属性做变化vector 动画依赖 android.animation.ObjectAnimator可动画的关键属性包括fillColor填充色可以做颜色渐变strokeColor描边色rotation旋转一般作用在 group 或 path 上scaleX / scaleY缩放translateX / translateY平移trimPathStart、trimPathEnd、trimPathOffset路径修剪。trim 系列是矢量描边动画的基础。trimPathStart 表示从路径起点裁掉多大比例trimPathEnd 表示从终点往回保留多大比例取值范围都是 0 到 1。把 trimPathStart 从 1 动画到 0就会出现一条线“逐渐画出来”的效果。很多应用首次进入时的 logo 描边动画本质就是这件事。4.2 pathData 形变动画最出效果的是让一个图形的轮廓平滑变成另一个图形。原理是对两组 pathData 做逐段插值因此要求两条路径的指令数量和类型完全一致。比如把“播放三角”变成“加号”播放三角的 pathDataM8,5 L8,19 L19,12 Z加号的 pathDataM5,12 L19,12 M12,5 L12,19这两组指令数量不一致直接做动画会失败或者出现跳变。想让形变成立必须让两条路径结构相同。实际项目里做 morph 动画前我的习惯是先用工具把两套图标统一成同一条折线路径保证 M、L、C 的命令数量和顺序完全一致。这个准备工作的复杂度通常比写动画代码本身更高。4.3 一个完整的 animated-vector 实例完成一个 AnimatedVectorDrawable需要三个文件配合静态 vector、objectAnimator、animated-vector 资源。先定义一个静态 vector给目标 path 起好名字vector xmlns:androidhttp://schemas.android.com/apk/res/android android:width48dp android:height48dp android:viewportWidth24 android:viewportHeight24 path android:namearrow android:strokeWidth2 android:strokeColor#FF000000 android:pathDataM2,12 L22,12 / /vector再定义动画器objectAnimator xmlns:androidhttp://schemas.android.com/apk/res/android android:duration600 android:propertyNametrimPathEnd android:valueFrom0 android:valueTo1 android:valueTypefloatType /接着用 animated-vector 把它们绑在一起animated-vector xmlns:androidhttp://schemas.android.com/apk/res/android android:drawabledrawable/ic_arrow target android:namearrow android:animationanimator/trim_end_animator / /animated-vector最后在代码里触发Drawable drawable ContextCompat.getDrawable(context, R.drawable.anim_arrow); ImageView imageView findViewById(R.id.iv_arrow); imageView.setImageDrawable(drawable); if (drawable instanceof Animatable) { ((Animatable) drawable).start(); }这套流程本质上还是一次 ObjectAnimator 的封装只是把动画目标换成了 vector 里的具名 path 或 group。命名要全局唯一target 里 name 写错时资源加载阶段通常不会崩执行动画时却完全没反应这是排查时最容易忽略的点。4.4 动画的兼容边界AnimatedVectorDrawable 从 API 21 开始可用低版本环境可以走兼容实现但支持范围有限。旧系统的兼容实现里pathData 形变动画支持得并不好trimPath 类动画问题更明显。如果产品必须覆盖 API 21 以下且动画又是核心卖点稳妥方案是高版本用 vector 动画低版本回退到帧动画或 View 的旋转、位移动画。动画性能也要有预期。vector 动画发生在 Drawable 渲染阶段每次属性变化都会触发一次重绘动画时长内连续的属性变化会给主线程带来压力。路径越复杂、Canvas 尺寸越大压力越明显。低端机测试时如果列表滑动场景同时播放多个 vector 动画出现掉帧优先考虑简化路径而不是换测试设备。5. 兼容、性能与避坑上线前必须知道的几件事5.1 编译期和运行期常见报错我把这些年遇到的 vector 相关问题整理成速查表排查思路基本都在里面现象常见原因处理方式编译报 Invalid file namedrawable 文件名含大写字母或非合法字符改成小写字母、数字、下划线XML 解析失败 / PathParser 报错pathData 里出现中文标点、多余空格、非法指令检查指令字母和分隔符严格按 SVG 语法低版本设备上 ImageView 显示空白未开启 support 库或使用了不参与兼容的控件开启 vectorDrawables.useSupportLibrary更换兼容控件动画执行但无效果target name 与 path 的 name 不一致逐个核对 name避免多个 name 重复形变动画跳到不可预期的形状两条 pathData 的指令序列不一致统一两套路径的结构后再动画tint 颜色不生效高版本用 android:tint低版本只依赖兼容库但代码未设置代码里通过 DrawableCompat.setTint / setTintList 设置表里这些情况大多来自实际项目。尤其是 pathData 里出现中文逗号这种低级错误网上的一些转换工具偶尔会输出奇怪字符不检查就会在编译流程里卡住很久。5.2 性能场景怎么选适合用 vector 的地方与不适合用 vector 的地方vector 不是万能灵药它有自己的成本。渲染一个复杂 vector 时CPU 要逐条解析 pathDataGPU 要处理各种图形的几何轮廓比直接渲染一张同尺寸位图更贵。大尺寸背景图、频繁重绘的列表 item、动画中的复杂图标都是潜在的压力点。场景推荐方案理由普通列表项内的小图标vector路径简单加载成本低适配无压力大尺寸启动图或插画PNG/WebP路径复杂时 vector 渲染开销高需要换肤变色的按钮图标vectortint 属性一行搞定复杂渐变图标PNG/WebPvector 不支持线性渐变硬转会失真通知栏等系统级场景PNG 兜底低版本系统兼容实现不可控判断标准很简单路径少于 100 个点、宽高小于 100dp、重绘频率不高放心用 vector反向场景多考虑位图方案。5.3 我的几点经验技巧先说 RTL。阿拉伯语、希伯来语环境下很多图标需要镜像显示。vector 文件加一句 android:autoMirroredtrue系统会自动根据 RTL 环境做镜像。但该属性只对方向性图标有意义不是所有图标都需要盲目开启会导致普通图标也被翻转。再说 tint。如果你在 vector 的 path 里写了 android:fillColor又在使用处给 ImageView 设置了 imageTintList最终显示优先级和 Drawable 的 tint 机制叠加可能出现“明明设置了颜色却不生效”的困惑。我的经验是统一用一种方式管理 tint要么全在 XML 里要么全在代码里 setTint不要混用。最后说资源瘦身。vector 源文件通常只有几 KB但一个 drawable 里塞几十个 path 时解析成本会显著上升。很多人从图标库导入后从不清理SVG 里残留一堆无用 path最后 vector 文件比一张 PNG 还大。导入后花几分钟删掉空路径、合并重复路径收益非常明显。实际上我刚开始接触 vector 时也踩过不少坑最深的体会是pathData 没有想象中神秘但它不值得每个人从头学到底。日常开发里大部分时间花在转换工具、兼容配置和动画命名上真正的语法细节反而不是瓶颈。如果团队里设计师资源充足非常建议推动“设计侧直接输出 SVG、开发侧转 vector”的协作流程这比开发自己去描图标高效得多。后续想深入的话优先研究 AnimatedVectorDrawable 的形变动画它是 vector 相比位图最大的优势也是做交互动效时最容易出彩的部分。
阅读完成 · 觉得有帮助?
咨询建站