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

一、问题背景

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

Java

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

遇到的痛点: DrawableCompat.setTint 底层采用的是 PorterDuff 或 BlendMode 颜色混合机制,它会将整个 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,直接修改 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);

五、总结

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

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

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

相关推荐
我命由我12345几秒前
Android 广播 - 组件通信
android·java·开发语言·android studio·android jetpack·android-studio·android runtime
阿俊-全栈开发1 分钟前
LikeShop 单商户标准版商品管理二开:分类、SKU 规格与库存联动实现
android·likeshop·likeshop开源商城
用户2181697049303 小时前
Kotlin 语法二
android
妖星3 小时前
android 源码分析surfaceFinger是如何通过设置属性启动bootanim
android
JoyMonitor3 小时前
APK mmap 内存增长定位:pagemap 与 DEX 结构联合定位方法
android
有什么事3 小时前
永久虚拟免费云手机能实现吗,本地安卓容器与云端托管的技术边界
android·智能手机
妖星3 小时前
Android 源码 .rc 语法详解
android
JMchen3 小时前
实战项目一——实现一个高性能图片滤镜App
android·c++·性能优化
白帽攻防录4 小时前
SRC 挖洞:Android 蓝牙预认证 RCE 深度复盘,CVE-2026-55330 怎么在蓝牙范围内零交互攻破手机
android·网络安全·智能手机·src漏洞挖掘