虽然现在已经有很多不错的换肤方案,但是这些方案或多或少都存在自己的问题。在这篇文章中,我将对 Android 现有的一些动态换肤方案进行梳理,对其底层实现原理进行分析,然后对开发一个新的换肤方案的可能性进行总结。
1、通过自定义 style 换肤
1.1 方案的基本原理
这种方案是我之前用得比较多的一种方案。我在使用的时候也做了很多的调整。开源版本可以参考 Colorful 这个库.
它的实现方式是:用户提前自定义一些 theme 主题,然后当设置主题的时候将制定主题对应的 id 记录到本地文件中,当 Activity RESUME 的时候,判断 Activity 当前的主题是否和之前设置的主题一致,不一致的话就调用当前 Activity 的 recreate()
方法进行重建。
在这种方案中还可以通过如下的方式预定义一些属性,
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="themed_divider_color" format="color"/>
<attr name="themed_foreground" format="color"/>
<!-- .... -->
</resources>
然后在自定义主题中使用为这些预定义属性赋值,
<style name="Base.AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="themed_foreground">@color/warm_theme_foreground</item>
<item name="themed_background">@color/warm_theme_background</item>
<!-- ... -->
</style>
最后在布局文件中通过如下的方式引用这些自定义属性,
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv"
android:textColor="?attr/themed_text_color_secondary"
... />
<View android:background="?attr/themed_divider_color"
android:layout_width="match_parent"
android:layout_height="1px"/>
这种引用方式的好处是只要切换了主题这些自定义属性可以动态发生变化。
1.2 对该方案的总结
这种方案在换肤之后需要重启 Activity,代价有些高,特别是当主页存在多个嵌套 Fragment 的时候,状态处理起来可能会特别复杂。对于简单类型的应用,这种方案是一种方便、快捷的选择。
2、通过 hook LayoutInflater 的换肤方案
2.1 LayoutInflater 的工作原理
通过 Hook LayoutInflater 进行换肤的方案是众多开源方案中比较常见的一种。在分析这种方案之前,我们最好先了解下 LayoutInflater 的工作原理。
通常当我们想要自定义 Layout 的 Factory 的时候可以调用下面两个方法将我们的 Factory 设置到系统的 LayoutInflater 中,
public abstract class LayoutInflater {
public void setFactory(Factory factory) {
if (mFactorySet) throw new IllegalStateException("A factory has already been set on this LayoutInflater");
if (factory == null) throw new NullPointerException("Given factory can not be null");
mFactorySet = true;
if (mFactory == null) {
mFactory = factory;
} else {
mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
}
}
public void setFactory2(Factory2 factory) {
if (mFactorySet) throw new IllegalStateException("A factory has already been set on this LayoutInflater");
if (factory == null) throw new NullPointerException("Given factory can not be null");
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
// ...
}
从上面的两个方法看出,setFactory()
方法底层有防重入校验,所以,如果想要手动进行赋值,需要使用反射修改 mFactorySet
、mFactory
和 mFactory2
。
那么 mFactory
和 mFactory2
时如何使用的呢?
当我们调用 inflator 从 xml 中加载控件的时候,将会走到如下代码真正执行加载操作,
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
// ....
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
// 处理 merge 标签
if (TAG_MERGE.equals(name)) {
rInflate(parser, root, inflaterContext, attrs, false);
} e