從使用到源碼,細說 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亦楓

微信掃描二維碼,歡迎關注個人我的公衆號:安卓筆記俠

不只分享個人原創技術文章,還有程序員的職場遐想

相關文章
相關標籤/搜索