Android高级开发进阶之路3——动态换肤(监听xml布局初始化,动态加载类,加载插件资源文件)

动态换肤(监听xml布局初始化,动态加载类,加载插件资源文件)

Android高级开发进阶之路3——动态换肤(监听xml布局初始化,动态加载类,加载插件资源文件)_第1张图片Android高级开发进阶之路3——动态换肤(监听xml布局初始化,动态加载类,加载插件资源文件)_第2张图片

一般小公司是不会接触到换肤的,尤其是动态下载皮肤插件来实现换肤。那么,今天我们来一探究竟。如何实现加载从网上下载的皮肤插件,并且替换到相应的控件中!

大致涉及到4个步骤:

1、下载皮肤插件(通常为apk,后期用skin.apk来表示皮肤插件)到本地

2、根据皮肤插件skin.apk的绝对路径,加载插件里的资源文件

3、准确找到需要换肤的控件

4、应用skin.apk里的资源,修改相应的属性

第一步下载皮肤插件就是简单地下载文件操作,这里就不做详细介绍,重点部分已经用红色加粗字体标记出来了!下面介绍一下关键的api,最后完整代码链接。

加载插件apk中的资源文件

public void setSkinPath(String skinPath) {
        this.skinPath = skinPath;

        //获取包管理类
        PackageManager packageManager = context.getPackageManager();

        //获取插件包信息类
        PackageInfo packageInfo = packageManager.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);

        //获取插件包名
        packageName = packageInfo.packageName;

        //获取插件的资源文件

        AssetManager assets = null;
        try {
            assets = AssetManager.class.newInstance();
            Method method = assets.getClass().getMethod("addAssetPath", String.class);//方法名,参数类型
            method.invoke(assets, packageName);//调用该方法的对象,传入的参数
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        resources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());

    }

找到需要换肤的控件

如果包含src,textColor,background等等属性,就认为该控件需要换肤,这个是根据项目的需求而定,没有强制要求的。下面三个步骤分别对应三段关键代码:

  1. 监听xml文件实例化View过程
  2. 可以取得每个View名字,通过每个view的名字,就可以动态实例化(实例化不会造成双重实例化,如果设置了监听,并且返回view的话,就不会掉用到系统的实例化)
  3. 实例化之后就可以得到相应的属性,通过AttributeSet获取到该View所有的属性,例如id,backgroundColor,@xxx(例如color)/xxx(例如white),获取到了信息其实就是对应R.xxx.xxx(例如R.color.white),根据这个名字去找到皮肤资源文件对应名字的资源,如果存在,那就替换,否则就不替换!
//监听xml文件实例化View过程
skinFactory = new SkinLayoutInflateFactory();
LayoutInflaterCompat.setFactory2(getLayoutInflater(), skinFactory);
class SkinLayoutInflateFactory implements LayoutInflater.Factory2 {

//...
//可以取得每个View名字,通过每个view的名字,就可以动态实例化(实例化不会造成双重实例化,如果设置了监听,并且返回view的话,就不会掉用到系统的实例化)

    /**
     * 一个完整的类名,生成一个
     * @param name
     * @param context
     * @param attrs
     * @return
     */
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        View view = null;
        try {
            Class aClass = context.getClassLoader().loadClass(name);

            Constructor constructor = aClass.getConstructor(new Class[]{Context.class, AttributeSet.class});

            view = constructor.newInstance(context, attrs);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return view;
    }
}
class SkinLayoutInflateFactory implements LayoutInflater.Factory2 {
//...
//实例化之后就可以得到相应的属性,通过AttributeSet获取到该View所有的属性,例如id,backgroundColor,@xxx(例如color)/xxx(例如white),获取到了信息其实就是对应R.xxx.xxx(例如R.color.white),根据这个名字去找到皮肤资源文件对应名字的资源,如果存在,那就替换,否则就不替换!

    /**
     * 如果控件已经实例化,那么我们就去判断这个控件是否符合换肤的需求
     * @param view
     * @param name
     * @param attrs
     */
    private void parseView(View view, String name, AttributeSet attrs) {

        List listSkinItem = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            String attrName = attrs.getAttributeName(i);
            String idStr = attrs.getAttributeValue(i);
            if (attrName.contains("background")
            ||attrName.contains("textColor")
                    ||attrName.contains("src")
                    ||attrName.contains("color")
            ) {
                //获取属性值id
                int id = Integer.parseInt(idStr.substring(1));

                String entryType = view.getResources().getResourceTypeName(id);
                String entryName = view.getResources().getResourceEntryName(id);

                SkinItem skinItem = new SkinItem(attrName, id, entryName, entryType);

                listSkinItem.add(skinItem);
            }
            SkinView skinView = new SkinView(view, listSkinItem);
            listWillBeChangeSkinView.add(skinView);
            //skinView.apply();
        }


        SkinManager.getInstance().applyNewSkin(listWillBeChangeSkinView);
    }

//...
}

修改相应的属性

前面一步拿到皮肤插件中对应名字资源的id,等等信息。通过id,利用皮肤插件的resource去查找这个id,如果有,就设置新的,没有就设置原来的。

public int getColor(int id) {
    if(resources == null) return id;
    //根据id获取属性值的名字
    String entryName = context.getResources().getResourceEntryName(id);

    //根据id获取属性值的类型
    String entryType = context.getResources().getResourceTypeName(id);
    //根据属性值的名称、属性值的类型、包名,查看该包名下这个属性的id
    int newId = resources.getIdentifier(entryName, entryType, packageName);

    if (newId == 0) {
        return id;
    }
    return resources.getColor(newId);

}

 

初学者积分用得比较快,为了帮助大家省点,项目我就放到github给你们吧,喜欢的朋友高抬贵手给我一个star,谢谢!

源码传送门:https://github.com/KubyWong/JetpackSample

总结:

下面再来简单总结一下整体流程,

加载apk外部皮肤插件,获取的resource操作资源对象(apk路径->包名->AssetsManager->Resource,类的动态加载newInstance,动态载入方法invoke)

重写系统实例化xml文件中的view的过程(setFactory2()方法设置一个接口,复写方法即可)

拿到view实例中的各种属性,如id,属性类型type,值的类型valuetype,值的名字valuename(涉及到resource和attrbuteset操作)

替换view中各种属性的值(涉及到根据id拿到属性名,属性类型context.getResources().getResourceTypeName(id)

;根据属性名,属性类型,包名,拿到相应的包名下单资源id,关键代码resources.getIdentifier(entryName, entryType, packageName),)

 

 

 

你可能感兴趣的:(Android开发)