看到 Android Weekly 最新一期有一篇文章:Tinting drawables,使用 ColorFilter 手動打造了一個TintBitmapDrawable,之前也看到有些文章使用這種方式來實現 Drawable 着色或者實現類似的功能。但是,這種方案並不完善,本文將介紹一個完美的后向兼容方案。
解決方案
其實在 Android Support V4 的包中提供了 DrawableCompat 類,我們很容易寫出如下的輔助方法來實現 Drawable 的着色,如下:
public static Drawable tintDrawable(Drawable drawable, ColorStateList colors) { final Drawable wrappedDrawable = DrawableCompat.wrap(drawable); DrawableCompat.setTintList(wrappedDrawable, colors); return wrappedDrawable; }
使用例子:
EditText editText1 = (EditText) findViewById(R.id.edit_1); final Drawable originalDrawable = editText1.getBackground(); final Drawable wrappedDrawable = tintDrawable(originalDrawable, ColorStateList.valueOf(Color.RED)); editText1.setBackgroundDrawable(wrappedDrawable); EditText editText2 = (EditText) findViewById(R.id.edit_2); editText2.setBackgroundDrawable(tintDrawable(editText2.getBackground(), ColorStateList.valueOf(Color.parseColor("#03A9F4"))));
效果如下:
這種方式支持幾乎所有的 Drawable 類型,並且能夠完美兼容幾乎所有的 Android 版本。
優化
使用 ColorStateList 着色
這種方式支持使用 ColorStateList 着色,這樣我們還可以根據 View 的狀態着色成不同的顏色。
對於上面的 EditText 的例子,我們就可以優化一下,根據它是否獲得焦點,設置成不同的顏色。我們新建一個res/color/edittext_tint_colors.xml 如下:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/red" android:state_focused="true" /> <item android:color="@color/gray" /> </selector>
代碼改成這樣:
editText2.setBackgroundDrawable(tintDrawable(editText2.getBackground(),
getResources().getColorStateList(R.color.edittext_tint_colors)));
BitmapDrawable 的優化
首先來看一下問題。原始的 Icon 如下圖所示:

我們使用兩個 ImageView,一個不做任何處理,一個使用如下代碼着色:
ImageView imageView = (ImageView) findViewById(R.id.image_1); final Drawable originalBitmapDrawable = getResources().getDrawable(R.drawable.icon); imageView.setImageDrawable(tintDrawable(originalBitmapDrawable, ColorStateList.valueOf(Color.MAGENTA)));
效果如下:

怎么回事?我明明只給后面的一個設置了着色的 Drawable,為什么兩個都被着色了?這是因為 Android 為了優化系統性能,資源 Drawable 只有一份拷貝,你修改了它,等於所有的都修改了。如果你給兩個 View 設置同一個資源,它的狀態是這樣的:

也是就是他們是共享狀態的。幸運的是,Drawable 提供了一個方法 mutate(),來打破這種共享狀態,等於就是要告訴系統,我要修改(mutate)這個 Drawable。給 Drawable 調用 mutate() 方法以后。他們的關系就變成如下的圖所示:

我們修改一下代碼:
ImageView imageView = (ImageView) findViewById(R.id.image_1); final Drawable originalBitmapDrawable = getResources().getDrawable(R.drawable.icon).mutate(); imageView.setImageDrawable(tintDrawable(originalBitmapDrawable, ColorStateList.valueOf(Color.MAGENTA)));

非常完美,達到了我們之前想要的效果。
你可能會有這樣的擔心,調用 mutate() 是不是在內存中把 Bitmap 拷貝了一份?其實不是這樣的,還是公用的 Bitmap,只是拷貝了一份狀態值,這個數據量很小,所以不用擔心。詳細情況可以參考這篇文章:Drawable mutations。
EditText 光標着色
通過前面的方法,我們已經可以把 EditText 的背景着色(Tint)成了任意想要的顏色。但是仔細一看,還有點問題,輸入的時候,光標的顏色還是原來的顏色,如下圖所示:

在 Android 3.1 (API 12) 開始就支持了 textCursorDrawable,也就是可以自定義光標的 Drawable。遺憾的是,這個方法只能在 xml 中使用,這和本文沒有啥關系,具體使用可以參考這個回答,並沒有提供接口來動態修改。
我們有一個比較折中的方案,就是通過反射機制,來獲得 CursorDrawable,然后通過本文的方法,來對這個 Drawable 着色。
public static void tintCursorDrawable(EditText editText, int color) { try { Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); fCursorDrawableRes.setAccessible(true); int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); Field fEditor = TextView.class.getDeclaredField("mEditor"); fEditor.setAccessible(true); Object editor = fEditor.get(editText); Class<?> clazz = editor.getClass(); Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); fCursorDrawable.setAccessible(true); if (mCursorDrawableRes <= 0) { return; } Drawable cursorDrawable = editText.getContext().getResources().getDrawable(mCursorDrawableRes); if (cursorDrawable == null) { return; } Drawable tintDrawable = tintDrawable(cursorDrawable, ColorStateList.valueOf(color)); Drawable[] drawables = new Drawable[] {tintDrawable, tintDrawable}; fCursorDrawable.set(editor, drawables); } catch (Throwable ignored) { } }
原理比較簡單,就是直接獲得到 EditText 的 mCursorDrawableRes,然后通過這個 id 獲取到對應的 Drawable,調用我們的着色函數 tintDrawable,然后設置進去。效果如下:

原理分析
上面就是我們的全部的解決方案,我們接下來分析一下 DrawableCompat 着色相關的源碼,理解其中的原理。再來回顧一下我們寫的 tintDrawable 函數,里面只調用了 DrawableCompat 的兩個方法。下面我們詳細分析這兩個方法。
首先通過 DrawableCompat.wrap() 獲得一個封裝的 Drawable:
// android.support.v4.graphics.drawable.DrawableCompat.java public static Drawable wrap(Drawable drawable) { return IMPL.wrap(drawable); }
調用了 IMPL 的 wrap 函數,IMPL 的實現如下:
/** * Select the correct implementation to use for the current platform. */ static final DrawableImpl IMPL; static { final int version = android.os.Build.VERSION.SDK_INT; if (version >= 23) { IMPL = new MDrawableImpl(); } else if (version >= 22) { IMPL = new LollipopMr1DrawableImpl(); } else if (version >= 21) { IMPL = new LollipopDrawableImpl(); } else if (version >= 19) { IMPL = new KitKatDrawableImpl(); } else if (version >= 17) { IMPL = new JellybeanMr1DrawableImpl(); } else if (version >= 11) { IMPL = new HoneycombDrawableImpl(); } else { IMPL = new BaseDrawableImpl(); } }
很明顯,這是根據不同的 API Level 選擇不同的實現類,再往下看一點,發現 API Level 大於等於 22 的繼承於LollipopMr1DrawableImpl,我們來看一下它的 wrap() 的實現:
static class LollipopMr1DrawableImpl extends LollipopDrawableImpl { @Override public Drawable wrap(Drawable drawable) { return DrawableCompatApi22.wrapForTinting(drawable); } } class DrawableCompatApi22 { public static Drawable wrapForTinting(Drawable drawable) { // We don't need to wrap anything in Lollipop-MR1 return drawable; } }
因為 API 22 開始 Drwable 本來就支持了 Tint,不需要做任何封裝了。
我們來看一下它的 wrap() 都是返回一個封裝了一層的 Drawable,我們以 BaseDrawableImpl 為例分析:
static class BaseDrawableImpl implements DrawableImpl { ... @Override public Drawable wrap(Drawable drawable) { return DrawableCompatBase.wrapForTinting(drawable); } ... }
這里調用了 DrawableCompatBase.wrapForTinting(),實現如下:
class DrawableCompatBase { ... public static Drawable wrapForTinting(Drawable drawable) { if (!(drawable instanceof DrawableWrapperDonut)) { return new DrawableWrapperDonut(drawable); } return drawable; } }
實際上這里是返回了一個 DrawableWrapperDonut 的封裝對象。同理分析其他 API Level 小於 22 的最后實現,發現最后都是返回一個繼承於 DrawableWrapperDonut 的對象。
回到最開始的代碼,我們分析 DrawableCompat.setTintList() 的實現,其實是調用了 IMPL.setTintList(),通過前面的分析我們知道,只有 API Level 小於 22 的才要做特殊的處理,我們還是以 BaseDrawableImpl 為例分析:
static class BaseDrawableImpl implements DrawableImpl { ... @Override public void setTintList(Drawable drawable, ColorStateList tint) { DrawableCompatBase.setTintList(drawable, tint); } ... }
這里調用了 DrawableCompatBase.setTintList():
class DrawableCompatBase { ... public static void setTintList(Drawable drawable, ColorStateList tint) { if (drawable instanceof DrawableWrapper) { ((DrawableWrapper) drawable).setTintList(tint); } } }
通過前面的分析,我們知道,這里傳入的 Drawable 都是 DrawableWrapperDonut 的子類,所以實際上就是調用了DrawableWrapperDonut 的 setTintList():
@Override public void setTintList(ColorStateList tint) { mTintList = tint; updateTint(getState()); } private boolean updateTint(int[] state) { if (mTintList != null && mTintMode != null) { final int color = mTintList.getColorForState(state, mTintList.getDefaultColor()); final PorterDuff.Mode mode = mTintMode; if (!mColorFilterSet || color != mCurrentColor || mode != mCurrentMode) { setColorFilter(color, mode); mCurrentColor = color; mCurrentMode = mode; mColorFilterSet = true; return true; } } else { mColorFilterSet = false; clearColorFilter(); } return false; }
看到這里最終是調用了 Drawable 的 setColorFilter() 方法。可以看到,這里和最開始提到的那篇文章的原理是一致的,但是這里處理更加細致,考慮更加全面。
通過源碼分析,感覺到可能這才是做 Android 后向兼容庫的正確姿勢吧。轉自http://www.race604.com/tint-drawable/
