1. 背景
在 App 当中,会有很多 形状相同、颜色不同 的 Icon。

例如上图这个场景,是筛选项的 icon,对应的代码可能为:
- Xml 中初始化颜色为黑色
xml
<ImageView
...
android:src="@drawable/icon_filter_black" />
- 代码中根据筛选状态,切换不同颜色的 icon
kotlin
// XXXFragment.kt
imageView.setImageDrawable(
if (isSelect) {
// 筛选后为橙色
resources.getDrawable(R.drawable.icon_filter_orange);
} else {
// 未筛选为黑色
resources.getDrawable(R.drawable.icon_filter_black);
}
)
如果为每种颜色的 Icon 都保存一张图片,会增加包体积。同时,会增加设计的工作负担。有没有什么办法可以做到只需要一张图片,然后动态的着色呢?
2. 使用 Tint 动态着色
2.1 使用方式
有以下几种方式:
- xml 中设置
xml
<ImageView
...
android:src="@drawable/icon_filter"
app:tint="@color/black"
/>
- 代码中为 ImageView 设置
kotlin
imageView.imageTintList = ColorStateList(...)
- 代码中直接为 Drawable 设置
kotlin
val drawable = resources.getDrawable(R.drawable.icon_filter)
drawable.setTint = Color.BLACK
imageView.setImageDrawable(drawable)
设置 tint 后,绘制时就会用 tint 的颜色渲染 icon。这样,就算只有一张图片,也可以渲染为多种颜色。很大程度的节省了包体积。
2.2 原理分析
- ImageView 会将 Tint 传给 Drawable
java
// ImageView.java
public void setImageTintList(@Nullable ColorStateList tint) {
mDrawableTintList = tint;
mHasDrawableTint = true;
applyImageTint();
}
private void applyImageTint() {
if (mDrawable != null && (mHasDrawableTint || mHasDrawableBlendMode)) {
mDrawable = mDrawable.mutate();
if (mHasDrawableTint) {
mDrawable.setTintList(mDrawableTintList); // 将 tint 传递给 Drawable!
}
// ...
}
}
- Drawable 在 收到 Tint 时会创建 ColorFilter,在 draw 时会用它做颜色过滤处理。这里拿 VectorDrawable 举例:
java
public class VectorDrawable extends Drawable {
private BlendModeColorFilter mBlendModeColorFilter ;
@Override
public void setTintList(ColorStateList tint) {
// 使用 tint 创建 ColorFilter
updateColorFilters(... , tint) ;
}
private void updateColorFilters(@Nullable BlendMode blendMode , ColorStateList tint) {
// ...
mBlendModeColorFilter = updateBlendModeFilter(mBlendModeColorFilter , tint , blendMode) ;
}
@Override
public void draw(Canvas canvas) {
// 绘制时,如果外部没有设置 mColorFilter,则使用 mBlendModeColorFilter
final ColorFilter colorFilter = (mColorFilter == null ? mBlendModeColorFilter : mColorFilter) ;
// ...
}
}
public abstract class Drawable {
@Nullable
BlendModeColorFilter updateBlendModeFilter(@Nullable ColorStateList tint , ... ) {
// ...
final int color = tint.getColorForState(getState() , Color.TRANSPARENT) ;
if (blendFilter == null || blendFilter.getColor() != color
|| blendFilter.getMode() != blendMode) {
return new BlendModeColorFilter(color , blendMode) ;
}
return blendFilter ;
}
}
3. 注意事项
3.1 同时设置 ImageView 与 Drawable,Drawable 的设置会失效
- 例如,我们在 xml 中设置 tint 为黑色
xml
<ImageView
...
android:src="@drawable/icon_filter"
app:tint="@color/black"
/>
- 在代码中,为 Drawable 设置为黄色
kotlin
val drawable = resources.getDrawable(R.drawable.icon_filter)
drawable.setTint = Color.YELLOW
imageView.setImageDrawable(drawable)
- 此时,对 Drawable 的设置是不生效的,因为此时 ImageView.setImageDrawable 会覆盖掉 Drawable 自身的 tint
kotlin
// ImageView.kt
public void setImageDrawable(@Nullable Drawable drawable) {
updateDrawable(drawable) ;
}
private void updateDrawable(Drawable d) {
applyImageTint() ;
}
private void applyImageTint() {
if (mHasDrawableTint) {
mDrawable.setTintList(mDrawableTintList) ;
}
}
- 所以如果同时设置 ImageView 与 Drawable 的 tint,要关注覆盖问题。