@TryLoveCatch
2022-05-05T08:42:22.000000Z
字数 7821
阅读 2603
Android知识体系
一般情况下,我们都会遇到,在点击一个icon的时候,要实现一个点击效果。正常的流程,就是UI给两张图片,一张默认的,一张点击的,然后我们实现一个selector.xml来实现点击切换的效果,如下:
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_pressed="true"android:drawable="@drawable/card_bg_selector" /><item android:drawable="@drawable/card_bg_default" /></selector>
这样写,是没有任何问题的,但是我们会发现,其实,大多数情况,这两张图card_bg_default和card_bg_selector,基本上没有差别,有的也就是颜色的变化,例如变暗,或者某些区域变色之类的,所以,问题来了:
有没有可能,只使用一张图片,来实现点击效果呢?
下面有几种方法,可以实现这个功能。
![]()
如上图,我们就可以利用layer-list来实现这种点击变暗的效果,如下:
<!--?xml version="1.0" encoding="utf-8"?--><selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_pressed="true"><layer-list><item android:drawable="@drawable/test" /><item><shape android:shape="oval" ><solid android:color="#51000000" /></shape></item></layer-list></item><item android:drawable="@drawable/test" /></selector>
在pressed为true的状态时,使用了layer-list对应的item,里面包含了一张图片和一个shape实现的半透明覆盖层,这样点击的时候效果就是图片变暗从而实现点击效果。
在来一个例子,看下图:
点击的时候,有一个缩小的动画,也可以用layer-list来实现,如下:
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_pressed="true"><layer-list><item android:drawable="@drawable/test"android:top="3dp"android:bottom="3dp"android:left="3dp"android:right="3dp"/></layer-list></item><item android:drawable="@drawable/test" /></selector>
PorterDuffColorFilter filter = new PorterDuffColorFilter(color, mode);Drawable drawable = getDrawable(R.mipmap.ic_launcher);drawable.setColorFilter(filter);
关于这个Mode,就是指PorterDuff.Mode,具体value和效果如下:

这里需要理解src和dst的意思
Src为源图像,意为将要绘制的图像
Dst为目标图像,意为我们将要把源图像绘制到的图像
在我们这个代码里面,src指color,dst就是R.mipmap.ic_launcher
所以,我们如果要实现改变icon颜色的功能,就应该是用SrcIn,所以代码更改如下:
PorterDuffColorFilter filter = new PorterDuffColorFilter(color,PorterDuff.Mode.SRC_IN);Drawable drawable = getDrawable(R.mipmap.ic_launcher);drawable.setColorFilter(filter);
Tint,即为着色。
最常见的,当我们开发 App 的时候,ActionBar 或者 Toolbar中设置的 colorPrimary, colorPrimaryDark等这些颜色,就全是 Tint 的功劳了!
在Android 5.0(API 级别 21)之后,android提供了setTint()来给 BitmapDrawable 或 NinePatchDrawable 对象着色,也可以使用android:tint 以及 android:tintMode属性设置您的布局中的着色颜色和模式,这个模式就是我们上面讲的PorterDuff.Mode。
如何兼容
Android Support V4的包中提供了DrawableCompat类,我们可以利用这个兼容类,来实现着色。
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);
这种方式支持几乎所有的 Drawable 类型,并且能够完美兼容几乎所有的 Android 版本
使用 ColorStateList 着色,这样我们还可以根据 View 的状态着色成不同的颜色。
修改上面EditText的例子,获取焦点的时候变色。新建一个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,一个不做任何处理,一个着色,如下:
ImageView imageView01 = (ImageView) findViewById(R.id.image_1);final Drawable originalBitmapDrawable = getResources().getDrawable(R.drawable.icon);imageView01.setImageDrawable(originalBitmapDrawable);ImageView imageView02 = (ImageView) findViewById(R.id.image_1);originalBitmapDrawable = getResources().getDrawable(R.drawable.icon);imageView02.setImageDrawable(tintDrawable(originalBitmapDrawable, ColorStateList.valueOf(Color.MAGENTA)));
效果如下:

WTF!!! 怎么都变色?
简而言之,这是因为 Android 为了优化系统性能,资源 Drawable 只有一份拷贝,你修改了它,等于所有的都修改了。如果你给两个 View 设置同一个资源,它的状态是这样的:
看图可以知道,这两个ImageView的drawable是共享状态的,所以drawable的state改变了,会影响所以使用这个drawable的ImageView。
不过,Drawable 提供了一个方法mutate(),来打破这种共享状态,等于就是要告诉系统,我要修改(mutate)这个 Drawable。给 Drawable 调用 mutate() 方法以后。他们的关系就变成如下的图所示:

我们修改一下工具类方法tintDrawable():
public static Drawable tintDrawable(Drawable drawable,ColorStateList colors) {Drawable newDrawable = drawable.getConstantState().newDrawable().mutate();final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);DrawableCompat.setTintList(wrappedDrawable, colors);return wrappedDrawable;}
上面说的那个问题,我们说是因为
Android 为了优化系统性能,资源 Drawable 只有一份拷贝
这个从源码中可以印证:
@NullableDrawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {...// First, check whether we have a cached version of this drawable// that was inflated against the specified theme.if (!mPreloading) {final Drawable cachedDrawable = caches.getInstance(key, theme);if (cachedDrawable != null) {return cachedDrawable;}}...}
可以看出来,源码中会先判断是否缓存了该
resId和Theme所对应的Drawable,如果缓存了,就返回了缓存这的对象,这也是getResources.getDrawable()方法返回同一个Drawable对象的原因。
我们上面也解决了这个问题了,里面涉及到了ConstantState,我们详细了解一下。如果把Drawable比作一个绘制容器,那么ConstantState就是容器中真正的内容。
每个
Drawable类对象类都关联有一个ConstantState类对象,这是为了保存Drawable类对象的一些恒定不变的数据,如果从同一个res中创建的Drawable类对象,为了节约内存,它们会共享同一个ConstantState类对象。
关键的源码如下:
public abstract class Drawable {public ConstantState getConstantState() {return null;}public Drawable mutate() {return this;}public static abstract class ConstantState {public abstract Drawable newDrawable();public Drawable newDrawable(Resources res) {return newDrawable();}public Drawable newDrawable(Resources res, Theme theme) {return newDrawable(null);}public abstract int getChangingConfigurations();public boolean canApplyTheme() {return false;}}}
从源码中看,
ConstantState其实是Drawable中的静态抽象类,并且getConstantState()方法默认返回null,这我们可以推测ConstantState类和getConstantState()方法会被具体的·Drawable·实现类继承和重写。这样不难想象,由于不同Drawbale,如BitmapDrawable、GradientDrawable这些自身存储的绘制内容数据原则上是不一样的,这就意味着Drawable为了易于扩展,ConstantState对象应该会存储属性可变的数据。
比如一个 ColorDrawable 类对象,它会关联一个 ColorState 类对象,color 的颜色值是保存在 ColorState 类对象中的。如果修改 ColorDrawable 的颜色值,会修改到 ColorState 的值,会导致和 ColorState 关联的所有的 ColorDrawable。 的颜色都改变。就向我们上面遇到的问题一样,所有的drawable都会改变。
接下来,我们具体看一下ColorDrawable
public class ColorDrawable extends Drawable {@Overridepublic Drawable mutate() {if (!mMutated && super.mutate() == this) {mColorState = new ColorState(mColorState);mMutated = true;}return this;}@Overridepublic void setTintList(ColorStateList tint) {mColorState.mTint = tint;mTintFilter = updateTintFilter(mTintFilter, tint,mColorState.mTintMode);invalidateSelf();}@Overridepublic ConstantState getConstantState() {return mColorState;}final static class ColorState extends ConstantState {ColorStateList mTint = null;@Overridepublic Drawable newDrawable() {return new ColorDrawable(this, null, null);}@Overridepublic Drawable newDrawable(Resources res) {return new ColorDrawable(this, res, null);}@Overridepublic Drawable newDrawable(Resources res, Theme theme) {return new ColorDrawable(this, res, theme);}}private ColorDrawable(ColorState state,Resources res,Theme theme) {if (theme != null && state.canApplyTheme()) {mColorState = new ColorState(state);applyTheme(theme);} else {mColorState = state;}mTintFilter = updateTintFilter(mTintFilter, state.mTint,state.mTintMode);}}
其实通过源码我们注意到,setTintList()其实就是设置的ColorState里面的成员变量,所以说,如果修改 ColorDrawable 的颜色值,会修改到 ColorState 的值,Drawable比作一个绘制容器,那么ConstantState就是容器中真正的内容。
在ColorState的newDrawable()就是调用了一个私有的构造方法,传入了当前ColorDrawable的ColorState,在mutate()里面,也就是new了一个ColorState对象,重新赋值给当前ColorDrawable的mColorState,也就是deepCopy了一份当前的数据。
所以说,我们先调用newDrawable(),然后会把当前的ColorDrawable1的mColorState传递给新new的ColorDrawable2,然后我们调用ColorDrawable2的mutate(),就会重新new一个ColorState,但是这些ColorState数据都是一样的,就实现了一个DeepCopy。
就像这张图所示的:
newDrawable()区分了Drawablemutate()区分了ConstantStateAndroid Support V4 的包中提供了 DrawableCompat 类,我们可以利用这个兼容类,来实现着色。Drawable比作一个绘制容器,那么ConstantState就是容器中真正的内容。ConstantState是Drawable的静态内部类。具体的实现都在Drawable子类里面newDrawable()会新new一个Drawable,参数是当前Drawable的ConstantStatemutate()会新new一个ConstantState,参数是当前的ConstantStateAndroid开发中使用一张图片实现变暗或缩小的点击效果
自定义控件其实很简单1/6
Drawable 着色的后向兼容方案
View绘制Drawable原理分析记录