对 Android 应用换肤方案的总结

作者:me

虽然现在已经有很多不错的换肤方案,但是这些方案或多或少都存在自己的问题。在这篇文章中,我将对 Android 现有的一些动态换肤方案进行梳理,对其底层实现原理进行分析,然后对开发一个新的换肤方案的可能性进行总结。

1通过自定义 style 换肤

1.1 方案的基本原理

这种方案是我之前用得比较多的一种方案。我在使用的时候也做了很多的调整。开源版本可以参考 Colorful 这个库。

https://github.com/garretyoder/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);
            } else {
                // 从 xml 中加载布局控件
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                // 生成布局参数 LayoutParams
                ViewGroup.LayoutParams params = null;
                if (root != null) {
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        temp.setLayoutParams(params);
                    }
                }
                // 加载子控件
                rInflateChildren(parser, temp, attrs, true);
                // 添加到根控件
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {/*...*/}
        return result;
    }
}

先来看通过 tag 创建 view 的逻辑。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
    // 老的布局方式
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }
    // 处理 theme
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }
    try {
        View view = tryCreateView(parent, name, context, attrs);
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值