从使用到源码,细说 Android 中的 tint 着色器

自 API 21 (Android L)开始,Android SDK 引入 tint 着色器,能够随意改变安卓项目中图标或者 View 背景的颜色,必定程度上能够减小同一个样式不一样颜色图标的数量,从而起到 Apk 瘦身的做用。不过使用 tint 存在必定的兼容性问题,且听本文慢慢说来。php

xml 中的 tint 和 tintMode 属性


  • android:tint:给图标着色的属性,值为所要着色的颜色值,没有版本限制;一般用于给透明通道的 png 图标或者点九图着色。html

  • android:tintMode:图标着色模式,值为枚举类型,共有 六种可选值(add、multiply、screen、src_over、src_in、src_atop),仅可用于 API 21 及更高版本。java

对应于给图片着色的这两个属性,给 View 背景着色也有两个属性:backgroundTintbackgroundTintMode,用法相同,只是做用于 android:background 属性。须要注意的是,这两个属性也只是做用于 API 21 及更高版本。android

这里咱们在使用默认 tintMode 的状况下,演示一下图标着色和背景着色的先后对比状况:程序员

原图:不作任何处理的 ImageButton,代码以下:微信

<ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_home" android:background="@android:color/transparent"/>复制代码

图标着色:使用 android:tint 属性对 src 属性指向的图标着色处理,代码以下:app

<ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:tint="@android:color/black" android:src="@mipmap/ic_home" android:background="@android:color/transparent"/>复制代码

背景着色:使用 backgroundTint 属性对 background 属性赋予的背景色着色处理,代码以下(这里只是为了演示,实际上直接改变 background 背景色便可):ide

<ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="@android:color/black" android:src="@mipmap/ic_home" android:background="@android:color/white"/>复制代码

这里 android:background 属性值使用的是颜色值,若是是图片的话,同样能够着色处理。而且,背景使用图片时着色的需求更现实一些。测试

注意:tint 或 backgroundTint 属性,与 src 或 background 属性必定是对应成对出现的。这个不难理解,要有处理源嘛。ui

java 代码中的 DrawableCompat


经过 xml 中的属性或者对应的 Java 代码中的 API 方法能够改变 View 所用到的图片颜色,可是存在必定的兼容性问题。好在有相应的兼容性 API 能够适配 6.0 以前的系统,也就是 DrawableCompat 类。直接看代码吧:

Drawable originalDrawable = ContextCompat.getDrawable(this, R.mipmap.ic_home);
Drawable tintDrawable = DrawableCompat.wrap(originalDrawable).mutate();
DrawableCompat.setTint(tintDrawable, Color.parseColor("#000000"));
mSamplesIv.setImageDrawable(tintDrawable);复制代码

能够看出,DrawableCompat 经过 setTint() 方法对 drawable 对象着色处理。值得注意的是,这里有两个特殊的方法须要特别说明一下:

DrawableCompat.wrap()

为了在不一样的系统 API 上使用 DrawableCompat.setTint() 作图标的着色处理,必须使用这个方法处理现有的 drawable 对象。而且,要将处理结果从新经过 setImageDrawable() 或者 setBackground() 赋值给 View 才能见效;

drawable.mutate()

咱们先来看一个有趣的现象:若是咱们有两个 ImageView 使用相同一个图片资源做为 src 或者 background 的属性值,而后在 Java 代码中经过 DrawableCompat 类对其中一个作着色处理,就像上面所写的代码这样,运行后你会发现,只有当前被赋值的 ImageView 显示的是被着色处理后的图片;可是去掉 mutate() 方法时,再次运行,两个 ImageView 都显示的是被着色处理后的图片!事实上,不只是两个,应用中全部使用到该图片资源的地方,都会显示成被着色处理过的样式。

这就是 mutate() 存在的必要性。要说到这个方法,就大有讲头啦。在此以前,咱们必须先了解一下 constant state 这个概念。

Android 系统为了减小内存消耗,将应用中所用到的相同 drawable (能够理解为相同资源)共享同一个 state,并称之为 constant state。这里用图表演示一下,两个 View 加载同一个图片资源,建立两个 drawables 对象,可是共享同一个 constant state 的场景:

这种设计固然大大节省内存,但也存在一个弊端。就是,当 constant state 属性发生变化时,全部使用相同资源的关联 drawable 都会随之改变,好比前面所说的这种现象。

而 mutate() 方法的出现就是为了解决这种问题的。你能够理解为 mutate() 方法就是复制一份 constant state,容许你随意改变属性,同时不对其余 drawable 有任何影响。如图:

这种设计在早期的官方文档上也有介绍,参考 drawable-mutations

再回到本文主题,可见,drawable 的着色处理必然要使用到 wrap() 和 mutate() 两个方法,也就瓜熟蒂落啦。

注意:为了起到兼容全部 API 的做用,着色处理时,建议同时使用 wrap() 和 mutate() 方法。可能,你在实际测试时,某些级别的系统 API 中,不会存在这种问题。

上面咱们使用 setTint() 方法直接改变 drawable 的颜色,可是有时候,咱们会给 Drawable 添加各类选择状态,好比点击时的 state_pressed 状态。DrawableCompat 类也提供有 setTintList() 方法,须要用到 ColorStateList。

举个例子,在 res/color 资源目录下定义一个 selector_home.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_pressed="true" android:color="@android:color/black"/>
    <item android:color="@android:color/white"/>

</selector>复制代码

在代码中经过 ContextCompat.getColorStateList 获取资源中的 ColorStateList 对象,并使用 DrawableCompat.setTintList() 方法着色处理便可:

Drawable originalDrawable = ContextCompat.getDrawable(this, R.mipmap.ic_home);
Drawable tintDrawable = DrawableCompat.wrap(originalDrawable).mutate();
DrawableCompat.setTintList(tintDrawable, ContextCompat.getColorStateList(this, R.color.selector_home));
mSamplesIv.setImageDrawable(tintDrawable);复制代码

固然你也能够直接在代码中手动建立一个 ColorStateList 对象:

int[] colors = new int[] { ContextCompat.getColor(this, android.R.color.black), ContextCompat.getColor(this, android.R.color.white)};
int[][] states = new int[2][];
states[0] = new int[] { android.R.attr.state_pressed};
states[1] = new int[] {};
ColorStateList colorStateList = new ColorStateList(states, colors);复制代码

效果都是同样的,如图:

Tint 着色器原理


前面讲到,使用 DrawableCompat 能够起到版本兼容效果。实际上,还有一种办法,就是使用 android.support.v7.widget 兼容包中的 AppCompatXXX 控件,好比 AppCompatImageView。这种控件提供有以下方法可用于着色处理:

  • setSupportBackgroundTintList(@Nullable ColorStateList tint)
  • setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode)

其实,不论是 DrawableCompat 仍是 AppCompatXXX 控件,底层实现原理都是同样的。咱们随便找一个看一下,就拿 AppCompatImageView 来看。看下 setSupportBackgroundTintList() 源码:

@Override
public void setSupportBackgroundTintList(@Nullable ColorStateList tint) {
    if (mBackgroundTintHelper != null) {
        mBackgroundTintHelper.setSupportBackgroundTintList(tint);
    }
}复制代码

调用 AppCompatBackgroundHelper 类的 setSupportBackgroundTintList 方法,继续深刻源码:

void setSupportBackgroundTintList(ColorStateList tint) {
    if (mBackgroundTint == null) {
        mBackgroundTint = new TintInfo();
    }
    mBackgroundTint.mTintList = tint;
    mBackgroundTint.mHasTintList = true;
    applySupportBackgroundTint();
}复制代码

继续深刻 applySupportBackgroundTint() 方法的源码:

void applySupportBackgroundTint() {
    final Drawable background = mView.getBackground();
    if (background != null) {
        if (shouldApplyFrameworkTintUsingColorFilter()
                && applyFrameworkTintUsingColorFilter(background)) {
            // This needs to be called before the internal tints below so it takes
            // effect on any widgets using the compat tint on API 21 (EditText)
            return;
        }

        if (mBackgroundTint != null) {
            AppCompatDrawableManager.tintDrawable(background, mBackgroundTint,
                    mView.getDrawableState());
        } else if (mInternalBackgroundTint != null) {
            AppCompatDrawableManager.tintDrawable(background, mInternalBackgroundTint,
                    mView.getDrawableState());
        }
    }
}复制代码

该方法的重心在于 AppCompatDrawableManager.tintDrawable() 方法,继续深刻:

static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) {
    if (DrawableUtils.canSafelyMutateDrawable(drawable)
            && drawable.mutate() != drawable) {
        Log.d(TAG, "Mutated drawable is not the same instance as the input.");
        return;
    }

    if (tint.mHasTintList || tint.mHasTintMode) {
        drawable.setColorFilter(createTintFilter(
                tint.mHasTintList ? tint.mTintList : null,
                tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
                state));
    } else {
        drawable.clearColorFilter();
    }

    if (Build.VERSION.SDK_INT <= 23) {
        // Pre-v23 there is no guarantee that a state change will invoke an invalidation,
        // so we force it ourselves
        drawable.invalidateSelf();
    }
}复制代码

找到这里,已经能看出一些端倪。原来是使用 drawable.setColorFilter() 进行颜色渲染处理的。而且经过 createTintFilter() 方法建立颜色过滤器:

private static PorterDuffColorFilter createTintFilter(ColorStateList tint, PorterDuff.Mode tintMode, final int[] state) {
    if (tint == null || tintMode == null) {
        return null;
    }
    final int color = tint.getColorForState(state, Color.TRANSPARENT);
    return getPorterDuffColorFilter(color, tintMode);
}

public static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) {
    // First, lets see if the cache already contains the color filter
    PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);

    if (filter == null) {
        // Cache miss, so create a color filter and add it to the cache
        filter = new PorterDuffColorFilter(color, mode);
        COLOR_FILTER_CACHE.put(color, mode, filter);
    }

    return filter;
}复制代码

PorterDuffColorFilter 类!这就是咱们要找的目标。PorterDuffColorFilter 能够获取 drawable 中的像素点,并使用相应的颜色过滤器予以处理。

知道原理以后,不妨试想一下,仅仅这样一句代码,是否是也能帮助咱们实现着色处理呢:

mSamplesIv.setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(this, android.R.color.black), PorterDuff.Mode.SRC_IN));复制代码

或者自定义 View 时也能将 AppCompatXXX 控件的相关源码复制过来,实现着色器功能。

固然,若是再去翻看 DrawableCompat 源码,虽然寻找路径不一样,但最终仍是会走到 drawable.setColorFilter() 方法。而且从 DrawableCompat 源码中,你还能看到为何 wrap() 方法可以兼容处理不一样系统 API 的缘由。这里就不细细展现啦,感兴趣的朋友能够本身阅读源码。

这就是 Android SDK 中的 tint 着色器相关知识。事实上,咱们也常常用到这个东西。举个最多见的例子,为何不一样主题下 EditText 背景的底部颜色条会不同呢?其实,这也是一张点九图,只是不一样主题下使用不一样颜色的着色器处理过而已。

关于我:亦枫,博客地址:yifeng.studio/,新浪微博:IT亦枫

微信扫描二维码,欢迎关注个人我的公众号:安卓笔记侠

不只分享个人原创技术文章,还有程序员的职场遐想

相关文章
相关标签/搜索