自 API 21 (Android L)開始,Android SDK 引入 tint 着色器,能夠隨意改變安卓項目中圖標或者 View 背景的顏色,必定程度上能夠減小同一個樣式不一樣顏色圖標的數量,從而起到 Apk 瘦身的做用。不過使用 tint 存在必定的兼容性問題,且聽本文慢慢說來。php
android:tint:給圖標着色的屬性,值爲所要着色的顏色值,沒有版本限制;一般用於給透明通道的 png 圖標或者點九圖着色。html
android:tintMode:圖標着色模式,值爲枚舉類型,共有 六種可選值(add、multiply、screen、src_over、src_in、src_atop),僅可用於 API 21 及更高版本。java
對應於給圖片着色的這兩個屬性,給 View 背景着色也有兩個屬性:backgroundTint 和 backgroundTintMode,用法相同,只是做用於 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
經過 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);複製代碼
效果都是同樣的,如圖:
前面講到,使用 DrawableCompat 能夠起到版本兼容效果。實際上,還有一種辦法,就是使用 android.support.v7.widget 兼容包中的 AppCompatXXX 控件,好比 AppCompatImageView。這種控件提供有以下方法可用於着色處理:
其實,不論是 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亦楓
微信掃描二維碼,歡迎關注個人我的公衆號:安卓筆記俠
不只分享個人原創技術文章,還有程序員的職場遐想