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 后向兼容库的正确姿式吧。

相关文章
相关标签/搜索