@TryLoveCatch
2022-05-05T16:42:22.000000Z
字数 7821
阅读 2354
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 只有一份拷贝
这个从源码中可以印证:
@Nullable
Drawable 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 {
@Override
public Drawable mutate() {
if (!mMutated && super.mutate() == this) {
mColorState = new ColorState(mColorState);
mMutated = true;
}
return this;
}
@Override
public void setTintList(ColorStateList tint) {
mColorState.mTint = tint;
mTintFilter = updateTintFilter(mTintFilter, tint,
mColorState.mTintMode);
invalidateSelf();
}
@Override
public ConstantState getConstantState() {
return mColorState;
}
final static class ColorState extends ConstantState {
ColorStateList mTint = null;
@Override
public Drawable newDrawable() {
return new ColorDrawable(this, null, null);
}
@Override
public Drawable newDrawable(Resources res) {
return new ColorDrawable(this, res, null);
}
@Override
public 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()
区分了Drawable
mutate()
区分了ConstantState
Android Support V4
的包中提供了 DrawableCompat
类,我们可以利用这个兼容类,来实现着色。Drawable
比作一个绘制容器,那么ConstantState
就是容器中真正的内容。ConstantState
是Drawable
的静态内部类。具体的实现都在Drawable
子类里面newDrawable()
会新new一个Drawable
,参数是当前Drawable
的ConstantState
mutate()
会新new一个ConstantState
,参数是当前的ConstantState
Android开发中使用一张图片实现变暗或缩小的点击效果
自定义控件其实很简单1/6
Drawable 着色的后向兼容方案
View绘制Drawable原理分析记录