`
ch_kexin
  • 浏览: 909461 次
  • 性别: Icon_minigender_2
  • 来自: 青岛
社区版块
存档分类
最新评论

Drawable 着色的后向兼容方案

 
阅读更多

看到 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/

分享到:
评论

相关推荐

    android中的drawable集合

    在Android开发中,Drawable是图形和图像处理的重要组成部分,它是一种可以绘制到Canvas上的对象,广泛用于界面元素的装饰和状态表示。以下是对Android中drawable集合的详细解释: 1. **Bitmap Drawable**: Bitmap ...

    安卓Drawable的使用

    在Android开发中,Drawable是图形和图像对象的抽象表示,用于在用户界面上创建各种视觉效果。本篇文章将深入探讨Android中的Drawable及其使用方法,包括系统提供的各种Drawable类型以及如何自定义Drawable。 首先,...

    Drawable的基础demo

    在Android开发中,`Drawable`是一个非常重要的概念,它代表了屏幕上的图形对象,可以是图像、颜色、形状或者是它们的组合。`Drawable`广泛应用于各种UI元素的背景、图标等,提供了丰富的自定义和动画效果。在这个...

    Android中一种巧妙的drawable.xml替代方案分享

    Android drawable.xml 替代方案分享 本文主要介绍了 Android 中一种巧妙的 drawable.xml 替代方案,通过 DataBinding 实现 drawable 的动态生成,解决了传统方案的缺陷,提高了开发效率和可读性。 一、传统方案...

    Android Drawable 各种高效用法

    在Android开发中,Drawable是图形资源的核心组成部分,用于在用户界面上绘制各种图像,如按钮、背景、图标等。理解并熟练运用Drawable的各种高效用法对于优化应用性能和提升用户体验至关重要。以下是一些关于Android...

    android的drawable图片集

    在Android开发中,`drawable`资源是至关重要的组成部分,它主要负责存储应用程序中使用的各种图形元素,如...理解并熟练运用`drawable`目录及其相关概念,能帮助开发者创建更加美观、高效且兼容性良好的Android应用。

    DrawableTextView.zip

    6. **样例代码**:在解压后的`DrawableTextView-master`目录中,可能包含了一个示例项目,展示了如何在实际应用中使用这个自定义控件。开发者可以参考这个示例,了解如何在XML布局文件中引入`DrawableTextView`,并...

    Android中Drawable分类汇总

    在Android开发中,Drawable是用于绘制图形的资源,它们被广泛应用于各种界面元素的背景或图片显示。Drawable资源是抽象的图形对象,可以包括图片、颜色、渐变等多种形式。在Android SDK中,开发者通常不需要直接操作...

    依赖的drawable文件

    在Android开发中,`drawable`文件是一个至关重要的组成部分,它主要负责应用的图形资源管理。`Drawable`资源是Android系统中用于表示图形的一种抽象概念,它可以是图片、颜色、形状或者动画等。在这个主题中,我们将...

    drawable(图片).zip

    在Android开发中,`drawable`资源是至关重要的组成部分,它主要负责存储应用程序中的各种图形元素,如图标、背景、按钮形状等。`drawable`资源通常存储在项目的`res/drawable`目录下,并且根据不同的密度(ldpi, ...

    Android自定义Drawable的代码例子

    创建好自定义Drawable后,可以通过以下方式使用: - 在XML布局中直接引用:`&lt;your.package.name.YourCustomDrawable/&gt;` - 在代码中动态创建:`YourCustomDrawable drawable = new YourCustomDrawable(context);` ...

    自定义Drawable

    在Android开发中,Drawable是图形对象的抽象表示,通常用于设置视图的背景、图标的样式等。自定义Drawable能够让我们根据项目需求创造出独特的视觉效果,例如标题中提到的圆角图片和圆形图片。本篇将深入探讨如何...

    Drawable资源管理图片处理

    在Android开发中,Drawable资源是用于管理应用中各种图形元素,如图片、颜色、形状等的关键组成部分。了解和熟练掌握Drawable资源的管理与图片处理,对于优化应用性能和提升用户体验至关重要。以下是一些关于...

    android 通过图片名称获取 drawable 目录下的图片

    5. **兼容性**:在使用反射操作时需要注意Android版本间的兼容性问题,特别是对于较新版本的API变更。 总之,通过图片名称动态加载Drawable资源的方法虽然不是常用的开发模式,但在特定场景下仍具有一定的实用价值...

    android drawable

    在Android开发中,`Drawable`是一个非常重要的概念,它代表了可以绘制到屏幕上的图形对象。这个示例项目"android drawable"旨在展示如何在Android应用中有效地使用和显示`Drawable`资源。 `Drawable`在Android中...

    drawable文件夹详解

    "Drawable 文件夹详解" Drawable 文件夹是 Android 应用程序中用于存储图片资源的文件夹。在 Android 中,Drawable 文件夹可以根据不同的屏幕密度、语言、国家地区等因素存储多种不同的图片资源。 Drawable 文件夹...

    文字转换成Drawable

    5. **转换为Drawable**:绘制完成后,将Bitmap转换为Drawable对象,可以使用BitmapDrawable构造函数完成。然后,这个Drawable对象就可以像其他图片资源一样在XML布局或代码中使用。 6. **进一步扩展**:除了基本的...

    Android的Drawable学习Demo

    在Android开发中,Drawable是图形和图像处理的重要组成部分,它涵盖了多种类型的图像资源,如颜色、形状、位图等。本篇文章将深入探讨Android的Drawable及其应用,通过一个实际的Demo来展示其功能和用法。 Drawable...

    xUtils3源码(已添加drawable支持)

    了解了xUtils3的Drawable支持后,我们来看一下如何在实际项目中使用这个特性。首先,需要在项目中引入xUtils3的依赖库,然后可以通过`Utils.display(imgView, "drawable://" + R.drawable.my_drawable)`这样的方式...

Global site tag (gtag.js) - Google Analytics