淺談 Android L 的 Tint(著色)
Tint 是什么?
Tint 翻譯為著色。
著色,著什么色呢,和背景有關(guān)?當(dāng)然是著背景的色。當(dāng)我們開發(fā) App 的時候,如果使用了 Theme.AppCompat 主題的時候,會發(fā)現(xiàn) ActionBar 或者 Toolbar 及相應(yīng)的控件的顏色會相應(yīng)的變成我們在 Theme 中設(shè)置的 colorPrimary, colorPrimaryDark, colorAccent 這些顏色,這是為什么呢,這就全是 Tint 的功勞了!
這樣做有什么好處呢?好處就是你不必再老老實(shí)實(shí)的打開 PS 再制作一張新的資源圖。而且大家都知道 apk 包最大的就是圖片資源了,這樣減少不必要資源圖,可以極大的減少了我們的 apk 包的大小。
實(shí)現(xiàn)的方式就是用一個顏色為我們的背景圖片設(shè)置 Tint(著色)。
例子:


大家可以看上面再張圖,這個是做的一個應(yīng)用“小白球”,如圖 1 的小圖片本來都是白色的(圓背景的單獨(dú)設(shè)的),但是經(jīng)過 Tint 著色后就變成了圖 2 中的淺藍(lán)色了。
好了,既然理解了tint的含義,我們趕緊看下這一切是如何實(shí)現(xiàn)的吧。
其實(shí)底層特別簡單,了解過渲染的同學(xué)應(yīng)該知道PorterDuffColorFilter這個東西,我們使用SRC_IN的方式,對這個Drawable進(jìn)行顏色方面的渲染,就是在這個Drawable中有像素點(diǎn)的地方,再用我們的過濾器著色一次。
實(shí)際上如果要我們自己實(shí)現(xiàn),只用獲取View的backgroundDrawable之后,設(shè)置下colorFilter即可。
看下最核心的代碼就這么幾行
if (filter == null) {
// Cache miss, so create a color filter and add it to the cache
filter = new PorterDuffColorFilter(color, mode);
}
d.setColorFilter(filter);
通常情況下,我們的mode一般都是SRC_IN,如果想了解這個屬性相關(guān)的資料,這里是傳送門: http://blog.csdn.net/t12x3456/article/details/10432935 (中文)
由于API Level 21以前不支持background tint在xml中設(shè)置,于是提供了ViewCompat.setBackgroundTintList方法和ViewCompat.setBackgroundTintMode用來手動更改需要著色的顏色,但要求相關(guān)的View繼承TintableBackgroundView接
源碼解析
以 EditText 為例,其它的基本一致
public AppCompatEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
...
ColorStateList tint = a.getTintManager().getTintList(a.getResourceId(0, -1)); //根據(jù)背景的resource id獲取內(nèi)置的著色顏色。
if (tint != null) {
setInternalBackgroundTint(tint); //設(shè)置著色
}
...
}
private void setInternalBackgroundTint(ColorStateList tint) {
if (tint != null) {
if (mInternalBackgroundTint == null) {
mInternalBackgroundTint = new TintInfo();
}
mInternalBackgroundTint.mTintList = tint;
mInternalBackgroundTint.mHasTintList = true;
} else {
mInternalBackgroundTint = null;
}
//上面的代碼是記錄tint相關(guān)的信息。
applySupportBackgroundTint(); //對背景應(yīng)用tint
}
private void applySupportBackgroundTint() {
if (getBackground() != null) {
if (mBackgroundTint != null) {
TintManager.tintViewBackground(this, mBackgroundTint);
} else if (mInternalBackgroundTint != null) {
TintManager.tintViewBackground(this, mInternalBackgroundTint); //最重要的,對tint進(jìn)行應(yīng)用
}
}
}
然后我們進(jìn)入tintViewBackground看下TintManager里面的源碼
public static void tintViewBackground(View view, TintInfo tint) {
final Drawable background = view.getBackground();
if (tint.mHasTintList) {
//如果設(shè)置了tint的話,對背景設(shè)置PorterDuffColorFilter
setPorterDuffColorFilter(
background,
tint.mTintList.getColorForState(view.getDrawableState(),
tint.mTintList.getDefaultColor()),
tint.mHasTintMode ? tint.mTintMode : null);
} else {
background.clearColorFilter();
}
if (Build.VERSION.SDK_INT <= 10) {
// On Gingerbread, GradientDrawable does not invalidate itself when it's ColorFilter
// has changed, so we need to force an invalidation
view.invalidate();
}
}
private static void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) {
if (mode == null) {
// If we don't have a blending mode specified, use our default
mode = DEFAULT_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);
}
// 最最重要,原來是對background drawable設(shè)置了colorFilter 完成了我們要的功能。
d.setColorFilter(filter);
}
private void applySupportBackgroundTint() {
if (getBackground() != null) {
if (mBackgroundTint != null) {
TintManager.tintViewBackground(this, mBackgroundTint);
} else if (mInternalBackgroundTint != null) {
TintManager.tintViewBackground(this, mInternalBackgroundTint); //最重要的,對tint進(jìn)行應(yīng)用
}
}
}
以上是對API21以下的兼容。
如果我們要實(shí)現(xiàn)自己的AppCompat組件實(shí)現(xiàn)tint的一些特性的話,我們就可以指定好ColorStateList,利用TintManager對自己的背景進(jìn)行著色,當(dāng)然需要對外開放設(shè)置的接口的話,我們還要實(shí)現(xiàn)TintableBackgroundView接口,然后用ViewCompat.setBackgroundTintList進(jìn)行設(shè)置,這樣能完成對v7以上所有版本的兼容。
自定義控件的著色
public class AppCompatView extends View implements TintableBackgroundView {
private TintInfo mTintInfo;
public AppCompatView(Context context) {
this(context, null);
}
public AppCompatView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AppCompatView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
EmBackgroundTintHelper.loadFromAttributes(this, attrs, defStyleAttr);
init();
}
private void init() {
mTintInfo = new TintInfo();
mTintInfo.mHasTintList = true;
}
@Override
public void setSupportBackgroundTintList(ColorStateList tint) {
EmBackgroundTintHelper.setSupportBackgroundTintList(this, mTintInfo,tint);
}
@Nullable
@Override
public ColorStateList getSupportBackgroundTintList() {
return EmBackgroundTintHelper.getSupportBackgroundTintList(mTintInfo);
}
@Override
public void setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
EmBackgroundTintHelper.setSupportBackgroundTintMode(this, mTintInfo, tintMode);
}
@Nullable
@Override
public PorterDuff.Mode getSupportBackgroundTintMode() {
return EmBackgroundTintHelper.getSupportBackgroundTintMode(mTintInfo);
}
}
最后
最后打個小廣告,并附上 Github 及我的 Blog

Blog:http://imli.me