【Android】批量 VectorDrawable 动态分别修改 fillColor 和 strokeColor 的踩坑与终极解决方案

一、问题背景

在 Android 开发中,矢量图(VectorDrawable)因其体积小、不失真的特性被广泛使用。通常,我们可以通过 DrawableCompat.setTint()ColorFilter 快速修改 Drawable 的颜色:

Java

复制代码
Drawable wrappedDrawable = DrawableCompat.wrap(drawable).mutate();
DrawableCompat.setTint(wrappedDrawable, color);

遇到的痛点: DrawableCompat.setTint 底层采用的是 PorterDuffBlendMode 颜色混合机制,它会将整个 Vector 中的所有图形元素(包括填充 fillColor 和描边 strokeColor)强行覆盖为同一种单一颜色

如果你遇到了以下需求:

  1. 项目中有大量的矢量图文件(例如 90+ 个 SVG/Vector)。

  2. 不可能逐个修改 XML 结构或改为自定义 View。

  3. 需要在代码中动态控制同一个 VectorDrawable 的 填充色(fillColor)描边色(strokeColor),并且两者颜色/透明度不同。

二、踩坑历程与分析

1. 尝试直接反射修改 mColor 字段(变色失败)

通过分析 VectorDrawable 源码,可以发现内部将 <path> 节点解析成了 VFullPath 对象,并用 ComplexColor 存储颜色。

最直观的想法是通过 Java 反射获取 mVectorState -> mRootGroup -> mChildren,然后找到 VFullPath,直接修改 mFillColormStrokeColor 内部的 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

我们将该逻辑封装为一个通用的工具类,支持一键加载并分别修改 fillColorfillAlphastrokeColor

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);

五、总结

  1. DrawableCompat.setTint 适合将整个 Drawable 渲染为统一颜色的场景。

  2. 海量 VectorDrawable 资源 下,为了避免重构 XML 或编写自定义 View,通过反射调用 VFullPath 隐藏的 setFillColor / setStrokeColor 方法 是最轻量、高兼容性的方案。

  3. 一定要记得调用 drawable.mutate(),避免修改共享状态影响到其他使用同一 Drawable 资源的 ImageView。

相关推荐
齐天qaq2 小时前
Android 系统 APK 分区与权限
android
心平气和量大福大2 小时前
android-控件-多选框CheckBox
android·gitee
pengyu3 小时前
【Kotlin 协程修仙录 · 大乘境 · 初阶】 | 跳出三界:协程在 KMP 与后端开发中的跨平台之道
android·kotlin
XiaoLeisj3 小时前
Android Memory Profiler:堆内存指标、内存抖动与泄漏定位
android·性能优化·内存抖动·内存泄露·memory profiler
未来猫咪花3 小时前
Everything is ViewModel:让状态管理回到对象世界
android·flutter·ios
古法安卓3 小时前
Android-重启流程源码解析
android·java·android studio
协议的旁观者5 小时前
Android 高级逆向实战(一):对抗 360 企业加固,Native 抽取还原与 Activity 生命周期重建
android
mmsx7 小时前
osmdroid 屏幕坐标与测量坐标互转:中心点+比例尺仿射换算
android·源码·地图·osmdroid