插件化实现Android多主题功能原理剖析

前言

以前咱们总结过B站的皮肤框架MagicaSakura,也点出了其不足,文章连接:来自B站的开源的MagicaSakura源码解析,该框架只能完成普通的换色需求,没有QQ,网易云音乐相似的皮肤包的功能。java

那么今天咱们就带来,拥有皮肤加载功能的插件化换肤框架。框架的分装和使用具体能够看个人工程里面的代码。
github.com/Jerey-Jobs/…android

这样作有两个好处:
git

  1. 皮肤能够不集成在apk中,减少apk体积
  2. 动态化增长皮肤,灵活性大,自由度很大

如何实现换肤功能

想固然的,在View建立的时候这是让咱们应用可以完美的加载皮肤的最好方案。github

那么咱们知道,对于Activity来讲,有一个能够复写的方法叫onCreateViewweb

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return super.onCreateView(parent, name, context, attrs);
}复制代码

咱们的view的建立就是经过这个方法来的,咱们甚至能够经过复写这个方法,实现view的替换,好比原本要的是TextView,咱们直接给它替换成Button.而这个方法实际上是实现的LayoutInflaterFactory接口。app

关于LayoutInflaterFactory,咱们能够看一下鸿神的文章www.tuicool.com/articles/EV…框架

建立View

根据拿到的onCreateView里面的name,来反射建立View,这边用到了一个技巧:onCreateView中的name,对于系统的View,是没有'.'符号的,好比"TextView"咱们拿到的直接是TextView,
可是自定义的View,咱们拿到的是带有包名的所有名称,所以反射时,对于系统的View,咱们须要加上系统的包名,自定义的View,则直接使用name。ide

也不用疑问为何用反射,这样不是慢吗?

由于系统的LayoutInflater在createView的时候也是这么作的,这边的代码都是参考系统的实现的。ui

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;
            // 系统控件,没有".",所以去建立系统View
            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;
                // 有'.'的状况下是自定义View,V4与V7也会走
            } else {
                // 直接根据名称建立View
                return createView(context, name, null);
            }
        } catch (Exception e) {
            // We do not want to catch these, lets return null and let the actual LayoutInflater
            // try
            return null;
        } finally {
            // Don't retain references on context.
            mConstructorArgs[0] = null;
            mConstructorArgs[1] = null;
        }
    }

    /** * 反射,使用View的两参数构造方法建立View * @param context * @param name * @param prefix * @return * @throws ClassNotFoundException * @throws InflateException */
private static View createView(Context context, String name, String prefix) throws ClassNotFoundException, InflateException {
    Constructor<? extends View> constructor = sConstructorMap.get(name);

    try {
        if (constructor == null) {
            // Class not found in the cache, see if it's real, and try to add it
            Class<? extends View> clazz = context.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);

            constructor = clazz.getConstructor(sConstructorSignature);
            sConstructorMap.put(name, constructor);
        }
        constructor.setAccessible(true);
        return constructor.newInstance(mConstructorArgs);
    } catch (Exception e) {
        // We do not want to catch these, lets return null and let the actual LayoutInflater
        // try
        return null;
    }
}复制代码

判断View是否须要换肤

与建立View同样,根据拿到的onCreateView里面的AttributeSet attrs
spa

拿到后,咱们解析attrs

/** * 拿到attrName和value * 拿到的value是R.id */
String attrName = attrs.getAttributeName(i);//属性名
String attrValue = attrs.getAttributeValue(i);//属性值复制代码

根据属性名和属性值进行判断,有背景的属性,是否符合须要换肤的属性、

插件化资源注入

咱们的皮肤包实际上是APK,是咱们写的另外一个app,与正式App不一样的是,其只有资源文件,且资源文件须要和主app同名。

1.经过 PackageManager拿皮肤包名
2.拿到皮肤包里面的Resource

可是由于咱们想new Resources()时候,发现其第一个参数是AssetManager,可是AssetManager的构造方法在源码中被@hide了,咱们没有方法拿到这个类,可是幸亏其类仍是能拿到的,咱们直接反射获取。

咱们拿资源的代码以下。

PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
/** * AssetManager assetManager = new AssetManager(); * 这个方法被@ hide了。。咱们只能经过反射newInstance */
AssetManager assetManager = AssetManager.class.newInstance();
/** * addAssetPath一样被系统给hide了 */
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, params[0]);
skinPath = skinPkgPath;
isDefaultSkin = false;
/** * 到此,咱们拿到了外置皮肤包的资源 */
return skinResource;复制代码

如何动态的从皮肤包中获取资源

咱们以从皮肤包里面获取color来举例

业务端是经过资源的id来获取color的,资源的id也就是一个在编译时就生成的int型。 而皮肤包的也是编译时生成的,所以两个id是不同的,咱们只能经过资源的id先拿到在咱们应用里的该id的名字,再经过名字去资源包里面拿资源。

public int getColor(int resId) {
    int originColor = ContextCompat.getColor(context, resId);
    /** * 若是皮肤资源包不存在,直接加载 */
    if (mResources == null || isDefaultSkin) {
        return originColor;
    }
    /** * 每一个皮肤包里面的id是不同的,只能经过名字来拿,id值是不同的。 * 1. 获取默认资源的名称 * 2. 根据名称从全局mResources里面获取值 * 3. 若获取到了,则获取颜色返回,若获取不到,老老实实使用原来的 */
    String resName = context.getResources().getResourceEntryName(resId);

    int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
    int trueColor;
    if (trueResId == 0) {
        trueColor = originColor;
    } else {
        trueColor = mResources.getColor(trueResId);
    }
    return trueColor;
}复制代码

实际使用

上面都是咱们插件化加载的须要了解的知识,真的进行框架使用的时候,使用了自定义属性,根据自定义属性判断是否须要换肤。

使用观察者模式,全部须要换肤的view都会存放在Activity一个集合中,在皮肤管理器通知皮肤更新时,主动更新视图状态。

说了这么多了,框架的分装和使用具体能够看个人工程里面的代码。
github.com/Jerey-Jobs/…

效果如图:

代码见:github.com/Jerey-Jobs/…

欢迎star

APK下载 App下载连接


本文做者:Anderson/Jerey_Jobs

博客地址 : jerey.cn/

简书地址 : Anderson大码渣

github地址 : github.com/Jerey-Jobs

相关文章
相关标签/搜索