Drawable 着色的後向兼容方案

看到 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)));

BitmapDrawable 的優化

首先來看一下問題。原始的 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 光標着色

經過前面的方法,咱們已經能夠把 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 後向兼容庫的正確姿式吧。

相關文章
相關標籤/搜索