一.原理

动态换肤是将多种资源文件放在皮肤包中,皮肤包本质上就是打包成的APK文件,与静态换肤相比,动态换肤将皮肤资源分离出来单独打包,可以有效减少APP的大小。下图是APK文件的内部组成:

Android动态人物 android动态换肤_加载

其中classes.dex文件中的内容对应的是Java代码,在皮肤包中这部分内容是不需要的。resources.arsc文件中的内容是资源文件,如下图所示:

Android动态人物 android动态换肤_Android动态人物_02

每一个资源文件都有一个ID,如“0x7f040026”,其中,“0x7f”是标准规范,每个资源ID都以它开头;“04”代表color类别,比如drawable为“06”、layout为“0a”;“0026”代表序号,资源顺序是按字母进行排序的。
动态换肤的基本原理是:
  • APP中需要替换的颜色、图片等资源和皮肤包中的资源是一一对应的,具有相同的文件名。
  • 在APP中,可以拿到需要替换的资源文件名和类型,根据资源文件名和类型可以去皮肤包中查找这个资源对应的ID。
  • 根据资源ID可以加载出皮肤包中的资源,从而替换APP中的资源文件。

二.实现

1. SkinManager类

首先新建一个皮肤管理类SkinManager类,该类首先要分别得到APP和皮肤包的Resources对象。APP的Resources对象可以通过Application.getResources()获得,皮肤包的Resources对象获取方法如下所示。
/**
     * 加载皮肤包资源
     * @param skinPath 皮肤包路径,为空则加载app内置资源
     */
    public void loaderSkinResources(String skinPath) {
        // 如果没有皮肤包或者没做换肤动作,方法不执行直接返回!
        if (TextUtils.isEmpty(skinPath)) {
            isDefaultSkin = true;
            return;
        }
        try {
            // 通过反射创建资源管理器(用来加载外部APk,不能用Application.getAssets())
            AssetManager assetManager = AssetManager.class.newInstance();
            // AssetManager中的addAssetPath方法只能通过反射去执行方法
            Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            // 设置私有方法可访问
            addAssetPath.setAccessible(true);
            // 执行addAssetPath方法
            addAssetPath.invoke(assetManager, skinPath);

            // 创建加载外部的皮肤包文件Resources
            skinResources = new Resources(assetManager,
                 appResources.getDisplayMetrics(), appResources.getConfiguration());

            // 根据apk文件路径,获取包名。
            skinPackageName = application.getPackageManager()
                    .getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
            // 如果获取包名失败,则加载app内置资源
            isDefaultSkin = TextUtils.isEmpty(skinPackageName);
            if (!isDefaultSkin) {
                cacheSkin.put(skinPath, new SkinCache(skinResources, skinPackageName));
            }    
        } catch (Exception e) {
            e.printStackTrace();
            isDefaultSkin = true;
        }
    }
得到皮肤包的Resources对象后,还需要有一个方法用于查找与APP资源对应的皮肤包资源ID。
private int getSkinResourceIds(int resourceId) {
        //如果使用默认皮肤,直接返回app内置资源!
        if (isDefaultSkin) return resourceId;
        // 根据APP内置资源获得资源名称和类型(如“mojave”, “drawable”)
        String resourceName = appResources.getResourceEntryName(resourceId);
        String resourceType = appResources.getResourceTypeName(resourceId);

        // 根据资源名称和类型去皮肤包中查找对应的资源ID
        int skinResourceId = skinResources.getIdentifier(resourceName, resourceType, skinPackageName);
        isDefaultSkin = skinResourceId == 0;
        return skinResourceId == 0 ? resourceId : skinResourceId;
    }
得到皮肤包的资源ID后,就可以根据该资源ID找到资源文件,并设置到View中。

2.自定义View

与静态换肤相同,在第一次加载布局时,通过设置自定义的Factory2对象,在onCreateView方法中用自定义的View去代替系统View,在自定义View中进行资源替换工作。自定义View均实现ViewChange接口,在skinChange中进行换肤操作。与静态换肤的区别就是资源是在外部皮肤包中加载的。部分代码如下:
// 根据自定义属性,获取styleable中的textColor属性
        key = R.styleable.SkinnableButton[R.styleable.SkinnableButton_android_textColor];
        int textColorResourceId = attrsBean.getViewResource(key);
        if (textColorResourceId > 0) {
            if (SkinManager.getInstance().isDefaultSkin()) {
            //如果是默认皮肤,则使用APP内置资源
                ColorStateList color = ContextCompat.getColorStateList(getContext(), textColorResourceId);
                setTextColor(color);
            } else {
            //如果选择了换肤,则使用皮肤包资源
                ColorStateList color = SkinManager.getInstance().getColorStateList(textColorResourceId);
                setTextColor(color);
            }
        }
换肤效果如图所示:

Android动态人物 android动态换肤_Android动态人物_03