一、问题背景
在 Android 开发中,矢量图(VectorDrawable)因其体积小、不失真的特性被广泛使用。通常,我们可以通过 DrawableCompat.setTint() 或 ColorFilter 快速修改 Drawable 的颜色:
Java
Drawable wrappedDrawable = DrawableCompat.wrap(drawable).mutate();
DrawableCompat.setTint(wrappedDrawable, color);
遇到的痛点: DrawableCompat.setTint 底层采用的是 PorterDuff 或 BlendMode 颜色混合机制,它会将整个 Vector 中的所有图形元素(包括填充 fillColor 和描边 strokeColor)强行覆盖为同一种单一颜色。
如果你遇到了以下需求:
-
项目中有大量的矢量图文件(例如 90+ 个 SVG/Vector)。
-
不可能逐个修改 XML 结构或改为自定义 View。
-
需要在代码中动态控制同一个 VectorDrawable 的 填充色(fillColor) 和 描边色(strokeColor),并且两者颜色/透明度不同。
二、踩坑历程与分析
1. 尝试直接反射修改 mColor 字段(变色失败)
通过分析 VectorDrawable 源码,可以发现内部将 <path> 节点解析成了 VFullPath 对象,并用 ComplexColor 存储颜色。
最直观的想法是通过 Java 反射获取 mVectorState -> mRootGroup -> mChildren,然后找到 VFullPath,直接修改 mFillColor 和 mStrokeColor 内部的 mColor 变量。
结果:修改后页面上的 VectorDrawable 完全不变色!
原因分析: 从 Android 8.0(API 26)开始,Android 系统为了优化 VectorDrawable 的绘制性能,将 Java 层的属性变动与 Native 层的 C++ 渲染管道(或 Shader 缓存)绑定在一起。仅仅通过反射去修改 Java 对象的成员变量,不会触发底层 Native 颜色的刷新以及缓存标记的清除,导致 View 重绘时依然渲染旧的颜色。
三、终极解决方案
经过对 VectorDrawable.VFullPath 隐藏 API 的深入分析,发现系统在内部其实提供了 setFillColor(int)、setStrokeColor(int) 和 setFillAlpha(float) 等私有方法。
通过反射优先调用这些 setter 方法,能够通知底层更新颜色状态并清除缓存;同时保留直接修改 Field 的代码作为低版本系统的保底方案。
1. 封装工具类 ReplaceColor.java
我们将该逻辑封装为一个通用的工具类,支持一键加载并分别修改 fillColor、fillAlpha 和 strokeColor:
Java
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.VectorDrawable;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
/**
* VectorDrawable 动态着色工具类
*/
public class ReplaceColor {
@Nullable
public static Drawable primaryColor(@NonNull Context context, @DrawableRes int drawableRes) {
int fillColor = ContextCompat.getColor(context, com.wingderm.tool.R.color.position_1);
int strokeColor = ContextCompat.getColor(context, com.wingderm.tool.R.color.position_1);
return getVectorDrawable(context, drawableRes, fillColor, 0.2f, strokeColor);
}
@Nullable
public static Drawable setColor(@NonNull Context context, @DrawableRes int drawableRes) {
int fillColor = ContextCompat.getColor(context, com.wingderm.tool.R.color.position);
int strokeColor = ContextCompat.getColor(context, com.wingderm.tool.R.color.position);
return getVectorDrawable(context, drawableRes, fillColor, 0.2f, strokeColor);
}
@Nullable
public static Drawable completeColor(@NonNull Context context, @DrawableRes int drawableRes) {
int fillColor = ContextCompat.getColor(context, com.wingderm.tool.R.color.green_1);
int strokeColor = ContextCompat.getColor(context, com.wingderm.tool.R.color.green_1);
return getVectorDrawable(context, drawableRes, fillColor, 0.2f, strokeColor);
}
@Nullable
public static Drawable getVectorDrawable(@NonNull Context context,
@DrawableRes int drawableRes,
@ColorInt int fillColor,
float fillAlpha,
@ColorInt int strokeColor) {
Drawable drawable = ContextCompat.getDrawable(context, drawableRes);
if (drawable == null) {
return null;
}
return setVectorColors(drawable, fillColor, fillAlpha, strokeColor);
}
/**
* 利用反射调用 VectorDrawable 内部 VFullPath 的 setter 方法分别更新填充色与描边色
*/
@NonNull
public static Drawable setVectorColors(@NonNull Drawable drawable,
@ColorInt int fillColor,
float fillAlpha,
@ColorInt int strokeColor) {
Drawable mutated = drawable.mutate();
if (!(mutated instanceof VectorDrawable)) {
return mutated;
}
try {
// 1. 获取 mVectorState
Object state = getFieldValue(mutated, "mVectorState");
if (state == null) return mutated;
// 2. 获取根节点 mRootGroup
Object root = getFieldValue(state, "mRootGroup");
if (root == null) return mutated;
// 3. 获取 mChildren 列表
Field childrenField = root.getClass().getDeclaredField("mChildren");
childrenField.setAccessible(true);
ArrayList<?> children = (ArrayList<?>) childrenField.get(root);
if (children == null) return mutated;
// 4. 遍历 Path 节点并更新颜色
for (Object child : children) {
if (child.getClass().getName().endsWith("VFullPath")) {
// 修改 fillColor (优先反射调用 setter)
if (!invokeMethod(child, "setFillColor", new Class[]{int.class}, fillColor)) {
setPathComplexColor(child, "mFillColor", fillColor);
}
// 修改 fillAlpha (0.0f ~ 1.0f)
if (!invokeMethod(child, "setFillAlpha", new Class[]{float.class}, fillAlpha)) {
setPathProperty(child, "mFillAlpha", fillAlpha);
}
// 修改 strokeColor (优先反射调用 setter)
if (!invokeMethod(child, "setStrokeColor", new Class[]{int.class}, strokeColor)) {
setPathComplexColor(child, "mStrokeColor", strokeColor);
}
}
}
mutated.invalidateSelf();
} catch (Exception e) {
e.printStackTrace();
}
return mutated;
}
private static boolean invokeMethod(Object target, String methodName, Class<?>[] paramTypes, Object... args) {
try {
Method method = target.getClass().getDeclaredMethod(methodName, paramTypes);
method.setAccessible(true);
method.invoke(target, args);
return true;
} catch (Exception e) {
return false;
}
}
private static void setPathComplexColor(Object target, String fieldName, int color) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Object complexColor = field.get(target);
if (complexColor != null) {
Field colorField = complexColor.getClass().getDeclaredField("mColor");
colorField.setAccessible(true);
colorField.setInt(complexColor, color);
}
}
private static void setPathProperty(Object target, String fieldName, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
private static Object getFieldValue(Object object, String fieldName) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
} catch (Exception e) {
return null;
}
}
}
四、使用方法
在需要设置图标颜色的逻辑中,直接传入目标 Vector 资源即可:
Java
// 获取并应用 Primary 状态颜色
Drawable drawable = ReplaceColor.primaryColor(context, R.drawable.ic_position_1);
imageView.setImageDrawable(drawable);
// 如果需要对某个 Vector 灵活定制不同的 fillColor 和 strokeColor
Drawable customDrawable = ReplaceColor.getVectorDrawable(
context,
R.drawable.ic_position_1,
Color.RED, // fillColor 设为红色
0.5f, // fillAlpha 透明度设为 50%
Color.BLUE // strokeColor 设为蓝色
);
imageView.setImageDrawable(customDrawable);
五、总结
-
DrawableCompat.setTint适合将整个 Drawable 渲染为统一颜色的场景。 -
海量 VectorDrawable 资源 下,为了避免重构 XML 或编写自定义 View,通过反射调用
VFullPath隐藏的setFillColor/setStrokeColor方法 是最轻量、高兼容性的方案。 -
一定要记得调用
drawable.mutate(),避免修改共享状态影响到其他使用同一 Drawable 资源的 ImageView。