Drawable mutations

有没有遇到过这样一种情况,我们要加载同一资源到两个ImageView,但需要给其中一个资源改变颜色或者透明度。如下面的代码

1
2
3
4
5
6
7
8
9
ImageView imageView1 = (ImageView) view.findViewById(R.id.imageview);
ImageView imageView2 = (ImageView) view.findViewById(R.id.imageview2);
Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
drawable.setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP);
imageView.setImageDrawable(drawable);
Drawable drawable1 = getResources().getDrawable(R.mipmap.ic_launcher);
imageView2.setImageDrawable(drawable1);

我们给imageView1设置ColorFilter改变一下图标的颜色,imageview2保持不变。这样会发生什么呢,看下面的图:

mutate(1)

这时候奇怪的事情发生了,两个ImageView都被改变了颜色。这是因为Drawable使用在Android系统中使用范围比较广,系统对此作了优化,同一资源的drawable共享一个状态,叫做ConstantState.例如,上面的R.mipmap.ic_launcher,每次新建一个drawable都是一个不同的drawable实例,但他们共享一个状态,这个状态中包含bitmap image,所以所有的R.mipmap.ic_launcher都共享一个bitmap,这就是两个ImageView都改变颜色原因。

Because drawables are used so extensively throughout the system, Android optimizes them when they are loaded from resources. For instance, every time you create a Button, a new drawable is loaded from the framework resources (android.R.drawable.btn_default). This means all buttons across all the apps use a different drawable instance as their background. However, all these drawables share a common state, called the “constant state.” The content of this state varies according to the type of drawable you are using, but it usually contains all the properties that can be defined by a resource. In the case of a button, the constant state contains a bitmap image. This way, all buttons across all applications share the same bitmap, which saves a lot of memory.(摘抄自Android Developers Blog)。如下图所示

mutate(2)

但有没有办法解决呢,Drawable提供了一个mutate方法,我们来看下mutate方法的注释

Make this drawable mutable. This operation cannot be reversed. A mutable drawable is guaranteed to not share its state with any other drawable. This is especially useful when you need to modify properties of drawables loaded from resources. By default, all drawables instances loaded from the same resource share a common state; if you modify the state of one instance, all the other instances will receive the same modification. Calling this method on a mutable Drawable will have no effect.

mutate不知道怎么翻译合适,姑且叫做可变的吧。这个mutate方法使得这个drawable变成可变的,这个操作不可逆转。调用了mutate方法,使得该drawable不和其他drawable共享状态。
我们来看下这个mutate的实现,发现在Drawable源码中,mutate方法只是返回了他自身。那我们来看下drawable子类有没有对该方法重写。我们找到BitmapDrawable

 @Override
public Drawable mutate() {
    if (!mMutated && super.mutate() == this) {
        mBitmapState = new BitmapState(mBitmapState);
        mMutated = true;
    }
    return this;
}

如果没有调用过mutate方法,会新建一个BitmapState,再将mMutated置为true。这样相当于做了一次状态的拷贝,就不会与其他drawable共享状态了。
接下来我们修改下上面的代码

ImageView imageView1 = (ImageView) view.findViewById(R.id.imageview);
ImageView imageView2 = (ImageView) view.findViewById(R.id.imageview2);

Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher).mutate();
drawable.setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP);
imageView.setImageDrawable(drawable);

Drawable drawable1 = getResources().getDrawable(R.mipmap.ic_launcher);
imageView2.setImageDrawable(drawable1);

运行效果:

mutate3
这次两个ImageView不相同了,下一节我们来学习下资源的加载机制,看Resources类是怎么加载资源的。