图片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着色(tint)的兼容方案源码解析
打赏
更多原创博文请访问我的个人博客
原创不易,文章转载请注明出处。