看到 Android Weekly 最新一期有一篇文章:Tinting drawables,使用 ColorFilter 手動打造了一個 TintBitmapDrawable
,以前也看到有些文章使用這種方式來實現 Drawable 着色或者實現相似的功能。可是,這種方案並不完善,本文將介紹一個完美的後向兼容方案。html
其實在 Android Support V4 的包中提供了 DrawableCompat
類,咱們很容易寫出以下的輔助方法來實現 Drawable 的着色,以下:java
public static Drawable tintDrawable(Drawable drawable, ColorStateList colors) { final Drawable wrappedDrawable = DrawableCompat.wrap(drawable); DrawableCompat.setTintList(wrappedDrawable, colors); return wrappedDrawable; }
使用例子:android
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"))));
效果以下:app
對比 Tinting drawables 文中的方法,除了它擁有的優點之外,這種方式支持幾乎全部的 Drawable 類型,而且可以完美兼容幾乎全部的 Android 版本。ide
到這裏,其實本文要說的解決方案已經說完了。若是繼續往下看,相信會有更多收穫。函數
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)));
首先來看一下問題。原始的 Icon 以下圖所示:優化
咱們使用兩個 ImageView,一個不作任何處理,一個使用以下代碼着色:ui
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 的背景着色(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 後向兼容庫的正確姿式吧。