图片Drawable完美着色方案及源码分析

莫看江面平如镜,要看水底万丈深。

前言

最近开发过程中有个需求是不同的角色使用不同的颜色的图片,如果使用传统方案,需要UI切很多张不同颜色的相同形状的图,并根据不同的角色在它们之间切换,代码的巨大开销以及apk大小的增加。于是便思考有没有更好的解决方案——通过只改变图片颜色的方式。

DrawableCompat

Google 在v4支持库中引入了DrawableCompat类,它介绍了前Lollipop设备的色彩功能。它有一个完整的API,甚至支持色调列表和镜像的RTL布局,但它是一个重量级的用例,我们还必须包装Drawable与wrap()方法才能使用它。

DrawableUtils

以下是用Kotlin编写的DrawableUtils.kt

通过传入Drawable或资源id和颜色,即可生成相应颜色的图片。


//给drawable着色
fun tintDrawable(drawable: Drawable?, colors: ColorStateList): Drawable? {
    if (drawable == null)return null
    val wrappedDrawable = DrawableCompat.wrap(drawable)
    DrawableCompat.setTintList(wrappedDrawable, colors)
    return wrappedDrawable
}
//给drawable着色
fun tintDrawable(drawableId: Int, colors: ColorStateList): Drawable? {
    return tintDrawable(getDrawable(drawableId), colors)
}

使用效果如下:

这里写图片描述

优化与扩展

mutate方法

使用中我发现一个问题,如果我对一个相同的图片赋值两次颜色后,实际生成的图片只有一种颜色,例如:

这里写图片描述

这是什么原因呢?等会我们在源码解析中再详细解释,先看看如何解决:

Drawable提供了一个mutate()方法,我们可以对生成后的Drawable调用mutate()方法来解决这个问题。


//给drawable着色
fun tintDrawable(drawable: Drawable?, colors: ColorStateList): Drawable? {
    if (drawable == null)return null
    val wrappedDrawable = DrawableCompat.wrap(drawable)
    DrawableCompat.setTintList(wrappedDrawable, colors)
    //*****变化在这里*****
    wrappedDrawable.mutate()   
    return wrappedDrawable
}
//给drawable着色
fun tintDrawable(drawableId: Int, colors: ColorStateList): Drawable? {
    return tintDrawable(getDrawable(drawableId), colors)
}

扩展

通过上面封装的方法可以达到变色的基本需求。但实际使用中有更多的使用场景,这种基础方案可能无法满足我们的需求。我们可以利用ColorStateList继续进行扩展:

  • 我们可以使用 ColorStateList 根据 View 的状态着色成不同的颜色:

<!- color_selector.xml ->
<?xml version="1.0" encoding="utf-8"?>  
<selector xmlns:android="http://schemas.android.com/apk/res/android">  
    <item android:color="@color/red" android:state_focused="true" />
    <item android:color="@color/gray" />
</selector> 

然后这样使用:


tintDrawable(drawable, getResources().getColorStateList(R.color.color_selector))

  • 利用StateListDrawable直接封装成代码版图片selector

 val stateListDrawable = StateListDrawable()
 stateListDrawable.addState(intArrayOf(android.R.attr.state_selected), selectedDrawable)//有状态的必须写在上面
 stateListDrawable.addState(intArrayOf(), normalDrawable)//没有状态的必须写在下面
 tintDrawable(stateListDrawable, colors)

完整代码


/**
 * ================================================
 * 作    者:JayGoo
 * 版    本:
 * 创建日期:2017/8/22
 * 描    述: Drawable变色工具类
 * ================================================
 */
//给drawable着色
fun tintDrawable(drawable: Drawable?, colors: ColorStateList): Drawable? {
    if (drawable == null)return null
    val wrappedDrawable = DrawableCompat.wrap(drawable)
    DrawableCompat.setTintList(wrappedDrawable, colors)
    wrappedDrawable.mutate()
    return wrappedDrawable
}
//给drawable着色
fun tintDrawable(drawableId: Int, colors: ColorStateList): Drawable? {
    return tintDrawable(getDrawable(drawableId), colors)
}
//生成Selected的Selector
fun createSelectorByStateSelected(selectedDrawable: Drawable?, normalDrawable: Drawable?): StateListDrawable? {
    if (selectedDrawable == null || normalDrawable == null)return null
    val stateListDrawable = StateListDrawable()
    stateListDrawable.addState(intArrayOf(android.R.attr.state_selected), selectedDrawable)//有状态的必须写在上面
    stateListDrawable.addState(intArrayOf(), normalDrawable)//没有状态的必须写在下面
    return stateListDrawable
}
//根据drawableId获取Drawable
fun getDrawable(drawableId: Int): Drawable? {
    return ResourcesCompat.getDrawable(SMApp.getInstance().resources, drawableId, null)
}

DrawableCompat源码解析

俗话说得好,要知其然知其所以然。工具类代码简单总结起来是以下三步:

  • DrawableCompat.wrap(drawable)

  • DrawableCompat.setTintList(wrappedDrawable, colors)

  • wrappedDrawable.mutate()

那么接下来请跟随我一起研究研究这三步都干了什么:

wrap()

首先先看下DrawableCompat.wrap()的源码:


public static Drawable wrap(@NonNull Drawable drawable) {
        return IMPL.wrap(drawable);
    }

很简单,就一句代码,那么IMPL它到底是个什么鬼呢?


   /**
     * Select the correct implementation to use for the current platform.
     */
    static final DrawableCompatBaseImpl IMPL;
    static {
        final int version = android.os.Build.VERSION.SDK_INT;
        if (version >= 23) {
            IMPL = new DrawableCompatApi23Impl();
        } else if (version >= 21) {
            IMPL = new DrawableCompatApi21Impl();
        } else if (version >= 19) {
            IMPL = new DrawableCompatApi19Impl();
        } else if (version >= 17) {
            IMPL = new DrawableCompatApi17Impl();
        } else {
            IMPL = new DrawableCompatBaseImpl();
        }
    }

很明显,这是根据不同的 API Level 选择不同的实现类.再往下看一点,我们发现DrawableCompatApi23Impl继承于DrawableCompatApi21Impl,然后一直向下继承直至DrawableCompatBaseImpl。那么我们就从DrawableCompatApi23Impl一直往下看wrap在各个版本的Impl中都干了什么:


    @RequiresApi(23)
    static class DrawableCompatApi23Impl extends DrawableCompatApi21Impl {
         ...
        @Override
        public Drawable wrap(Drawable drawable) {
            // No need to wrap on M+
            //直接返回原始Drawable实例
            return drawable;
        }
       ...
    }
    @RequiresApi(21)
    static class DrawableCompatApi21Impl extends DrawableCompatApi19Impl {
        ...
        @Override
        public Drawable wrap(Drawable drawable) {
            //当前传入的Drawable实例不属于TintAwareDrawable
            if (!(drawable instanceof TintAwareDrawable)) {
                return new DrawableWrapperApi21(drawable);
            }
            return drawable;
        }
        ...
    }
     @RequiresApi(19)
    static class DrawableCompatApi19Impl extends DrawableCompatApi17Impl {
        ...
        @Override
        public Drawable wrap(Drawable drawable) {
            //当前传入的Drawable实例不属于TintAwareDrawable
            if (!(drawable instanceof TintAwareDrawable)) {
                return new DrawableWrapperApi19(drawable);
            }
            return drawable;
        }
        ...
    }
    //DrawableCompatApi17Impl使用的是DrawableCompatBaseImpl的wrap()
    static class DrawableCompatBaseImpl {
        ...
        public Drawable wrap(Drawable drawable) {
            //当前传入的Drawable实例不属于TintAwareDrawable
            if (!(drawable instanceof TintAwareDrawable)) {
                return new DrawableWrapperApi14(drawable);
            }
            return drawable;
        }
        ...
    }

从上面的源码很清晰的看出来wrap()方法在各个版本的Impl的实现,API大于等于23时,直接返回原来的drawable,API大于等于21时,生成一个DrawableWrapperApi21实例,API大于等于19时,生成一个DrawableWrapperApi19实例,API小于19时,生成一个DrawableWrapperApi14实例。接下来,我们一起看看这三个类的构造方法,了解它们都做了什么:


@RequiresApi(21)
class DrawableWrapperApi21 extends DrawableWrapperApi19 {
    DrawableWrapperApi21(Drawable drawable) {
        super(drawable);
    }
}
@RequiresApi(19)
class DrawableWrapperApi19 extends DrawableWrapperApi14 {
    DrawableWrapperApi19(Drawable drawable) {
        super(drawable);
}
@RequiresApi(14)
class DrawableWrapperApi14 extends Drawable
        implements Drawable.Callback, DrawableWrapper, TintAwareDrawable {
   /**
     * Creates a new wrapper around the specified drawable.
     *
     * @param dr the drawable to wrap
     */
    DrawableWrapperApi14(@Nullable Drawable dr) {
        mState = mutateConstantState();
        // Now set the drawable...
        setWrappedDrawable(dr);
    }
}

从他们的构造方法看来并没有太大的差别,都是使用了DrawableWrapperApi14的方法,只是扩展了一些方法。那么我们继续向下探究mutateConstantState()setWrappedDrawable()方法,看看到底干了什么。

首先是mutateConstantState()方法,让我们一直向下寻根究底:


//返回一个新的constant state
DrawableWrapperState mutateConstantState() {
        return new DrawableWrapperStateBase(mState, null);
}
//继承父类DrawableWrapperState的构造方法
DrawableWrapperStateBase(@Nullable DrawableWrapperState orig, @Nullable Resources res) {
            super(orig, res);
}
//初始化一些属性值
 DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) {
            if (orig != null) {
                mChangingConfigurations = orig.mChangingConfigurations;
                mDrawableState = orig.mDrawableState;
                mTint = orig.mTint;
                mTintMode = orig.mTintMode;
            }
}

一路跟踪下来发现mutateConstantState()其实是生成一个新的默认的状态值DrawableWrapperState给drawable。

接下来再寻根究底的看看setWrappedDrawable()都干了什么:


   /**
     * Sets the current wrapped {@link Drawable}
     */
    public final void setWrappedDrawable(Drawable dr) {
        if (mDrawable != null) {
            mDrawable.setCallback(null);
        }
        mDrawable = dr;
        if (dr != null) {
            dr.setCallback(this);
            // Only call setters for data that's stored in the base Drawable.
            setVisible(dr.isVisible(), true);
            setState(dr.getState());
            setLevel(dr.getLevel());
            setBounds(dr.getBounds());
            if (mState != null) {
                mState.mDrawableState = dr.getConstantState();
            }
        }
        invalidateSelf();
    }

这里涉及到了几个方法,它们的作用分别是:

  • setVisible(boolean visible, boolean restart)用于控制Drawable实例是否执行动画,对于AnimationDrawable实例会产生效果,控制是否执行动画

  • setState(final int[] stateSet)由于DrawableWrapperState构造函数中初始化的值,其实这个函数是为了移除与Drawable实例关联的ColorFilter

  • setLevel直接使用的是其父类Drawable中的方法,由于DrawableWrapperState构造函数中初始化的值导致其实这一步什么都没有干。

  • setBounds为新产生的DrawableWrapperApi14实例设置其绘制范围与原始Drawable实例一致

总结:DrawableCompat.wrap(@NonNull Drawable drawable)在SDK版本 >= 23时直接返回了原来的Drawable实例,其他情况返回了DrawableWrapperApi14的一个新实例,为其生成一个DrawableWrapperState,并且移除了该新实例关联过的ColorFilter,将该新实例的绘制范围和原始Drawable实例保持一致。

setTintList

接下来再寻根究底的看看setTintList()都干了什么:


 public static void setTintList(@NonNull Drawable drawable, @Nullable ColorStateList tint) {
        IMPL.setTintList(drawable, tint);
}
  @RequiresApi(23)
    static class DrawableCompatApi23Impl extends DrawableCompatApi21Impl {
         ...
        //未单独实现,使用的是父类的setTintList
       ...
    }
    @RequiresApi(21)
    static class DrawableCompatApi21Impl extends DrawableCompatApi19Impl {
        ...
         @Override
        public void setTintList(Drawable drawable, ColorStateList tint) {
              //使用Drawable原生的setTintList
            drawable.setTintList(tint);
        }
        ...
    }
    //DrawableCompatApi19Impl、DrawableCompatApi17Impl
    //使用的是DrawableCompatBaseImpl的setTintList()
    static class DrawableCompatBaseImpl {
        ...
         public void setTintList(Drawable drawable, ColorStateList tint) {
            if (drawable instanceof TintAwareDrawable) {
                ((TintAwareDrawable) drawable).setTintList(tint);
            }
        }        
        ...
    }

从源码可以看出 API>=21时使用的是Drawable的setTintList,当API<21时使用的是TintAwareDrawable的setTintList

TintAwareDrawable.setTintList()


  @Override
    public void setTintList(ColorStateList tint) {
        /** 
         *  1:在上面分析DrawableCompat.wrap时候,mState的值如下:
         *  mState(DrawableWrapperState)中,下面成员变量的值都是默认值:
         *  int mChangingConfigurations;
         *  Drawable.ConstantState mDrawableState;
         *  ColorStateList mTint = null;
         *  PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;
         *
         *  2:在DrawableCompat.setTint时候,mState.mTint不再为空值
         */
        mState.mTint = tint;
        updateTint(getState());
    }
    /**
     * 之前DrawableCompat.wrap已经执行过一次updateTint,
     * 现在DrawableCompat.setTint第二次执行!!
     */
    private boolean updateTint(int[] state) {
        //isCompatTintEnabled()返回true
        if (!isCompatTintEnabled()) {
            // If compat tinting is not enabled, fail fast
            return false;
        }
        //此时mState.mTint已经在setTintList中赋值不为null
        final ColorStateList tintList = mState.mTint;
        //mState.mTintMode依然为默认值不为null
        final PorterDuff.Mode tintMode = mState.mTintMode;
        if (tintList != null && tintMode != null) {
            //两者都不为空,因而执行if条件下代码
            //获取当前状态下对应的颜色
            final int color = tintList.getColorForState(state, tintList.getDefaultColor());
            /**
             *  mColorFilterSet默认是false
             *  color即为setTint时候传入的颜色
             *  mCurrentColor默认值是0
             *  tintMode是mState中的mTintMode=DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN
             *  mCurrentMode默认值是null
             */
            if (!mColorFilterSet || color != mCurrentColor || tintMode != mCurrentMode) {
                //对Drawable实例产生着色的,本质上还是执行了Drawable中的setColorFilter方法。
                setColorFilter(color, tintMode);
                mCurrentColor = color;
                mCurrentMode = tintMode;
                mColorFilterSet = true;
                return true;
            }
        } else {
            mColorFilterSet = false;
            clearColorFilter();
        }
        return false;
    }

Drawable.setTintList()


public void setTintList(@Nullable ColorStateList tint) {
    //你没有看错,竟然是个空方法!!!!
}

原因是我们在获取Drawable原始实例的时,获取的其实是Drawable的子类实例,在Drawable子类里对setTintList做了重写。

mutate


   /**
     * Make this drawable mutable. This operation cannot be reversed. A mutable
     * drawable is guaranteed to not share its state with any other drawable.
     * This is especially useful when you need to modify properties of drawables
     * loaded from resources. By default, all drawables instances loaded from
     * the same resource share a common state; if you modify the state of one
     * instance, all the other instances will receive the same modification.
     *
     * Calling this method on a mutable Drawable will have no effect.
     *
     * @return This drawable.
     * @see ConstantState
     * @see #getConstantState()
     */
    public @NonNull Drawable mutate() {
        return this;
    }

单纯看源码解释可能比较抽象,说的通俗一点,我们通过Resource获取drawable文件夹下的一张资源图片,在获取Drawable初始实例时候如果不使用mutate(),那么我们对这个Drawable进行着色,不仅改变了当前Drawable实例的颜色,以后任何通过这个图片获取到的Drawable实例,都会具有之前设置的颜色。所以如果我们对一张资源图片的着色不是APP全局生效的,就需要使用mutate()

Android为了优化系统性能,同一张资源图片生成的Drawable实例在内存中只存在一份,在不使用mutate的情况下,修改任意Drawable都会全局发生变化。使用mutate(),Android系统也没有把Drawable实例又单独拷贝一份,仅仅是单独存放了状态值,很小的一部分数据,Drawable实例在内存中仍然保持1份,因而并不会影响系统的性能。

下面用两张图更能形象的描述这一过程:

  • 不使用mutate:

这里写图片描述

  • 使用mutate后:

这里写图片描述

参考

Drawable 着色的后向兼容方案

其实你不懂:Drawable着色(tint)的兼容方案源码解析

打赏

更多原创博文请访问我的个人博客

原创不易,文章转载请注明出处。