关于android:Android-换肤指南

10次阅读

共计 26174 个字符,预计需要花费 66 分钟才能阅读完成。

一、换肤计划

目前,市面上 Android 的换肤计划次要有 Resource 计划和 AssetManager 替换计划两种计划。

其中,Resource 计划是用户提前自定义一些主题,而后将指定主题对应的 id 设置成默认的主题即可。而 AssetManager 替换计划,应用的是 Hook 零碎 AssetMananger 对象,而后再编译期动态对齐资源文件对应的 id 数值。

1.1 Resource 计划

Resource 计划的原理大略如下:

1、创立新的 Resrouce 对象(代理的 Resource)

2、替换零碎 Resource 对象

3、运行时动静映射(原理雷同资源在不同的资源表中的 Type 和 Name 一样)

4、xml 布局解析拦挡(xml 布局中的资源不能通过代理 Resource 加载,LayoutInflater)

此计划的劣势是反对 String/Layout 的替换,不过毛病也很显著:

  • 资源获取效率有影响
  • 不反对 style、asset 目录
  • Resource 多出替换,Resource 包装类代码量大

    1.2 AssetManager 计划

应用的是 Hook 零碎 AssetMananger 对象,而后再编译期动态对齐资源文件对应的 id 数值,达到替换资源的目标。此种计划,最常见的就是 Hook LayoutInflater 进行换肤。

二、Resource 换肤

此种形式采纳的计划是:用户提前自定义一些主题,而后当设置主题的时候将指定主题对应的 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"/>

三、Hook LayoutInflater 计划

3.1 工作原理

通过 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);
        }
    }
    
}

当咱们调用 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;
    }
}

接下来,咱们看一下 createViewFromTag() 办法。

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('.')) {view = onCreateView(context, parent, name, attrs);
                } else {view = createView(context, name, null, attrs);
                }
            } finally {mConstructorArgs[0] = lastContext;
            }
        }
        return view;
    } catch (InflateException e) {// ...}
}

public final View tryCreateView(View parent, String name, Context context, AttributeSet attrs) {if (name.equals(TAG_1995)) {return new BlinkLayout(context, attrs);
    }

    // 优先应用 mFactory2 创立 view,mFactory2 为空则应用 mFactory,否则应用 mPrivateFactory
    View view;
    if (mFactory2 != null) {view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {view = mFactory.onCreateView(name, context, attrs);
    } else {view = null;}

    if (view == null && mPrivateFactory != null) {view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }
    return view;
}

能够看出,这里优先应用 mFactory2 创立 view,mFactory2 为空则应用 mFactory,否则应用 mPrivateFactory 加载 view。所以,如果咱们想要对 view 创立过程进行 hook,就应该 hook 这里的 mFactory2,因为它的优先级最高。
留神到这里的 办法中并没有循环,所以,第一次的时候只能加载根布局。那么根布局内的子控件是如何加载的呢?这就用到了 inflaterInflateChildren() 办法。

 final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;

    while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {if (type != XmlPullParser.START_TAG) continue;

        final String name = parser.getName();
        if (TAG_REQUEST_FOCUS.equals(name)) {
            // 解决 requestFocus 标签
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            // 解决 tag 标签
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            // 解决 include 标签
            if (parser.getDepth() == 0) {throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            // 解决 merge 标签
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 这里解决的是一般的 view 标签
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            // 持续解决子控件
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }
    if (pendingRequestFocus) {parent.restoreDefaultFocus();
    }
    if (finishInflate) {parent.onFinishInflate();
    }
}

留神到该办法外部又调用了 createViewFromTag 和 rInflateChildren 办法,也就是说,这里通过递归的形式实现对整个 view 树的遍历,从而将整个 xml 加载为 view 树。以上是安卓的 LayoutInflater 从 xml 中加载控件的逻辑,能够看出咱们能够通过 hook 实现对创立 view 的过程的“监听”。
下面咱们说了下换肤的原理,上面咱们介绍几种 Android 换肤的技术框架:Android-Skin-Loader、ThemeSkinning 和 Android-skin-support。

3.2 Android-Skin-Loader

3.2.1 应用流程

学习了 Hook LayoutInflator 的底层原理之后,咱们来看几个基于这种原理实现的换肤计划。首先是 Android-Skin-Loader 这个库,这个库须要你覆写 Activity,而后再替换皮肤,Activity 局部代码如下。

public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{

    private SkinInflaterFactory mSkinInflaterFactory;

    @Override
    protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
        mSkinInflaterFactory = new SkinInflaterFactory();
        getLayoutInflater().setFactory(mSkinInflaterFactory);
    }

    // ...
}

能够看出,这里将自定义的 Factory 设置给了 LayoutInflator,SkinInflaterFactory 的实现如下:

public class SkinInflaterFactory implements Factory {

    private static final boolean DEBUG = true;
    private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        // 读取自定义属性 enable,这里用了自定义的 namespace
        boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
        if (!isSkinEnable){return null;}
        // 创立 view
        View view = createView(context, name, attrs);
        if (view == null){return null;}
        parseSkinAttr(context, attrs, view);
        return view;
    }

    private View createView(Context context, String name, AttributeSet attrs) {
        View view = null;
        try {
            // 兼容低版本创立 view 的逻辑(低版本是没有完整包名)if (-1 == name.indexOf('.')){if ("View".equals(name)) {view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
                } 
                if (view == null) {view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
                } 
                if (view == null) {view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);
                } 
            } else {
                // 新的创立 view 的逻辑
                view = LayoutInflater.from(context).createView(name, null, attrs);
            }
        } catch (Exception e) {view = null;}
        return view;
    }

    private void parseSkinAttr(Context context, AttributeSet attrs, View view) {List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
        // 对 xml 中控件的属性进行解析
        for (int i = 0; i < attrs.getAttributeCount(); i++){String attrName = attrs.getAttributeName(i);
            String attrValue = attrs.getAttributeValue(i);
            // 判断属性是否反对,属性是预约义的
            if(!AttrFactory.isSupportedAttr(attrName)){continue;}
            // 如果是援用类型的属性值
            if(attrValue.startsWith("@")){
                try {int id = Integer.parseInt(attrValue.substring(1));
                    String entryName = context.getResources().getResourceEntryName(id);
                    String typeName = context.getResources().getResourceTypeName(id);
                    // 退出属性列表
                    SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
                    if (mSkinAttr != null) {viewAttrs.add(mSkinAttr);
                    }
                } catch (NumberFormatException e) {/*...*/}
            }
        }
        if(!ListUtils.isEmpty(viewAttrs)){
            // 构建该控件的属性关系
            SkinItem skinItem = new SkinItem();
            skinItem.view = view;
            skinItem.attrs = viewAttrs;
            mSkinItems.add(skinItem);
            if(SkinManager.getInstance().isExternalSkin()){skinItem.apply();
            }
        }
    }
}

这里自定义了一个 xml 属性,用来指定是否启用换肤配置。而后在创立 view 的过程中解析 xml 中定义的 view 的属性信息,比方,background 和 textColor 等属性。并将其对应的属性、属性值和控件以映射的模式记录到缓存中。当产生换肤的时候依据这里的映射关系在代码中更新控件的属性信息。

 public class BackgroundAttr extends SkinAttr {

    @Override
    public void apply(View view) {if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
            // 留神这里获取属性值的时候是通过 SkinManager 的办法获取的
            view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
        }else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
            view.setBackground(bg);
        }
    }
}

如果是动静增加的 view,比方在 java 代码中,该库提供了 等办法来动静增加映射关系到缓存中。在 activity 的生命周期办法中注册监听换肤事件:

public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{
    @Override
    protected void onResume() {super.onResume();
        SkinManager.getInstance().attach(this);
    }

    @Override
    protected void onDestroy() {super.onDestroy();
        SkinManager.getInstance().detach(this);
        // 清理缓存数据
        mSkinInflaterFactory.clean();}

    @Override
    public void onThemeUpdate() {if(!isResponseOnSkinChanging){return;}
        mSkinInflaterFactory.applySkin();}
    // ... 
}

当换肤的时候会告诉到 Activity 并触发 onThemeUpdate 办法,接着调用 SkinInflaterFactory 的 apply 办法。SkinInflaterFactory 的 apply 办法中对缓存的属性信息遍历更新实现换肤。

3.2.2 皮肤包加载逻辑

接下来,咱们看一下皮肤包的加载逻辑,即通过自定义的 AssetManager 实现,相似于插件化。

public void load(String skinPackagePath, final ILoaderListener callback) {new AsyncTask<String, Void, Resources>() {protected void onPreExecute() {if (callback != null) {callback.onStart();
            }
        };

        @Override
        protected Resources doInBackground(String... params) {
            try {if (params.length == 1) {String skinPkgPath = params[0];

                    File file = new File(skinPkgPath); 
                    if(file == null || !file.exists()){return null;}

                    PackageManager mPm = context.getPackageManager();
                    PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
                    skinPackageName = mInfo.packageName;

                    AssetManager assetManager = AssetManager.class.newInstance();
                    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                    addAssetPath.invoke(assetManager, skinPkgPath);

                    Resources superRes = context.getResources();
                    Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());

                    SkinConfig.saveSkinPath(context, skinPkgPath);

                    skinPath = skinPkgPath;
                    isDefaultSkin = false;
                    return skinResource;
                }
                return null;
            } catch (Exception e) {/*...*/}
        };

        protected void onPostExecute(Resources result) {
            mResources = result;
            if (mResources != null) {if (callback != null) callback.onSuccess();
                notifySkinUpdate();}else{
                isDefaultSkin = true;
                if (callback != null) callback.onFailed();}
        };
    }.execute(skinPackagePath);
}

而后,在获取值的时候应用上面的办法:

public int getColor(int resId){int originColor = context.getResources().getColor(resId);
    if(mResources == null || isDefaultSkin){return originColor;}

    String resName = context.getResources().getResourceEntryName(resId);
    int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
    int trueColor = 0;

    try{trueColor = mResources.getColor(trueResId);
    }catch(NotFoundException e){e.printStackTrace();
        trueColor = originColor;
    }
    return trueColor;
}

3.2.3 计划特点

此种计划换肤,有如下的一些特点:

  • 换肤须要继承自定义 activity
  • 皮肤包和 APK 如果应用了资源混同加载的时候就会呈现问题
  • 没解决属性值通过 的模式援用的状况?attr
  • 每个换肤的属性须要本人注册并实现
  • 有些控件的一些属性可能没有提供对应的 java 办法,因而在代码中换肤就行不通
  • 没有解决应用 style 的状况
  • 基于 实现,版本太老 android.app.Activity
  • 在 inflator 创立 view 的时候,其实只做了对属性的拦挡解决操作,能够通过代理零碎的 Factory 实现创立 view 的操作

    3.3 ThemeSkinning

这个库是基于下面的 Android-Skin-Loader 开发的,在其根底之上做了许多的调整,其地址是 ThemeSkinning。次要调整的内容如下:

3.3.1 AppCompactActivity 调整

该库基于 AppCompactActivity 和 LayoutInflaterCompat.setFactory 开发,改变的内容如下:

public class SkinBaseActivity extends AppCompatActivity implements ISkinUpdate, IDynamicNewView {

    private SkinInflaterFactory mSkinInflaterFactory;
    private final static String TAG = "SkinBaseActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {mSkinInflaterFactory = new SkinInflaterFactory(this);
        LayoutInflaterCompat.setFactory2(getLayoutInflater(), mSkinInflaterFactory);
        super.onCreate(savedInstanceState);
        changeStatusColor();}

    // ...
}

同时,该库也提供了批改状态栏的办法,尽管能力比拟无限。

3.3.2 SkinInflaterFactory 调整

SkinInflaterFactory 对创立 View 做了一些调整,代码如下:

public class SkinInflaterFactory implements LayoutInflater.Factory2 {private Map<View, SkinItem> mSkinItemMap = new HashMap<>();
    private AppCompatActivity mAppCompatActivity;

    public SkinInflaterFactory(AppCompatActivity appCompatActivity) {this.mAppCompatActivity = appCompatActivity;}

    @Override
    public View onCreateView(String s, Context context, AttributeSet attributeSet) {return null;}

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // 沿用之前的一些逻辑
        boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
        AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
        View view = delegate.createView(parent, name, context, attrs);

        // 对字体兼容做了反对,这里是通过动态形式将其缓存到内存,动静新增和移除,加载字体之后调用 textview 的 settypeface 办法替换
        if (view instanceof TextView && SkinConfig.isCanChangeFont()) {TextViewRepository.add(mAppCompatActivity, (TextView) view);
        }

        if (isSkinEnable || SkinConfig.isGlobalSkinApply()) {if (view == null) {
                // 创立 view 的逻辑做了调整
                view = ViewProducer.createViewFromTag(context, name, attrs);
            }
            if (view == null) {return null;}
            parseSkinAttr(context, attrs, view);
        }
        return view;
    }

    // ...
}

以下是 View 的创立逻辑的相干代码:

class ViewProducer {private static final Object[] mConstructorArgs = new Object[2];
    private static final Map<String, Constructor<? extends View>> sConstructorMap = new ArrayMap<>();
    private static final Class<?>[] sConstructorSignature = new Class[]{Context.class, AttributeSet.class};
    private static final String[] sClassPrefixList = {"android.widget.", "android.view.", "android.webkit."};

    static View createViewFromTag(Context context, String name, AttributeSet attrs) {if (name.equals("view")) {name = attrs.getAttributeValue(null, "class");
        }

        try {
            // 结构参数,缓存,复用
            mConstructorArgs[0] = context;
            mConstructorArgs[1] = attrs;

            if (-1 == name.indexOf('.')) {for (int i = 0; i < sClassPrefixList.length; i++) {final View view = createView(context, name, sClassPrefixList[i]);
                    if (view != null) {return view;}
                }
                return null;
            } else {
                // 通过构造方法创立 view
                return createView(context, name, null);
            }
        } catch (Exception e) {return null;} finally {mConstructorArgs[0] = null;
            mConstructorArgs[1] = null;
        }
    }

    // ...
}

3.3.3 对 style 的兼容解决

private void parseSkinAttr(Context context, AttributeSet attrs, View view) {List<SkinAttr> viewAttrs = new ArrayList<>();
    for (int i = 0; i < attrs.getAttributeCount(); i++) {String attrName = attrs.getAttributeName(i);
        String attrValue = attrs.getAttributeValue(i);
        if ("style".equals(attrName)) {
            // 对 style 的解决,从 theme 中获取 TypedArray 而后获取 resource id,再获取对应的信息
            int[] skinAttrs = new int[]{android.R.attr.textColor, android.R.attr.background};
            TypedArray a = context.getTheme().obtainStyledAttributes(attrs, skinAttrs, 0, 0);
            int textColorId = a.getResourceId(0, -1);
            int backgroundId = a.getResourceId(1, -1);
            if (textColorId != -1) {String entryName = context.getResources().getResourceEntryName(textColorId);
                String typeName = context.getResources().getResourceTypeName(textColorId);
                SkinAttr skinAttr = AttrFactory.get("textColor", textColorId, entryName, typeName);
                if (skinAttr != null) {viewAttrs.add(skinAttr);
                }
            }
            if (backgroundId != -1) {String entryName = context.getResources().getResourceEntryName(backgroundId);
                String typeName = context.getResources().getResourceTypeName(backgroundId);
                SkinAttr skinAttr = AttrFactory.get("background", backgroundId, entryName, typeName);
                if (skinAttr != null) {viewAttrs.add(skinAttr);
                }
            }
            a.recycle();
            continue;
        }
        if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) {
            // 老逻辑
            try {
                //resource id
                int id = Integer.parseInt(attrValue.substring(1));
                if (id == 0) continue;
                String entryName = context.getResources().getResourceEntryName(id);
                String typeName = context.getResources().getResourceTypeName(id);
                SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
                if (mSkinAttr != null) {viewAttrs.add(mSkinAttr);
                }
            } catch (NumberFormatException e) {/*...*/}
        }
    }
    if (!SkinListUtils.isEmpty(viewAttrs)) {SkinItem skinItem = new SkinItem();
        skinItem.view = view;
        skinItem.attrs = viewAttrs;
        mSkinItemMap.put(skinItem.view, skinItem);
        if (SkinManager.getInstance().isExternalSkin() ||
                SkinManager.getInstance().isNightMode()) {// 如果以后皮肤来自于内部或者是处于夜间模式
            skinItem.apply();}
    }
}

3.3.4 fragment 调整

在 Fragment 的生命周期办法完结的时候从缓存当中移除指定的 View。

@Override
public void onDestroyView() {removeAllView(getView());
    super.onDestroyView();}

protected void removeAllView(View v) {if (v instanceof ViewGroup) {ViewGroup viewGroup = (ViewGroup) v;
        for (int i = 0; i < viewGroup.getChildCount(); i++) {removeAllView(viewGroup.getChildAt(i));
        }
        removeViewInSkinInflaterFactory(v);
    } else {removeViewInSkinInflaterFactory(v);
    }
}

这种计划绝对第一个框架改良了很多,然而此库曾经有 4,5 年没有保护了,组件和代码都比拟老。

3.4 Android-skin-support

接下来,咱们再看一下 Android-skin-support。次要批改的局部如下:

3.4.1 主动注册 layoutinflator.factory

public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {private SkinActivityLifecycle(Application application) {application.registerActivityLifecycleCallbacks(this);
        installLayoutFactory(application);
        // 注册监听
        SkinCompatManager.getInstance().addObserver(getObserver(application));
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {if (isContextSkinEnable(activity)) {installLayoutFactory(activity);
            // 更新 acitvity 的窗口的背景
            updateWindowBackground(activity);
            // 触发换肤... 如果 view 没有创立是不是就容易导致 NPE?
            if (activity instanceof SkinCompatSupportable) {((SkinCompatSupportable) activity).applySkin();}
        }
    }

    private void installLayoutFactory(Context context) {
        try {LayoutInflater layoutInflater = LayoutInflater.from(context);
            LayoutInflaterCompat.setFactory2(layoutInflater, getSkinDelegate(context));
        } catch (Throwable e) {/* ... */}
    }

    // 获取 LayoutInflater.Factory2,这里加了一层缓存
    private SkinCompatDelegate getSkinDelegate(Context context) {if (mSkinDelegateMap == null) {mSkinDelegateMap = new WeakHashMap<>();
        }
        SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
        if (mSkinDelegate == null) {mSkinDelegate = SkinCompatDelegate.create(context);
            mSkinDelegateMap.put(context, mSkinDelegate);
        }
        return mSkinDelegate;
    }
    // ...
}

LayoutInflaterCompat.setFactory2() 办法源码如下:

public final class LayoutInflaterCompat {public static void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {inflater.setFactory2(factory);
        if (Build.VERSION.SDK_INT < 21) {final LayoutInflater.Factory f = inflater.getFactory();
            if (f instanceof LayoutInflater.Factory2) {forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
            } else {forceSetFactory2(inflater, factory);
            }
        }
    }

    // 通过反射的形式间接批改 mFactory2 字段
    private static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {if (!sCheckedField) {
            try {sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
                sLayoutInflaterFactory2Field.setAccessible(true);
            } catch (NoSuchFieldException e) {/* ... */}
            sCheckedField = true;
        }
        if (sLayoutInflaterFactory2Field != null) {
            try {sLayoutInflaterFactory2Field.set(inflater, factory);
            } catch (IllegalAccessException e) {/* ... */}
        }
    }
    // ...
}

3.4.2 LayoutInflater.Factory2

public class SkinCompatDelegate implements LayoutInflater.Factory2 {
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {View view = createView(parent, name, context, attrs);
        if (view == null) return null;
        // 退出缓存
        if (view instanceof SkinCompatSupportable) {mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
        }
        return view;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {View view = createView(null, name, context, attrs);
        if (view == null) return null;
        // 退出缓存,继承这个接口的次要是 view 和 activity 这些
        if (view instanceof SkinCompatSupportable) {mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
        }
        return view;
    }

    public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        // view 生成逻辑被包装成了 SkinCompatViewInflater
        if (mSkinCompatViewInflater == null) {mSkinCompatViewInflater = new SkinCompatViewInflater();
        }
        List<SkinWrapper> wrapperList = SkinCompatManager.getInstance().getWrappers();
        for (SkinWrapper wrapper : wrapperList) {Context wrappedContext = wrapper.wrapContext(mContext, parent, attrs);
            if (wrappedContext != null) {context = wrappedContext;}
        }
        // 
        return mSkinCompatViewInflater.createView(parent, name, context, attrs);
    }
    // ...
}

3.4.3 SkinCompatViewInflater

上述办法中 SkinCompatViewInflater 获取 view 的逻辑如下。

public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
    // 通过 inflator 创立 view
    View view = createViewFromHackInflater(context, name, attrs);
    if (view == null) {view = createViewFromInflater(context, name, attrs);
    }
    // 依据 view 标签创立 view
    if (view == null) {view = createViewFromTag(context, name, attrs);
    }
    // 解决 xml 中设置的点击事件
    if (view != null) {checkOnClickListener(view, attrs);
    }
    return view;
}

private View createViewFromHackInflater(Context context, String name, AttributeSet attrs) {
    View view = null;
    for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getHookInflaters()) {view = inflater.createView(context, name, attrs);
        if (view == null) {continue;} else {break;}
    }
    return view;
}

private View createViewFromInflater(Context context, String name, AttributeSet attrs) {
    View view = null;
    for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {view = inflater.createView(context, name, attrs);
        if (view == null) {continue;} else {break;}
    }
    return view;
}

public View createViewFromTag(Context context, String name, AttributeSet attrs) {
    // <view class="xxxx"> 模式的 tag,和 <xxxx> 一样
    if ("view".equals(name)) {name = attrs.getAttributeValue(null, "class");
    }
    try {
        // 结构参数缓存
        mConstructorArgs[0] = context;
        mConstructorArgs[1] = attrs;
        if (-1 == name.indexOf('.')) {for (int i = 0; i < sClassPrefixList.length; i++) {
                // 通过构造方法创立 view
                final View view = createView(context, name, sClassPrefixList[i]);
                if (view != null) {return view;}
            }
            return null;
        } else {return createView(context, name, null);
        }
    } catch (Exception e) {return null;} finally {mConstructorArgs[0] = null;
        mConstructorArgs[1] = null;
    }
}
这里用来创立视图 的充气器 是通过 获取的。这样设计的目标在于裸露接口给调用者,用来自定义控件的充气器 逻辑。比方,针对三方控件和自定义控件的逻辑等。SkinCompatManager.getInstance().getInflaters()

该库自带的一个实现是,public class SkinAppCompatViewInflater implements SkinLayoutInflater, SkinWrapper {
   @Override
    public View createView(Context context, String name, AttributeSet attrs) {View view = createViewFromFV(context, name, attrs);

        if (view == null) {view = createViewFromV7(context, name, attrs);
        }
        return view;
    }

    private View createViewFromFV(Context context, String name, AttributeSet attrs) {
        View view = null;
        if (name.contains(".")) {return null;}
        switch (name) {
            case "View":
                view = new SkinCompatView(context, attrs);
                break;
            case "LinearLayout":
                view = new SkinCompatLinearLayout(context, attrs);
                break;
            // ... 其余控件的实现逻辑
        }
    }
    // ...
}

四、其余计划

除了下面介绍的计划外,还有如下的一些计划:

4.1 TG 换肤计划

TG 的换肤只反对夜间和日间主题之间的切换,所以,绝对下面几种计划 TG 的换肤就简略得多。

在浏览 TG 的代码的时候,我也 TG 在做页面布局的时候做了一件很疯狂的事件——他们没有应用任何 xml 布局,所有布局都是通过 java 代码实现的。

为了反对对主题的自定义 TG 把我的项目内简直所有的色彩别离定义了一个名称,对以文本模式记录到一个文件中,数量十分多,而后将其放到 assets 上面,利用内通过读取这个资源文件来获取各个控件的色彩。

 

4.2 自定义控件 + 全局播送实现换肤

这种计划根后面 hook LayoutInflator 的主动替换视图 的计划差不多。不过,这种计划不须要做 hook,而是对利用的内罕用的控件全副做一边自定义。自定义控件外部监听换肤的事件。当自定义控件接管到换肤事件的时候,自定义控件外部触发换肤逻辑。不过这种换肤的计划绝对于上述通过 hook LayoutInflator 的计划而言,可控性更好一些。

正文完
 0