【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。

相关推荐
千里马学框架13 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台13 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone14 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc14 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo16 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770016 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼17 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone18 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen18 小时前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone18 小时前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui