本文章是在内置换肤的基础上进行扩充,内置换肤代码和原理讲解地址:内置换肤原理。如果没有查看这一章节内容,请跳过,本文绝大部分代码是内置换肤代码的扩展。这里只讲解核心代码。

一、动态换肤原理

首先需要明白resources.arsc资源映射表。打包一个apk,点击查看apk,包含如下内容:

动态overlay实现一键换肤_加载


这里可以看到resources.arsc,点击查看,如下截图:

动态overlay实现一键换肤_app动态换肤_02


左边一栏是资源类型type,右边包含资源id和资源名称name。三者对应关系如下:

  1. 不同的资源类型和资源名称会对应不同的资源id
  2. 相同的资源类型和资源名称对应相同的资源id
  3. 只要资源类型和资源名称有一个不相同,资源id就不同

动态换肤其实也就是根据三者的关系进行了"偷梁换柱",比如我们app中内置有一张图片,对应的资源id为0x7f060000,那么可以根据这个resourceId,获取到图片名称为img,类型为drawable。它是如何匹配皮肤包的相关资源呢?首先,你的皮肤包中必须有相同的图片名称和图片类型,根据图片名称和类型就可以获取到皮肤包的资源id,拿到这个资源id就可以设置图片了。

二、皮肤包

皮肤包其实就是一个apk,只是这个apk只需要配置res目录下的内容和字体样式。需要注意以下几点:

  1. colors.xml:里面的颜色资源要和app名称对应一致
  2. strings.xml:需要配置字体路径,名称要和app对应一致

以下为截图对比:

动态overlay实现一键换肤_加载_03


动态overlay实现一键换肤_app动态换肤_04

动态overlay实现一键换肤_动态overlay实现一键换肤_05


动态overlay实现一键换肤_加载_06


配置完成以后,只需要生成一下皮肤包apk即可。然后将生成的apk名称修改,这里我改成了qb.skin。然后将它放到SD卡即可。

配置皮肤包需要记住一下几点:

  1. 打出来的皮肤包可删除classes.dex,但至少保留:AndroidManifest.xml、resources.arsc、res/xxx。应该使用解压软件打开删除不需要的即可,不能先解压,删除后再进行压缩。
  2. app内置资源id和皮肤包的资源id不是一定一致的。比如app的drawable中有a.png,b.png,换肤1.png,换肤2.png,对应的资源id分别为0x7f060001,0x7f060002,0x7f060003,0x7f060004。我们皮肤包的drawable中有换肤1.png,换肤2.png,但是它对应资源id分别为0x7f060001,0x7f060002。

三、资源加载

1、初始化app资源appResource、皮肤包资源skinResource,皮肤包包名skinPackageName。主要代码如下:

private SkinManager(Application application) {

        mApplication = application;

        //获取当前app的资源
        appResources = application.getResources();

        skinCaches = new HashMap<>();
    }
/**
     * 1、获取皮肤包资源 skinResources
     * 2、获取皮肤包包名 skinPackageName
     *
     * @param skinPath 皮肤包本地路径
     */
    public void loadSkinResources(String skinPath) {

        if (TextUtils.isEmpty(skinPath)) {
            Log.e("SkinManager>>>>>>>>>", "加载本地资源");
            isDefault = true;
            return;
        }

        if (loadSkinCaches(skinPath)) {
            isDefault = false;
            Log.e("SkinManager>>>>>>>>>", "缓存找到");
            return;
        }

        /*
            获取当前app资源管理器 为什么不能使用mApplication.getAssets()
            因为没有这种方式并没有初始化该类,是没法应用方法反射的
         */
        //AssetManager assetManager = mApplication.getAssets();
        try {
            AssetManager assetManager = AssetManager.class.newInstance();

            //获取当前要反射方法
            Method method = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class);
            method.setAccessible(true);

            //执行反射,将皮肤包skin资源加载到当前app资源中
            method.invoke(assetManager, skinPath);

            //创建加载皮肤包资源,依然在app应用中加载
            skinResources = new Resources(assetManager, appResources.getDisplayMetrics(),
                    appResources.getConfiguration());

            //获取皮肤包包名
            skinPackageName = mApplication.getPackageManager().getPackageArchiveInfo(skinPath,
                    PackageManager.GET_ACTIVITIES).packageName;

            //加载成功,设置标识为false,表示可以加载皮肤包资源
            if (!TextUtils.isEmpty(skinPackageName)) {
                isDefault = false;
                //保存到缓存中
                skinCaches.put(skinPath, new SkinCache(skinPackageName, skinResources));
            }

        } catch (Exception e) {
            e.printStackTrace();
            isDefault = true;
        }

    }

2、根据资源app资源id加载皮肤包资源id。主要代码如下:

**
     * 获取皮肤包资源id 参考resource.arsc资源映射表
     *
     * @param resourceId app的资源id
     * @return 如果找到返回皮肤包资源id,反之返回app的resourceId
     */
    public int loadResouceId(int resourceId) {

        //没有皮肤包 直接返回app资源id
        if (isDefault) return resourceId;

        //获取app资源名称,比如ic_launcher.png
        String resourceName = appResources.getResourceEntryName(resourceId);

        //获取app资源类型 比如drawable
        String resourceType = appResources.getResourceTypeName(resourceId);

        /*
            加载皮肤包的资源id 失败返回0
         */
        int skinResourceId = skinResources.getIdentifier(resourceName, resourceType,
                skinPackageName);

        //失败isDefault = true
        isDefault = skinResourceId == 0;

        return isDefault ? resourceId : skinResourceId;
    }

四、使用

:1、在需要换肤的activity中调用如下方法:

SkinManager.getInstance().loadSkinResources(skinPath);

2、skinPath为皮肤包的路径,通过以下方式获取:

//加载皮肤包
 skinPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "qb.skin";

3、执行换肤的时候,比如TextView控件,在SkinTextView.class中的换肤方法中,修改之前代码如下:

public void viewChange() {
        //改变控件背景颜色
        int bgKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_background];
        int bgColor = skinBean.getResource(bgKey);

        //动态改变背景颜色
        if (bgColor > 0) {

            //是否加载皮肤包成功
            if(SkinManager.getInstance().isDefault()){
                // 兼容包转换
                Drawable drawable = ContextCompat.getDrawable(getContext(), bgColor);
                // 控件自带api,这里不用setBackgroundColor()因为在9.0测试不通过
                // setBackgroundDrawable本来过时了,但是兼容包重写了方法
                setBackgroundDrawable(drawable);
            }else{
                Object skinResourceId = SkinManager.getInstance().getBackgroundOrSrc(bgColor);
                // 兼容包转换
                if (skinResourceId instanceof Integer) {
                    int color = (int) skinResourceId;
                    setBackgroundColor(color);
                    // setBackgroundResource(color); // 未做兼容测试
                } else {
                    Drawable drawable = (Drawable) skinResourceId;
                    setBackgroundDrawable(drawable);
                }
            }

        }

        int textColorKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_textColor];
        int textColor = skinBean.getResource(textColorKey);

        if (textColor > 0) {
            //是否加载皮肤包成功
            if(SkinManager.getInstance().isDefault()){
                ColorStateList color = ContextCompat.getColorStateList(getContext(), textColor);
                this.setTextColor(color);
            }else{
                ColorStateList skinColor = SkinManager.getInstance().getColorStateList(textColor);
                this.setTextColor(skinColor);
            }

        }

        //字体样式
        // 根据自定义属性,获取styleable中的字体 custom_typeface 属性
        int typefaceKey = R.styleable.SkinTextView[R.styleable.SkinTextView_custom_typeface];
        int textTypefaceResourceId = skinBean.getResource(typefaceKey);
        if (textTypefaceResourceId > 0) {
            //是否加载皮肤包成功
            if (SkinManager.getInstance().isDefault()) {
                setTypeface(Typeface.DEFAULT);
            } else {
                setTypeface(SkinManager.getInstance().getTypeface(textTypefaceResourceId));
            }
        }
    }

其他自定义控件类似相关修改。

4、注意不要忘记在app的配置文件中加上权限

<!--皮肤包权限-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

最后上一波效果图,字体,图片和自定义控件都换了。是不是6666!!!

动态overlay实现一键换肤_加载_07

动态换肤代码地址如下:

代码地址:app动态换肤