深入 Android AnimatedVectorDrawable 全链路:从 XML 矢量路径动画到 Compose 声明式矢量动画的演进
一个图标动画引发的追溯
最近在做图标过渡动画时,用 AnimatedVectorDrawable 实现了一个播放/暂停按钮的形态切换。效果跑通了,但有个细节让我好奇:<target> 标签里的 android:name 到底是怎么匹配到 VectorDrawable 内部 path 的?它跟 ObjectAnimator 又是什么关系?
顺着这个问题翻了源码,发现 AVD 的设计比表面看起来精巧——它本质上是一层轻量的桥接层,真正干活的是 ObjectAnimator。
AVD 不执行动画,它是 Animator 的宿主
AnimatedVectorDrawable 本身不做任何动画计算。它持有一组 ObjectAnimator,由这些 Animator 分别驱动 VectorDrawable 内部的各个命名节点。
AVD 的 XML 结构分三层:
<animated-vector xmlns:android="..."
android:drawable="@drawable/ic_play_pause">
<target
android:name="left_bar"
android:animation="@animator/left_bar_rotate" />
<target
android:name="right_bar"
android:animation="@animator/right_bar_rotate" />
</animated-vector>
每个 <target> 是一条映射:name 必须与 VectorDrawable 中某个 Group 或 Path 的 android:name 完全一致。animation 引用的 XML 由 AnimatorInflater 解析为一组 ObjectAnimator。
说白了,AVD 只是 Animator 的容器,ObjectAnimator 是引擎,VectorDrawable 里的命名节点是被操作的目标。
VectorDrawable 的命名树
VectorDrawable 内部是一棵 Group/Path 树,每个节点可以有一个 android:name:
<vector xmlns:android="..."
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<group android:name="rotation_group"
android:pivotX="12" android:pivotY="12">
<path
android:name="left_bar"
android:fillColor="#FF000000"
android:pathData="M6,6 L10,6 L10,18 L6,18 Z" />
</group>
</vector>
AVD 启动时调用 VectorDrawable.getTargetByName(name) 遍历整棵树,找到对应的 VGroup 或 VFullPath 对象。找到后,ObjectAnimator 通过 PropertyValuesHolder 直接操作这些对象的属性——不需要反射,因为 VGroup 和 VFullPath 都实现了对应的 getter/setter。
VGroup 内部维护一个 3×3 变换矩阵,每次属性变更后调用 invalidateSelf() 触发重绘,调用链非常直接。
ObjectAnimator 如何驱动节点属性
ObjectAnimator 通过 propertyName 匹配 setter,这里有一个隐式的命名约定:
<objectAnimator xmlns:android="..."
android:propertyName="rotation"
android:duration="300"
android:valueFrom="0"
android:valueTo="180" />
当 propertyName="rotation" 时,Animator 反射调用 VGroup.setRotation(float)。同样,pivotX 对应 setPivotX(),scaleY 对应 setScaleY()。
常用的属性映射:
| propertyName | 目标节点类型 | 行为 |
|---|---|---|
| rotation | Group | 矩阵旋转 |
| pivotX / pivotY | Group | 变换中心点 |
| scaleX / scaleY | Group | 矩阵缩放 |
| translateX / translateY | Group | 矩阵平移 |
| pathData | Path | 路径数据替换 |
| fillColor | Path | 填充色渐变 |
| trimPathStart / End / Offset | Path | 路径裁剪 |
| strokeColor / strokeWidth | Path | 描边属性 |
Group 级别的动画做的是矩阵变换,Path 级别的动画做的是属性值替换。两者可以并行——一个 Group 在旋转的同时,其子 Path 可以独立做 fillColor 渐变,彼此不干扰。
pathData 动画的坑:结构必须一致
pathData 动画最容易让人产生误解。它不是逐帧插值两个任意路径的坐标,而是要求源路径和目标路径具有完全相同的命令结构——命令类型、数量、顺序必须一致,只有坐标参数不同。
<objectAnimator
android:propertyName="pathData"
android:valueFrom="M12,2 L2,22 L22,22 Z"
android:valueTo="M12,2 L2,12 L22,12 Z"
android:valueType="pathType"
android:duration="300" />
SDK 内置的 PathParser 在解析时会对比两个路径的命令序列。如果结构不匹配,不会抛异常——路径会直接跳变到最终状态,动画”失效”但没有错误提示。这是最常见的排查盲区。
踩过的一个坑是在 Android 5.0 上,某些复杂 pathData 会导致 IllegalArgumentException: Unknown pattern。追到源码发现 PathParser 的正则匹配在高版本才完善,低版本遇到特定 path 命令组合会直接崩溃。解决方案是手动保证两个 pathData 的命令完全一致,或者低版本降级为静态图标切换。
Compose 的声明式范式:AnimatedImageVector
AVD 的问题不在于功能,而在于心智负担:你需要同时维护 VectorDrawable XML、Animator XML 和 AVD XML 三个文件,修改一个节点名称要在三条引用链中逐个确认。
Compose 把模型翻转了——它并不是重新发明一套矢量动画引擎,而是直接复用已有的 AVD XML 资源,用声明式状态驱动播放:
val image = AnimatedImageVector.animatedVectorResource(R.drawable.avd_play_to_pause)
var atEnd by remember { mutableStateOf(false) }
Icon(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = null,
modifier = Modifier.clickable { atEnd = !atEnd }
)
AnimatedImageVector.animatedVectorResource() 加载的其实还是同一份 <animated-vector> XML 资源,rememberAnimatedVectorPainter 根据传入的 atEnd 布尔值在起始态和结束态之间触发已定义好的 ObjectAnimator 动画,并不存在名为 animateTo 的方法。也就是说 Compose 这一层解决的是“少写一个 View 层的 ImageView + Drawable 桩代码”的问题,动画本身的定义(name 匹配、propertyName、pathData 结构一致性)仍然完全由 AVD XML 承担,前面提到的所有坑一个都逃不掉。
View 到 Compose 的桥接:最低成本的迁移
不想迁移已有 AVD 资源?直接用 DrawablePainter 桥接:
val drawable = remember {
AnimatedVectorDrawableCompat.create(context, R.drawable.avd_icon)
}
Image(
painter = rememberDrawablePainter(drawable),
contentDescription = null,
modifier = Modifier.clickable { drawable.start() }
)
这种方式把 View 体系的 Drawable 寄生在 Compose 中运行。invalidateSelf() 最终委托给 DrawablePainter 触发重组,路径闭环但多了一层桥接开销。对于存量项目是性价比最高的过渡方案。
选型思路
实际项目中,我的判断标准:
- 简单切换:用 Compose 的
AnimatedContent或Crossfade,不需要上向量动画 - 单一节点的连续变换(旋转、缩放、路径变形):AVD XML 方案成熟度最高,Vector Asset Studio 可以直接导入 SVG 生成模板
- Compose 优先的新项目:
AnimatedImageVector+rememberAnimatedVectorPainter够用,它来自androidx.compose.animation:animation-graphics这个独立 artifact,最低支持到 minSdk 21(与 Compose 本身的最低支持版本一致),不需要 API 33,动画定义仍然是背后那份 AVD XML - 低版本兼容:用 AppCompat 的
AnimatedVectorDrawableCompat,但 ProGuard 规则要保留android.support.graphics.drawable路径下的类,不然类名混淆后动画静默失效
理清了 AVD 的 ObjectAnimator 驱动模型之后,遇到”动画不生效”的问题,就知道从三个环节排查:name 是否匹配、propertyName 是否有对应 setter、pathData 命令结构是否一致。原理吃透了,排查效率自然就上来了。