美文网首页android学习之路爱码蛋Android
Android 在线换肤方案总结分享

Android 在线换肤方案总结分享

作者: imesong | 来源:发表于2016-04-07 17:33 被阅读3836次

    文章首发于imesong的个人网站

    本文主要是在公司内部的一次分享会内容,基于 Android-Skin-Loader开源库,实现一套完整的在线换肤方案。同时,也参考了很多网上总结的换肤思路,后面会给出主要的参考资料。

    我们先看下Demo效果图

    Demo效果图

    为什么要做在线换肤?

    皮肤模块独立,减小 Apk 包大小

    资源文件在App大小中有很大的比重,特别是工具类的App,用户有使用多套皮肤的需求,如果多套皮肤资源全部在 App 中,会极大的增加 App 的大小,不利于用户下载以及渠道推广。

    技术方案通用,不局限于具体的 App

    现在公司有十几款移动端产品,但是还没有App使用在线换肤或者使用类似插件式的换肤方案,如果能调研出一套完整的在线换肤方案,公司的各个 App 都可以使用。

    服务端控制皮肤包,动态更新,满足运营需求

    皮肤包资源放在服务端,动态更新,可以满足运营需求,发布不同主题皮肤资源包,避免通过发布版本迭代更新。

    降低维护成本

    通过把皮肤模块独立出来,减少主工程的逻辑,精简主工程代码,降低维护成本。

    在线换肤的难点在哪里?

    调研一个技术方案,有难点,有重点。当我们把这些重点难点攻克,方案的雏形基本就完成了。

    如何加载皮肤资源文件

    在线换肤,皮肤资源肯定不会在Apk内部,要怎么加载外部的皮肤资源呢?下载到本地,如何加载外部的资源文件呢?
    我们先看下 Apk 的打包流程。

    Android打包流程

    这里流程中,有两个关键点
    1.R文件的生成
    R文件是一个Java文件,通过R文件我们就可以找到对应的资源。R文件就像一张映射表,帮助我们找到资源文件。
    2.资源文件的打包生成

    资源文件经过压缩打包,生成 resources 文件,通过R文件找到里面保存的对映的资源文件
    在 App 内部,我们一般通过下面代码,获取资源

    context.getResource.getString(R.string.hello);
    context.getResource.getColor(R.color.black);
    context.getResource.getDrawable(R.drawable.splash);
    
    

    这个时获取 App 内部的资源,能我们家在皮肤资源什么思路吗?加载外部资源的 Resources 能通过类似的思路吗?
    我们查看下 Resources 类的源码,发现 Resources 的构造函数

    /**
         * Create a new Resources object on top of an existing set of assets in an
         * AssetManager.
         *
         * @param assets Previously created AssetManager.
         * @param metrics Current display metrics to consider when
         *                selecting/computing resource values.
         * @param config Desired device configuration to consider when
         *               selecting/computing resource values (optional).
         */
        public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
            this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
        }
    

    这里关键是第一个参数如何获取,第二和第三个参数可以通过 Activity 获取到。
    我们再去看下 AssetManager 的代码,同时会发现下面的这个

    /**
         * Add an additional set of assets to the asset manager.  This can be
         * either a directory or ZIP file.  Not for use by applications.  Returns
         * the cookie of the added asset, or 0 on failure.
         * {@hide}
         */
        public final int addAssetPath(String path) {
            synchronized (this) {
                int res = addAssetPathNative(path);
                makeStringBlocks(mStringBlocks);
                return res;
            }
        }
    

    AssetManager 可以加载一个zip 格式的压缩包,而 Apk 文件不就是一个 压缩包吗。我们通过反射的方法,拿到 AssetManager,加载 Apk 内部的资源,获取到 Resources 对象,这样再想办法,把 R文件里面保存的ID获取到,这样既可以拿到对应的资源文件了。理论上我们的思路时成立的。
    我们看下,如何通过代码获取 Resources 对象。

    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());
    

    标记需要换肤的 View

    找到资源文件之后,我们要接着标记需要换肤的 View 。
    找到需要换肤的 View
    怎么寻找哪些是我们要关注的 View 呢? 我们还是重 View 的创建时机寻找机会。我们添加一个布局文件时,会使用 LayoutInflater的 Inflater方法,我们看下这个方法是怎么讲一个View添加到Activity 中的。
    LayoutInflater 中有个接口

     public interface Factory {
            /**
             * Hook you can supply that is called when inflating from a LayoutInflater.
             * You can use this to customize the tag names available in your XML
             * layout files.
             * 
             * <p>
             * Note that it is good practice to prefix these custom names with your
             * package (i.e., com.coolcompany.apps) to avoid conflicts with system
             * names.
             * 
             * @param name Tag name to be inflated.
             * @param context The context the view is being created in.
             * @param attrs Inflation attributes as specified in XML file.
             * 
             * @return View Newly created view. Return null for the default
             *         behavior.
             */
            public View onCreateView(String name, Context context, AttributeSet attrs);
        }
    
    

    根据这里的注释描述,我们可以自己实现这个接口,在 onCreateView 方法中选择我们需要标记的View,根据 AttributeSet 值,过滤不需要关注的View。
    标记 View 与对应的资源
    我们在 View 创建时,通过过滤 Attribute 属性,找到我们要标记的 View ,下面我们就把这些View的属性记下来

    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) {
                        e.printStackTrace();
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
    

    然后把这些 View 和属性值,一起封装保存起来

    if(!ListUtils.isEmpty(viewAttrs)){
                SkinItem skinItem = new SkinItem();
                skinItem.view = view;
                skinItem.attrs = viewAttrs;
                mSkinItems.add(skinItem);
                if(SkinManager.getInstance().isExternalSkin()){
                    skinItem.apply();
                }
        }
    

    及时更新 UI

    由于我们把需要更新的View 以及属性值都保存起来了,更新的时候只要把他们取出来遍历一遍即可。

    @Override
        public void onThemeUpdate() {
            if(!isResponseOnSkinChanging){
                return;
            }
            mSkinInflaterFactory.applySkin();
        }
    //applySkin 的具体实现
    
    public void applySkin(){
            if(ListUtils.isEmpty(mSkinItems)){
                return;
            }   
            for(SkinItem si : mSkinItems){
                if(si.view == null){
                    continue;
                }
                si.apply();
            }
        }
    

    制作皮肤包

    皮肤包制作相对简单
    1.创建独立 model,包名自定义
    2.添加资源文件到 model 中,不需要 java 代码
    3.运行 build.gradle 脚本,生成 xxx.skin 皮肤包

    制定一套完整的在线换肤方案

    到这里之后,还是没看到在线换肤方案啊~说好的在线换肤方案呢?
    1.将制作好的皮肤包上传到服务端后台
    2.客户端根据接口数据,处理皮肤加载逻辑

    模块依赖关系

    三个模块的依赖关系

    换肤方案的介绍基本完成,下面是一些参考资料和资源

    分享ppt文件

    Demo源码

    参考开源资料xmind文件下载

    相关文章

      网友评论

      • 相互交流:楼主如果我要动态改变整个APP 的字体大小和颜色,,,改怎么办啊???
        imesong:@相互交流 同样的思路啊
      • ping0505:博主 你好 在生成皮肤包的时候 怎么运行build.gradle脚本
        imesong:Android Studio 最右边,有Gradle task 列表,选中对应的module,Tasks--build--asembleRelease
        墨源为水:@imesong 博主,具体如何做呢?
        imesong:@ping0505 皮肤包其实就是一个apk 文件,执行下 build.gradle 中对应library中的release 命令

      • fendo:赞一个
      • 捡淑:mark
      • I喵先生:请问如果是zip的皮肤包,要怎么实现呢,
        imesong:@herohd 自己实现Resource类,自己实现资源的查找和加载,需要对系统有比较深入的了解,这个我也不是很清楚怎么实现。在文章末尾的xmind 引用的参考资料中有一篇是介绍这个点,可以看下。
        I喵先生:@imesong 能说下换个方案是什么思路吗,之前网上又说道修改resource的来实现,不过试过以后发觉加载.9那些处理起来很麻烦。
        imesong:@herohd apk 包也是一种压缩格式的文件,如果是纯zip 包,需要换个方案去做,默认都是把 apk当作zip 包来用,主要是加载里面的资源比较方便
      • yangjianan:博主,我用的genymotion运行的,一直 吐司 (请检查/storage/emulated/0/CrazyGold.skin是否存在), 请问这个文件怎么要自己放在对应路径下吗? :joy:
        imesong: @yangjianan 是的,把皮肤包放在对应的目录下

      本文标题:Android 在线换肤方案总结分享

      本文链接:https://www.haomeiwen.com/subject/lvzmlttx.html