[译] 使用 ColorFilter 快速地主题化

原文地址:Fast Android asset theming with ColorFilter

从 Android L 开始,Material Theme 能够借助某些属性主题化 APP(图标及其他)

1
2
3
4
5
<style name="AppTheme" parent="android:Theme.Material">
<item name="android:colorPrimary">@color/primary</item>
<item name="android:colorPrimaryDark">@color/primary_dark</item>
<item name="android:colorAccent">@color/accent</item>
</style>

这种方式能够节省很多时间。只需修改一次就能改变整个 APP 的颜色,或调整单个 drawable 的颜色,而不需要找设计师拿新的资源。

我们可以通过查看 ImageView 的 android:tint 属性来了解这种方式是如何执行的。你可以在 xml 中设置一个 tint color,噢!资源的颜色已经不一样了!

对于 ImageView 的 tint 属性,应当注意两点:

  • ImageView 的 tint 会混合 tint color 以及原来的资源。你希望 tint color 为主,而不是将 tint 叠加于现有颜色的上方。例如,如果原来资源是黑色的(#FF000000),而你希望它变成 #77FFFFFF(半透白色阴影),最终你得到的是白色阴影叠加在黑色背景之上。

  • android:tint 仅限于 ImageView,但你想全部有 Drawable 的 View 也能实现这种效果。

通过阅读 ImageView 中 tint 相关的实现,可以知道通过 ColorFilter 即能轻易地解决上述两个问题。更详细地,ImageView 使用了 Porter/Duff 将相应颜色应用到图片的上层来合成而达到 tint 的效果。

为了解决第一个问题,我们只需要使用不同的 Porter/Duff 模式。ImageView 使用的是 PorterDuff.Mode.SRC_ATOP,该模式将 tint color 叠加于图片的上层中(但仍然将原图绘制在下方)。对于主题化,我们实际需要的是 PorterDuff.Mode.SRC_IN,该模式只会绘制 tint color。

第二个问题也能轻易地避免,因为所有 Drawable 对象都有 setColorFilter() 方法。

利用该方法将相应主题颜色应用到 View 的背景中的简单实例:

1
2
3
4
5
6
Resources res = getResources();
Drawable background = res.getDrawable(R.drawable.background);
int primaryColor = res.getColor(R.color.primary);
background.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN);
View view = findViewById(R.id.my_view);
view.setBackgroundDrawable(background);

你也可以封装一个可复用的用于创建不同颜色 Drawable 的方法:

1
2
3
4
5
6
7
public Drawable getTintedDrawable(Resources res,
@DrawableRes int drawableResId, @ColorRes int colorResId) {
Drawable drawable = res.getDrawable(drawableResId);
int color = res.getColor(colorResId);
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
return drawable;
}

(附注:使用 @DrawableRes@ColorRes 注解可以帮助我们避免调用方法时传入错误的参数。)

虽然这种方式不如 Android L 那么方便地主题化,但该方法可向下兼容,所以你也可以考虑使用。

附录

Added July 24, 2015

提醒一句:使用 ColorFilter 时会影响到 Drawable 的状态共享。

作为优化,Drawable 会在应用内共享。例如,在不同的 Toolbar 中使用相同的 Action Bar 图标实际上只会产生一个 Drawable。同样地,如果你使用 ColorFilter,你可能会发现所有的 Drawable 都会发生变化即使你希望只改变其中一个。

这篇文章更深入地介绍了这种优化策略。如果你正在使用 ColorFilter 并且不希望改变所有的 Drawable,确保先调用 Drawable.mutate() 再执行其他操作。