Android 图片着色 Tint 详解
问题描述
在app中可能存在一张图片只是因为颜色的不同而引入了多张图片资源的情况。比如 一张右箭头的图片,有白色、灰色和黑色三种图片资源存在。所以我们可不可以只保留一张基础图片,在此图片基础上只是颜色改变的情况是否可以通过代码设置来动态修改呢?
知识点概览: 
1. setTint、setTintList :对drawable 进行着色。 
2. DrawableCompat.wrap: 对drawable 进行包装,使其可以在不同版本中设置着色生效。 
3. drawable.mutate(): 使drawable 可变,打破其共享资源模式。 
4. ConstantState :① 享元模式。② 保存资源信息。③可通过自己创建新的drawable 对象。
初识tint
为了兼容android 的不同版本,google 在DrawableCompat API中提供了着色的相关方法。 
setTint、setTintList
先构造好我们的测试demo。提供一个工具类用于对Drawable 进行着色。 
(注:为了测试对低版本的兼容,这里使用的测试机型为三星 galaxy s4 android版本为4.4.2)
public class SkxDrawableHelper {
    /**
     * 对目标Drawable 进行着色
     *
     * @param drawable 目标Drawable
     * @param color    着色的颜色值
     * @return 着色处理后的Drawable
     */
    public static Drawable tintDrawable(@NonNull Drawable drawable, int color) {
        Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
        DrawableCompat.setTint(wrappedDrawable, color);
        return wrappedDrawable;
    }
    /**
     * 对目标Drawable 进行着色
     *
     * @param drawable 目标Drawable
     * @param colors   着色值
     * @return 着色处理后的Drawable
     */
    public static Drawable tintListDrawable(@NonNull Drawable drawable, ColorStateList colors) {
        Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
        // 进行着色
        DrawableCompat.setTintList(wrappedDrawable, colors);
        return wrappedDrawable;
    }
}测试代码: 
调用此方法对Deawable进行着色。我们分别对设置背景的Drawable着色 #30c3a6,  图片着色为#ff4081
Drawable originBitmapDrawable = ContextCompat.getDrawable(this,
                R.drawable.icon_beijing);
mImageView1.setBackground(
                SkxDrawableHelper.tintDrawable(originBitmapDrawable,
                        Color.parseColor("#30c3a6")));
mImageView2.setImageDrawable(
                SkxDrawableHelper.tintDrawable(originBitmapDrawable,
                        Color.parseColor("#ff4081")));没有进行着色处理的原效果: 
进行着色后的效果如下: 
一脸懵逼,这都什么跟什么啊?!!! 我只修改了下面的两个ImageView,并没有对上面的两个ImageView进行修改啊。而且 图4是怎么出来那么个畸形的。 
好吧,一步步来!
DrawableCompat wrap
这里简单介绍下wrap 这个方法。这个方法的作用是对目标Drawable进行包装,它可以用于跨越不同的API级别,通过在这个类中的着色方法,简单来说就是为了兼容不同的版本。如果想对Drawable 进行着色就必须调用此方法。
* Drawable bg = DrawableCompat.wrap(view.getBackground());
* // Need to set the background with the wrapped drawable
* view.setBackground(bg);
*
* // You can now tint the drawable
* DrawableCompat.setTint(bg, ...);与wrap 方法对应的有 unwrap(@NonNull Drawable drawable) 方法,用于解除对目标Drawable的包装。
ConstantState 享元模式
为什么会出现上面出现的这种情况呢? 
这里简单解释下。不同的Drawble如果加载的是同一个资源,那么将拥有共同的状态,这是google对Drawable
做的内存优化。在Drawable 中的表现为 ConstantState,ConstantState是抽象静态内部类,Drawable
的子类如ColorDrawble,BitmapDrawable 也分别都进行了不同的实现。而在ConstantState
内部类中保存的就是Drawable 需要展示的信息,在ColorDrawable 中ConstantState
的实现类是ColorState,其中包含了一些颜色信息;在BitmapDrawable
中ConstantState的实现类是BitmapState,其中包含了Paint,Bitmap,ColorStateList等一些属性,不同的Drawable子类依靠其对应的ConstantState实现类来刷新渲染视图。默认情况下,从同一资源加载的所有drawables实例都共享一个公共状态,如果修改一个实例的状态,所有其他实例将接收相同的修改。
我们从ContextCompat类获取Drawable 方法一步步往下看android 是如何实现Drawable共享的。 
ContextCompat.java
    public static final Drawable getDrawable(Context context, int id) {
        final int version = Build.VERSION.SDK_INT;
        if (version >= 21) {
            return ContextCompatApi21.getDrawable(context, id);
        } else {
            return context.getResources().getDrawable(id);
        }
    }Resources.java
  public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
        TypedValue value;
        ......
        //  从这里继续跟进去,这是加载Drawable的方法
        final Drawable res = loadDrawable(value, id, theme);
        synchronized (mAccessLock) {
            if (mTmpValue == null) {
                mTmpValue = value;
            }
        }
        return res;
    }   @Nullable
   Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
        ......
        final boolean isColorDrawable;
        // Drawable 的资源缓存类
        final DrawableCache caches;
        // 缓存的key
        final long key;
        ......
        // 这里先判断是否加载过,如果已经加载过就去缓存里面去取,如果成功从缓存中取到就返回。
        if (!mPreloading) {
            final Drawable cachedDrawable = caches.getInstance(key, theme);
            if (cachedDrawable != null) {
                return cachedDrawable;
            }
        }
        // 缓存中没有,则根据ConstantState 来创建新的Drawable
        final ConstantState cs;
        if (isColorDrawable) {
            cs = sPreloadedColorDrawables.get(key);
        } else {
            cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
        }
        Drawable dr;
        if (cs != null) {
            dr = cs.newDrawable(this);
        } else if (isColorDrawable) {
            dr = new ColorDrawable(value.data);
        } else {
            dr = loadDrawableForCookie(value, id, null);
        }
        ......
        // 缓存Drawable
        if (dr != null) {
            dr.setChangingConfigurations(value.changingConfigurations);
            cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
        }
        return dr;
    }可以看下cacheDrawable 这个方法,虽然从名字上理解是缓存Drawable,但其实是缓存的Drawable对应的ConstantState 。
  private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
            Theme theme, boolean usesTheme, long key, Drawable dr) {
        final ConstantState cs = dr.getConstantState();
        ......
        // 缓存ConstantState
        caches.put(key, theme, cs, usesTheme);
        ......
        }
    }DrawableCache.java
public Drawable getInstance(long key, Resources.Theme theme) {
        // 注意这里,从缓存中取出来的是 ConstantState
        final Drawable.ConstantState entry = get(key, theme);
        if (entry != null) {
            return entry.newDrawable(mResources, theme);
        }
        return null;
    }跟到这里心里大概也有谱了,原来android 不是共享的Drawable ,而是共享的内部类 ConstantState,ConstantState 中才是保存相关信息的。所以也就会出现如果修改了资源的某一个项信息,引用相同资源的其他Drawable 也就一同变化。这会儿我们看下面的这张图也就不难理解了! 
而如果要实现对同一个Drawable进行不同着色就必须要打破这种共享状态。使之成为下图所展示的状态。 
 
那么如何才能打破这种状态呢?
mutate() 使Drawable可变
上面说到如果要实现对同一个Drawable进行不同着色就必须要打破这种共享状态。默认情况下,从同一资源加载的所有drawables实例都共享一个公共状态;
 如果修改一个实例的状态,所有其他实例将接收相同的修改。而mutate() 方法就是使drawable 可变,
一个可变的drawable不与任何其他drawable共享它的状态,这样如果只修改可变drawable的属性就不会影响到其他与它加载同一个资源的drawable。
那么为mutate方法是如何打破共享状态呢? 
Drawable 是抽象类,同时mutate()返回的是this,我们以BitmapDrawable 为例,看下mutate() 这个方法。
 /**
     * A mutable BitmapDrawable still shares its Bitmap with any other Drawable
     * that comes from the same resource.
     *
     * @return This drawable.
     */
    @Override
    public Drawable mutate() {
        /*
          mMutated 是个标签,用来保证mutate只会设置一次,也就解释了在Drawable中对mutate()
          方法的一个解释,Calling this method on a mutable Drawable will have no
          effect(在已经可变的drawable上调用此方法无效),因为返回的还是自身
        */
        if (!mMutated && super.mutate() == this) {
            // 重新引用了一个新的状态对象
            mBitmapState = new BitmapState(mBitmapState);
            mMutated = true;
        }
        return this;
    }而BitmapState(BitmapState bitmapState) 这个构造方法是对自己的属性重新进行了赋值。这样就相当于不再引用共享的公共状态了,重新指向了一个新的状态。
ok,修改我们的工具类重新看下效果。
  /**
     * 对目标Drawable 进行着色
     *
     * @param drawable 目标Drawable
     * @param color    着色的颜色值
     * @return 着色处理后的Drawable
     */
    public static Drawable tintDrawable(@NonNull Drawable drawable, int color) {
        Drawable wrappedDrawable = DrawableCompat.wrap(drawable).mutate();
        DrawableCompat.setTint(wrappedDrawable, color);
        return wrappedDrawable;
    }效果: 
又是一脸懵逼中,怎么还是不对。虽然上面的两个ImageView 显示ok 了,但为下面的两个ImageVIew显示还是不对啊?奇了怪了!
猜想1:文档中有这么一句介绍 “Calling this method on a mutable Drawable will have
no effect.”在可变的Drawable 上调用此方法无效。所以我猜想会不会因为目标Drawable 已经可变的了,但是因为
warp()方法是对同一个Drawable 对象做的包装,如果已经调用过mutate()方法了,那么再次调用mutate
方法无效,对Drawable的最后一次修改覆盖了之前的修改。猜想来源于俩个现象,1.上面的两个ImageView
没有受影响,显示的是正确的;2.后修改的红色生效,而原本应该显示绿色ImageView 却显示成了红色。
猜想2:以BitmapDrawable 为例,在BitmapDrawable 的mutate 方法中有这么一句描述:“A mutable
BitmapDrawable still shares its Bitmap with any other Drawable that
comes from the same resource.”
那么经过wrap 处理过的drawable 是否还是原来的drawable呢? 
打印  DrawableCompat.wrap(drawable).toString()
发现两次得到的结果是不一样的,也就是说传入的和包装后的不是同一个对象。但是我用小米5 android版本是7.0
得到的结果又是一样的,即传入的和包装后的是同一个对象。
测试机型为小米5 系统版本为7.0。出现的效果和三星Galaxy s4 是一样。
Log.e("drawable", drawable.toString());
Log.e("wrap", DrawableCompat.wrap(drawable).toString());
02-07 21:41:36.557 24675-24675/com.skx.tomike E/drawable:
                        android.graphics.drawable.BitmapDrawable@12bc2f1
02-07 21:41:36.557 24675-24675/com.skx.tomike E/wrap:
                        android.graphics.drawable.BitmapDrawable@12bc2f1
02-07 21:41:36.558 24675-24675/com.skx.tomike E/drawable:
                        android.graphics.drawable.BitmapDrawable@12bc2f1
02-07 21:41:36.558 24675-24675/com.skx.tomike E/wrap:
                        android.graphics.drawable.BitmapDrawable@12bc2f1通过查看代码中也得到了相应的答案。
DrawableCompat.java
version >= 23
static class MDrawableImpl extends LollipopDrawableImpl {
        @Override
        public void setLayoutDirection(Drawable drawable, int layoutDirection) {
            DrawableCompatApi23.setLayoutDirection(drawable, layoutDirection);
        }
        @Override
        public int getLayoutDirection(Drawable drawable) {
            return DrawableCompatApi23.getLayoutDirection(drawable);
        }
        @Override
        public Drawable wrap(Drawable drawable) {
            // No need to wrap on M+  M以上版本不需要包装,直接返回drawable
            return drawable;
        }
    }
version >= 19
static class KitKatDrawableImpl extends JellybeanMr1DrawableImpl {
        @Override
        public void setAutoMirrored(Drawable drawable, boolean mirrored) {
            DrawableCompatKitKat.setAutoMirrored(drawable, mirrored);
        }
        @Override
        public boolean isAutoMirrored(Drawable drawable) {
            return DrawableCompatKitKat.isAutoMirrored(drawable);
        }
        @Override
        public Drawable wrap(Drawable drawable) {
            // 这里是new 出来的新对象。
            return DrawableCompatKitKat.wrapForTinting(drawable);
        }
        @Override
        public int getAlpha(Drawable drawable) {
            return DrawableCompatKitKat.getAlpha(drawable);
        }
    }这里我摘出来两个来进行对比。当api版本>=23 时,wrap 方法返回是传入的drawable。当api版本>=19 && <21 时,warp方法返回的是DrawableCompatKitKat.wrapForTinting(drawable)。这也就解释了为什么api版本不同,返回的结果不同了。
在高版本上(api>23)也就验证了猜想1是正确的,因为前后两次着色都是针对同一个drawable对象,而mutate 方法又只会生效一次,所以第二次的设置就理所应当的覆盖了第一次的设置,那么表现出来的结果就应该都是后面设置的颜色。
但是对于低版本就不太清楚为什么了,对drawable 进行包装后得到的两个不同的对象,既然是不同的对象,而且还都进行了mutate()设置为什么还是会表现出一样呢?这里做个记录!
针对猜想1我们做个简单试验。如果只是因为引用的是同一个Drawable对象的话,那我们只需要引用不同的Drawable 对象就OK了。 
这样做下简单修改:
Drawable originBitmapDrawable = ContextCompat.getDrawable(this,
        R.drawable.icon_beijing);
mImageView1.setBackground(
        SkxDrawableHelper.tintDrawable(originBitmapDrawable,
            Color.parseColor("#30c3a6")));
Drawable originBitmapDrawable2 = ContextCompat.getDrawable(this,
        R.drawable.icon_beijing);
mImageView2.setImageDrawable(
        SkxDrawableHelper.tintDrawable(originBitmapDrawable2,
            Color.parseColor("#ff4081")));
效果: 
对了?还是很懵,还有好多想不通的地方!还是要多翻源码啊。
Drawable getConstantState()
返回一个持有此Drawable的共享状态的ConstantState实例。而ConstantState类中也提供了方法来创建Drawable,在上面的部分我们也见到过。
newDrawable:从当前共享状态来创建一个drawable 实例。
这样的话我们就可以通过 getConstantState() 方法来获取drawable 所持有的共享状态的ConstantState,然后通过 newDrawable 方法来获取相应的drawable实例。
Android Tint工具类
public class SkxDrawableHelper {
    /**
     * 对目标Drawable 进行着色
     *
     * @param drawable 目标Drawable
     * @param color    着色的颜色值
     * @return 着色处理后的Drawable
     */
    public static Drawable tintDrawable(@NonNull Drawable drawable, int color) {
        // 获取此drawable的共享状态实例
        Drawable wrappedDrawable = getCanTintDrawable(drawable);
        // 进行着色
        DrawableCompat.setTint(wrappedDrawable, color);
        return wrappedDrawable;
    }
    /**
     * 对目标Drawable 进行着色。
     * 通过ColorStateList 指定单一颜色
     *
     * @param drawable 目标Drawable
     * @param color    着色值
     * @return 着色处理后的Drawable
     */
    public static Drawable tintListDrawable(@NonNull Drawable drawable, int color) {
        return tintListDrawable(drawable, ColorStateList.valueOf(color));
    }
    /**
     * 对目标Drawable 进行着色
     *
     * @param drawable 目标Drawable
     * @param colors   着色值
     * @return 着色处理后的Drawable
     */
    public static Drawable tintListDrawable(@NonNull Drawable drawable, ColorStateList colors) {
        Drawable wrappedDrawable = getCanTintDrawable(drawable);
        // 进行着色
        DrawableCompat.setTintList(wrappedDrawable, colors);
        return wrappedDrawable;
    }
    /**
     * 获取可以进行tint 的Drawable
     * <p>
     * 对原drawable进行重新实例化  newDrawable()
     * 包装  warp()
     * 可变操作 mutate()
     *
     * @param drawable 原始drawable
     * @return 可着色的drawable
     */
    @NonNull
    private static Drawable getCanTintDrawable(@NonNull Drawable drawable) {
        // 获取此drawable的共享状态实例
        Drawable.ConstantState state = drawable.getConstantState();
        // 对drawable 进行重新实例化、包装、可变操作
        return DrawableCompat.wrap(state == null ? drawable : state.newDrawable()).mutate();
    }
}Android 图片着色 Tint 详解的更多相关文章
- 给 Android 开发者的 RxJava 详解
		我从去年开始使用 RxJava ,到现在一年多了.今年加入了 Flipboard 后,看到 Flipboard 的 Android 项目也在使用 RxJava ,并且使用的场景越来越多 .而最近这几个 ... 
- [转]ANDROID L——Material Design详解(动画篇)
		转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 转自:http://blog.csdn.net/a396901990/article/de ... 
- Android屏幕适配问题详解
		上篇-Android本地化资源目录详解 :http://www.cnblogs.com/steffen/p/3833048.html 单位: px(像素):屏幕上的点. in(英寸):长度单位. mm ... 
- android ------- 开发者的 RxJava 详解
		在正文开始之前的最后,放上 GitHub 链接和引入依赖的 gradle 代码: Github: https://github.com/ReactiveX/RxJava https://github. ... 
- MVC图片上传详解   IIS (安装SSL证书后) 实现 HTTP 自动跳转到 HTTPS  C#中Enum用法小结  表达式目录树  “村长”教你测试用例  引用provinces.js的三级联动
		MVC图片上传详解 MVC图片上传--控制器方法 新建一个控制器命名为File,定义一个Img方法 [HttpPost]public ActionResult Img(HttpPostedFile ... 
- 转:给 Android 开发者的 RxJava 详解
		转自: http://gank.io/post/560e15be2dca930e00da1083 评注:多图解析,但是我还是未看懂. 前言 我从去年开始使用 RxJava ,到现在一年多了.今年加入 ... 
- android Camera2 API使用详解
		原文:android Camera2 API使用详解 由于最近需要使用相机拍照等功能,鉴于老旧的相机API问题多多,而且新的设备都是基于安卓5.0以上的,于是本人决定研究一下安卓5.0新引入的Came ... 
- 《Android NFC 开发实战详解 》简介+源码+样章+勘误ING
		<Android NFC 开发实战详解>简介+源码+样章+勘误ING SkySeraph Mar. 14th 2014 Email:skyseraph00@163.com 更多精彩请直接 ... 
- Android开发之InstanceState详解
		Android开发之InstanceState详解 本文介绍Android中关于Activity的两个神秘方法:onSaveInstanceState() 和 onRestoreInstanceS ... 
随机推荐
- 森林 BZOJ 3123
			题解: 第k大直接用主席树解决 合并利用启发式合并,将较小的连接到较大的树上 #include<cmath> #include<cstdio> #include<cstd ... 
- Codeforces Round #275 (Div. 2)  B. Friends and Presents 二分+数学
			8493833 2014-10-31 08:41:26 njczy2010 B - Friends and Presents G ... 
- Laravel 5.1 简单学习
			Laravel 5.1 简单学习 (1)Laravel的nginx配置,不配置或者配置错误,可能会报404或500错误,访问不到页面. location / { try_files $uri $uri ... 
- hdu2448 / 费用流 / harbin赛区c题
			题(自)目(己)错(英)综(语)复(太)杂(差),关系理了半小时+翻译才看明白,看明白之后,直接建图,费用流击杀./简单题. 2A:有的地方,可用互通的要建双向边! #include<cstdi ... 
- CDOJ_844 程序设计竞赛
			原题地址:http://acm.uestc.edu.cn/#/problem/show/844 "你动规无力,图论不稳,数据结构松散,贪心迟钝,没一样像样的,就你还想和我同台竞技,做你的美梦 ... 
- 《Java虚拟机原理图解》 1.2.2、Class文件中的常量池详解(上)
			我的上一篇文章<Java虚拟机原理图解> 1.class文件基本组织结构中已经提到了class的文件结构,在class文件中的魔数.副版本号.主版本之后,紧接着就是常量池的数据区域了,如下 ... 
- JDK内置工具jstack(Java Stack Trace)(转)
			1.介绍 jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息,如果是在64位机器上,需要指定选项"-J-d64",Windows的js ... 
- 新闻:融资600万 他用一套系统优化15大HR工作场景 精简入转调离 月开通214家 | IT桔子
			新闻:融资600万 他用一套系统优化15大HR工作场景 精简入转调离 月开通214家 | IT桔子 功劳说不上 
- jQuery学习总结(一)——jQuery基础与学习资源
			前一段时间录了一套关于jQuery的视频分享给大家,可以在下载区下载到,本来想配合文字一起的,后面发现视频+帮助文档也是非常好的学习方法. 一.jQuery简介与第一个jQuery程序 1.1.jQu ... 
- Go -- 交叉编译
			编译Windows程序和mac程序 
