问题描述

在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)

  1. public class SkxDrawableHelper {
  2. /**
  3. * 对目标Drawable 进行着色
  4. *
  5. * @param drawable 目标Drawable
  6. * @param color 着色的颜色值
  7. * @return 着色处理后的Drawable
  8. */
  9. public static Drawable tintDrawable(@NonNull Drawable drawable, int color) {
  10. Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
  11. DrawableCompat.setTint(wrappedDrawable, color);
  12. return wrappedDrawable;
  13. }
  14. /**
  15. * 对目标Drawable 进行着色
  16. *
  17. * @param drawable 目标Drawable
  18. * @param colors 着色值
  19. * @return 着色处理后的Drawable
  20. */
  21. public static Drawable tintListDrawable(@NonNull Drawable drawable, ColorStateList colors) {
  22. Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
  23. // 进行着色
  24. DrawableCompat.setTintList(wrappedDrawable, colors);
  25. return wrappedDrawable;
  26. }
  27. }

测试代码:
调用此方法对Deawable进行着色。我们分别对设置背景的Drawable着色 #30c3a6, 图片着色为#ff4081

  1. Drawable originBitmapDrawable = ContextCompat.getDrawable(this,
  2. R.drawable.icon_beijing);
  3. mImageView1.setBackground(
  4. SkxDrawableHelper.tintDrawable(originBitmapDrawable,
  5. Color.parseColor("#30c3a6")));
  6. mImageView2.setImageDrawable(
  7. SkxDrawableHelper.tintDrawable(originBitmapDrawable,
  8. Color.parseColor("#ff4081")));

没有进行着色处理的原效果:

进行着色后的效果如下:

一脸懵逼,这都什么跟什么啊?!!! 我只修改了下面的两个ImageView,并没有对上面的两个ImageView进行修改啊。而且 图4是怎么出来那么个畸形的。
好吧,一步步来!


DrawableCompat wrap

这里简单介绍下wrap 这个方法。这个方法的作用是对目标Drawable进行包装,它可以用于跨越不同的API级别,通过在这个类中的着色方法,简单来说就是为了兼容不同的版本。如果想对Drawable 进行着色就必须调用此方法。

  1. * Drawable bg = DrawableCompat.wrap(view.getBackground());
  2. * // Need to set the background with the wrapped drawable
  3. * view.setBackground(bg);
  4. *
  5. * // You can now tint the drawable
  6. * 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

  1. public static final Drawable getDrawable(Context context, int id) {
  2. final int version = Build.VERSION.SDK_INT;
  3. if (version >= 21) {
  4. return ContextCompatApi21.getDrawable(context, id);
  5. } else {
  6. return context.getResources().getDrawable(id);
  7. }
  8. }

Resources.java

  1. public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
  2. TypedValue value;
  3. ......
  4. // 从这里继续跟进去,这是加载Drawable的方法
  5. final Drawable res = loadDrawable(value, id, theme);
  6. synchronized (mAccessLock) {
  7. if (mTmpValue == null) {
  8. mTmpValue = value;
  9. }
  10. }
  11. return res;
  12. }
  1. @Nullable
  2. Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
  3. ......
  4. final boolean isColorDrawable;
  5. // Drawable 的资源缓存类
  6. final DrawableCache caches;
  7. // 缓存的key
  8. final long key;
  9. ......
  10. // 这里先判断是否加载过,如果已经加载过就去缓存里面去取,如果成功从缓存中取到就返回。
  11. if (!mPreloading) {
  12. final Drawable cachedDrawable = caches.getInstance(key, theme);
  13. if (cachedDrawable != null) {
  14. return cachedDrawable;
  15. }
  16. }
  17. // 缓存中没有,则根据ConstantState 来创建新的Drawable
  18. final ConstantState cs;
  19. if (isColorDrawable) {
  20. cs = sPreloadedColorDrawables.get(key);
  21. } else {
  22. cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
  23. }
  24. Drawable dr;
  25. if (cs != null) {
  26. dr = cs.newDrawable(this);
  27. } else if (isColorDrawable) {
  28. dr = new ColorDrawable(value.data);
  29. } else {
  30. dr = loadDrawableForCookie(value, id, null);
  31. }
  32. ......
  33. // 缓存Drawable
  34. if (dr != null) {
  35. dr.setChangingConfigurations(value.changingConfigurations);
  36. cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
  37. }
  38. return dr;
  39. }

可以看下cacheDrawable 这个方法,虽然从名字上理解是缓存Drawable,但其实是缓存的Drawable对应的ConstantState 。

  1. private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
  2. Theme theme, boolean usesTheme, long key, Drawable dr) {
  3. final ConstantState cs = dr.getConstantState();
  4. ......
  5. // 缓存ConstantState
  6. caches.put(key, theme, cs, usesTheme);
  7. ......
  8. }
  9. }

DrawableCache.java

  1. public Drawable getInstance(long key, Resources.Theme theme) {
  2. // 注意这里,从缓存中取出来的是 ConstantState
  3. final Drawable.ConstantState entry = get(key, theme);
  4. if (entry != null) {
  5. return entry.newDrawable(mResources, theme);
  6. }
  7. return null;
  8. }

跟到这里心里大概也有谱了,原来android 不是共享的Drawable ,而是共享的内部类 ConstantState,ConstantState 中才是保存相关信息的。所以也就会出现如果修改了资源的某一个项信息,引用相同资源的其他Drawable 也就一同变化。这会儿我们看下面的这张图也就不难理解了!

而如果要实现对同一个Drawable进行不同着色就必须要打破这种共享状态。使之成为下图所展示的状态。

那么如何才能打破这种状态呢?


mutate() 使Drawable可变

上面说到如果要实现对同一个Drawable进行不同着色就必须要打破这种共享状态。默认情况下,从同一资源加载的所有drawables实例都共享一个公共状态;
如果修改一个实例的状态,所有其他实例将接收相同的修改。而mutate() 方法就是使drawable 可变,
一个可变的drawable不与任何其他drawable共享它的状态,这样如果只修改可变drawable的属性就不会影响到其他与它加载同一个资源的drawable。

那么为mutate方法是如何打破共享状态呢?
Drawable 是抽象类,同时mutate()返回的是this,我们以BitmapDrawable 为例,看下mutate() 这个方法。

  1. /**
  2. * A mutable BitmapDrawable still shares its Bitmap with any other Drawable
  3. * that comes from the same resource.
  4. *
  5. * @return This drawable.
  6. */
  7. @Override
  8. public Drawable mutate() {
  9. /*
  10. mMutated 是个标签,用来保证mutate只会设置一次,也就解释了在Drawable中对mutate()
  11. 方法的一个解释,Calling this method on a mutable Drawable will have no
  12. effect(在已经可变的drawable上调用此方法无效),因为返回的还是自身
  13. */
  14. if (!mMutated && super.mutate() == this) {
  15. // 重新引用了一个新的状态对象
  16. mBitmapState = new BitmapState(mBitmapState);
  17. mMutated = true;
  18. }
  19. return this;
  20. }

而BitmapState(BitmapState bitmapState) 这个构造方法是对自己的属性重新进行了赋值。这样就相当于不再引用共享的公共状态了,重新指向了一个新的状态。

ok,修改我们的工具类重新看下效果。

  1. /**
  2. * 对目标Drawable 进行着色
  3. *
  4. * @param drawable 目标Drawable
  5. * @param color 着色的颜色值
  6. * @return 着色处理后的Drawable
  7. */
  8. public static Drawable tintDrawable(@NonNull Drawable drawable, int color) {
  9. Drawable wrappedDrawable = DrawableCompat.wrap(drawable).mutate();
  10. DrawableCompat.setTint(wrappedDrawable, color);
  11. return wrappedDrawable;
  12. }

效果:

又是一脸懵逼中,怎么还是不对。虽然上面的两个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 是一样。

  1. Log.e("drawable", drawable.toString());
  2. Log.e("wrap", DrawableCompat.wrap(drawable).toString());
  3. 02-07 21:41:36.557 24675-24675/com.skx.tomike E/drawable:
  4. android.graphics.drawable.BitmapDrawable@12bc2f1
  5. 02-07 21:41:36.557 24675-24675/com.skx.tomike E/wrap:
  6. android.graphics.drawable.BitmapDrawable@12bc2f1
  7. 02-07 21:41:36.558 24675-24675/com.skx.tomike E/drawable:
  8. android.graphics.drawable.BitmapDrawable@12bc2f1
  9. 02-07 21:41:36.558 24675-24675/com.skx.tomike E/wrap:
  10. android.graphics.drawable.BitmapDrawable@12bc2f1

通过查看代码中也得到了相应的答案。

DrawableCompat.java

  1. version >= 23
  2. static class MDrawableImpl extends LollipopDrawableImpl {
  3. @Override
  4. public void setLayoutDirection(Drawable drawable, int layoutDirection) {
  5. DrawableCompatApi23.setLayoutDirection(drawable, layoutDirection);
  6. }
  7. @Override
  8. public int getLayoutDirection(Drawable drawable) {
  9. return DrawableCompatApi23.getLayoutDirection(drawable);
  10. }
  11. @Override
  12. public Drawable wrap(Drawable drawable) {
  13. // No need to wrap on M+ M以上版本不需要包装,直接返回drawable
  14. return drawable;
  15. }
  16. }
  17. version >= 19
  18. static class KitKatDrawableImpl extends JellybeanMr1DrawableImpl {
  19. @Override
  20. public void setAutoMirrored(Drawable drawable, boolean mirrored) {
  21. DrawableCompatKitKat.setAutoMirrored(drawable, mirrored);
  22. }
  23. @Override
  24. public boolean isAutoMirrored(Drawable drawable) {
  25. return DrawableCompatKitKat.isAutoMirrored(drawable);
  26. }
  27. @Override
  28. public Drawable wrap(Drawable drawable) {
  29. // 这里是new 出来的新对象。
  30. return DrawableCompatKitKat.wrapForTinting(drawable);
  31. }
  32. @Override
  33. public int getAlpha(Drawable drawable) {
  34. return DrawableCompatKitKat.getAlpha(drawable);
  35. }
  36. }

这里我摘出来两个来进行对比。当api版本>=23 时,wrap 方法返回是传入的drawable。当api版本>=19 && <21 时,warp方法返回的是DrawableCompatKitKat.wrapForTinting(drawable)。这也就解释了为什么api版本不同,返回的结果不同了。

在高版本上(api>23)也就验证了猜想1是正确的,因为前后两次着色都是针对同一个drawable对象,而mutate 方法又只会生效一次,所以第二次的设置就理所应当的覆盖了第一次的设置,那么表现出来的结果就应该都是后面设置的颜色。

但是对于低版本就不太清楚为什么了,对drawable 进行包装后得到的两个不同的对象,既然是不同的对象,而且还都进行了mutate()设置为什么还是会表现出一样呢?这里做个记录!

针对猜想1我们做个简单试验。如果只是因为引用的是同一个Drawable对象的话,那我们只需要引用不同的Drawable 对象就OK了。
这样做下简单修改:

  1. Drawable originBitmapDrawable = ContextCompat.getDrawable(this,
  2. R.drawable.icon_beijing);
  3. mImageView1.setBackground(
  4. SkxDrawableHelper.tintDrawable(originBitmapDrawable,
  5. Color.parseColor("#30c3a6")));
  6. Drawable originBitmapDrawable2 = ContextCompat.getDrawable(this,
  7. R.drawable.icon_beijing);
  8. mImageView2.setImageDrawable(
  9. SkxDrawableHelper.tintDrawable(originBitmapDrawable2,
  10. Color.parseColor("#ff4081")));

效果:

对了?还是很懵,还有好多想不通的地方!还是要多翻源码啊。


Drawable getConstantState()

返回一个持有此Drawable的共享状态的ConstantState实例。而ConstantState类中也提供了方法来创建Drawable,在上面的部分我们也见到过。

newDrawable:从当前共享状态来创建一个drawable 实例。

这样的话我们就可以通过 getConstantState() 方法来获取drawable 所持有的共享状态的ConstantState,然后通过 newDrawable 方法来获取相应的drawable实例。


Android Tint工具类

  1. public class SkxDrawableHelper {
  2. /**
  3. * 对目标Drawable 进行着色
  4. *
  5. * @param drawable 目标Drawable
  6. * @param color 着色的颜色值
  7. * @return 着色处理后的Drawable
  8. */
  9. public static Drawable tintDrawable(@NonNull Drawable drawable, int color) {
  10. // 获取此drawable的共享状态实例
  11. Drawable wrappedDrawable = getCanTintDrawable(drawable);
  12. // 进行着色
  13. DrawableCompat.setTint(wrappedDrawable, color);
  14. return wrappedDrawable;
  15. }
  16. /**
  17. * 对目标Drawable 进行着色。
  18. * 通过ColorStateList 指定单一颜色
  19. *
  20. * @param drawable 目标Drawable
  21. * @param color 着色值
  22. * @return 着色处理后的Drawable
  23. */
  24. public static Drawable tintListDrawable(@NonNull Drawable drawable, int color) {
  25. return tintListDrawable(drawable, ColorStateList.valueOf(color));
  26. }
  27. /**
  28. * 对目标Drawable 进行着色
  29. *
  30. * @param drawable 目标Drawable
  31. * @param colors 着色值
  32. * @return 着色处理后的Drawable
  33. */
  34. public static Drawable tintListDrawable(@NonNull Drawable drawable, ColorStateList colors) {
  35. Drawable wrappedDrawable = getCanTintDrawable(drawable);
  36. // 进行着色
  37. DrawableCompat.setTintList(wrappedDrawable, colors);
  38. return wrappedDrawable;
  39. }
  40. /**
  41. * 获取可以进行tint 的Drawable
  42. * <p>
  43. * 对原drawable进行重新实例化 newDrawable()
  44. * 包装 warp()
  45. * 可变操作 mutate()
  46. *
  47. * @param drawable 原始drawable
  48. * @return 可着色的drawable
  49. */
  50. @NonNull
  51. private static Drawable getCanTintDrawable(@NonNull Drawable drawable) {
  52. // 获取此drawable的共享状态实例
  53. Drawable.ConstantState state = drawable.getConstantState();
  54. // 对drawable 进行重新实例化、包装、可变操作
  55. return DrawableCompat.wrap(state == null ? drawable : state.newDrawable()).mutate();
  56. }
  57. }

Android 图片着色 Tint 详解的更多相关文章

  1. 给 Android 开发者的 RxJava 详解

    我从去年开始使用 RxJava ,到现在一年多了.今年加入了 Flipboard 后,看到 Flipboard 的 Android 项目也在使用 RxJava ,并且使用的场景越来越多 .而最近这几个 ...

  2. [转]ANDROID L——Material Design详解(动画篇)

    转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 转自:http://blog.csdn.net/a396901990/article/de ...

  3. Android屏幕适配问题详解

    上篇-Android本地化资源目录详解 :http://www.cnblogs.com/steffen/p/3833048.html 单位: px(像素):屏幕上的点. in(英寸):长度单位. mm ...

  4. android ------- 开发者的 RxJava 详解

    在正文开始之前的最后,放上 GitHub 链接和引入依赖的 gradle 代码: Github: https://github.com/ReactiveX/RxJava https://github. ...

  5. MVC图片上传详解 IIS (安装SSL证书后) 实现 HTTP 自动跳转到 HTTPS C#中Enum用法小结 表达式目录树 “村长”教你测试用例 引用provinces.js的三级联动

    MVC图片上传详解   MVC图片上传--控制器方法 新建一个控制器命名为File,定义一个Img方法 [HttpPost]public ActionResult Img(HttpPostedFile ...

  6. 转:给 Android 开发者的 RxJava 详解

    转自:  http://gank.io/post/560e15be2dca930e00da1083 评注:多图解析,但是我还是未看懂. 前言 我从去年开始使用 RxJava ,到现在一年多了.今年加入 ...

  7. android Camera2 API使用详解

    原文:android Camera2 API使用详解 由于最近需要使用相机拍照等功能,鉴于老旧的相机API问题多多,而且新的设备都是基于安卓5.0以上的,于是本人决定研究一下安卓5.0新引入的Came ...

  8. 《Android NFC 开发实战详解 》简介+源码+样章+勘误ING

    <Android NFC 开发实战详解>简介+源码+样章+勘误ING SkySeraph Mar. 14th  2014 Email:skyseraph00@163.com 更多精彩请直接 ...

  9. Android开发之InstanceState详解

    Android开发之InstanceState详解   本文介绍Android中关于Activity的两个神秘方法:onSaveInstanceState() 和 onRestoreInstanceS ...

随机推荐

  1. 启动第一个 KVM 虚机

    本节演示如何使用 virt-manager 启动 KVM 虚机. 首先通过命令 virt-manager 启动图形界面 1 # virt-manager 点上面的图标创建虚机 给虚机命名为 kvm1, ...

  2. python 之递归及冒泡排序

    一.递归函数 在函数内部,可以调用其他函数,如果一个函数在内部调用本身,这个函数就是递归函数 1.递归的基本原理: 每一次函数调用都会有一次返回.当程序流执行到某一级递归的结尾处时,它会转移到前一级递 ...

  3. Mybatis resultMap空值映射问题

    参考博客:https://www.oschina.net/question/1032714_224673 http://stackoverflow.com/questions/22852383/how ...

  4. 不拖控件的asp.net编程方法——第1回

    以前写的asp.net程序基本上都用了webfrom的控件编写的,当然有个好处就是易入门.快速效率高,但感觉自己这了几个小系统,还是没学到什么东西,感觉心里没底,因为都是封装好的东西,拿来就用的,功能 ...

  5. koa2 从入门到进阶之路 (一)

    首先我们先来了解一下 Koa 是什么,https://koa.bootcss.com/,这是 Koa 的官方网站,映入眼帘的第一句就是 Koa -- 基于 Node.js 平台的下一代 web 开发框 ...

  6. js时间戳和时间格式之间的转换

    //时间戳转换成日期时间2014-8-8 下午11:40:20 function formatDate(ns){ return new Date(parseInt(ns) * 1000).toLoca ...

  7. ASP.NET Core默认注入方式下如何注入多个实现(多种方式) - sky 胡萝卜星星 - CSDN博客

    原文:ASP.NET Core默认注入方式下如何注入多个实现(多种方式) - sky 胡萝卜星星 - CSDN博客 版权声明:本文为starfd原创文章,转载请标明出处. https://blog.c ...

  8. 将一个文件从gbk编码转换为utf8编码

    用django展示模板时,出现如下错误: 'utf8' codec can't decode byte 0xd3 in position 197: invalid continuation byte ...

  9. 在chrome中屏蔽百度推荐

    在chrome中屏蔽百度推荐 方法1:可以使用adblock plus来进行屏蔽: 需要将chrome的扩展程序打开为调试者模式: 下载地址:http://chromecj.com/productiv ...

  10. binary-tree-preorder-traversal——前序遍历

    Given a binary tree, return the preorder traversal of its nodes' values. For example:Given binary tr ...