动态换肤三(加载皮肤包中的资源)

前言

  上一篇文章,不但获取到了所有的 View,还将需要换肤的 View 进行了筛选并且保存在了 List 中,那么接下来,就需要获取皮肤包中的资源,然后让这些 View 加载这些资源就 OK 了。

上一篇文章地址:https://www.jianshu.com/p/1e180d8ed33b

生成皮肤包

  加载皮肤包的资源,首先要生成皮肤包,当然,按理来说这个应该是从网上下载下来的,但是我们现在自己造一个皮肤包,保存到 sdcard 中。
  继续看我们 MainActivity 的布局文件,现在只有一个 LinearLayout 和第二个 TextView 可以换肤(该 TextView 可以换背景和字体颜色),其中,background 属性对应的值是 ?attr/colorAccent 和 @color/black ,textColor 属性对应的值是 @color/black。
  接下来我们新建一个项目,用这个新项目生成皮肤包,该项目不需要写什么代码,就将上述这些对应的资源进行设置即可,假如说我们将背景设置成绿色,将字体设置成红色,然后生成 apk,导入到 sdcard 中。
cd sdcard/
app-skin-debug.apk

获取皮肤包中的资源

  导入成功后,我们就先要加载这个 apk,然后获取其资源,具体怎么获取呢?首先我们要获取路径,然后将路径设置到 AssetManager 中,然后根据这个 AssetManager 获取皮肤包中的 Resources,获取皮肤包的包名。接下来我们需要将主包中的资源和皮肤包中的资源进行转换, 怎么转换?假如属性的值是 R.drawable.ic_launcher,我们先获取他的名字 ic_launcher,再获取他的类型 drawable,然后通过 Resources.getIdentifier(name,type,packageName);就可以获得类型为 drawable 的 ic_launcher 在 packageName 中的资源 id 的值。

import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.text.TextUtils;
import android.widget.Toast;

import com.radish.android.skin_core.util.SkinPreference;
import com.radish.android.skin_core.util.SkinResources;

import java.io.File;
import java.lang.reflect.Method;
import java.util.Observable;

/**
 * 皮肤管理器
 */

public class SkinManager extends Observable {

    private static SkinManager instance = null;

    public static void init(Application application){
        if(instance == null){
            synchronized (SkinManager.class){
                if(instance == null){
                    instance = new SkinManager(application);
                }
            }
        }
    }

    public static SkinManager getInstance(){
        return instance;
    }

    private Application application;

    private SkinManager(Application application){
        this.application = application;
        application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
        SkinResources.init(application);
        SkinPreference.init(application);

    }

    /**
     * 加载皮肤包
     * @param path  皮肤包的路径
     */
    public void loadSkin(String path) throws Exception {
        if(TextUtils.isEmpty(path)){
            //如果是空,就加载默认资源,清空皮肤包的配置
            SkinPreference.getInstance().setSkin("");
            SkinResources.getInstance().reset();
        }else{
            File file = new File(path);
            if(!file.exists()){
                throw new Exception("您加载的资源包不存在");
            }

        /*
            这里,由于 AssetMaanger 的限制比较大,我们通过反射来获取 AssetManager 实例对象,
            然后给他设置路径的方法是被隐藏了,我们继续使用反射的方式来获取。
         */
            AssetManager assetManager = AssetManager.class.newInstance();
            Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
            method.setAccessible(true);
            method.invoke(assetManager,path);

            //这个是我们应用的 Resources
            Resources resources = application.getResources();
            //这个是皮肤包对应的 Resources
            Resources skinResources = new Resources(assetManager,resources.getDisplayMetrics(),resources.getConfiguration());

            //获取包名
            PackageManager packageManager = application.getPackageManager();
            PackageInfo info = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
            if(info != null){
                String packageName = info.packageName;
                //设置后可以获取对应皮肤包中的资源了
                SkinResources.getInstance().applySkin(skinResources,packageName);
                //设置完成后,记得保存一下皮肤包的路径
                SkinPreference.getInstance().setSkin(path);
            }else{
                Toast.makeText(application, "加载皮肤资源包失败", Toast.LENGTH_SHORT).show();
            }
        }
        //通知观察者进行更新
        setChanged();
        notifyObservers();
    }
}
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;

/**
 * 资源转换工具类
 */

public class SkinResources {

    private static SkinResources instance;
    private String packageName;

    private SkinResources(Context context){
        this.mAppResources = context.getResources();
    }

    public static void init(Context context){
        if(instance == null){
            synchronized (SkinResources.class){
                if(instance == null){
                    instance = new SkinResources(context);
                }
            }
        }
    }

    private Resources mSkinResources;
    private Resources mAppResources;
    /**
     * 用于表示是否加载默认皮肤
     */
    private boolean isDefaultSkin = true;

    public static SkinResources getInstance(){
        return instance;
    }

    public void applySkin(Resources resources,String packageName){
        this.mSkinResources = resources;
        this.packageName = packageName;
        isDefaultSkin = TextUtils.isEmpty(packageName) || (resources == null);
    }

    public int getIdentifier(int resId){
        String resourceEntryName = mAppResources.getResourceEntryName(resId);
        String resourceTypeName = mAppResources.getResourceTypeName(resId);
        return mSkinResources.getIdentifier(resourceEntryName, resourceTypeName, packageName);
    }

    public int getColor(int resId){
        if(isDefaultSkin){
            return resId;
        }
        int skinId = getIdentifier(resId);
        if(skinId == 0){
            return mAppResources.getColor(resId);
        }
        return mSkinResources.getColor(skinId);
    }

    public ColorStateList getColorStateList(int resId){
        if(isDefaultSkin){
            return mAppResources.getColorStateList(resId);
        }
        int skinId = getIdentifier(resId);
        if(skinId== 0){
            return mAppResources.getColorStateList(resId);
        }
        return mSkinResources.getColorStateList(skinId);
    }

    public Drawable getDrawable(int resId){
        if(isDefaultSkin){
            return mAppResources.getDrawable(resId);
        }
        int skinId = getIdentifier(resId);
        if(skinId == 0){
            return mAppResources.getDrawable(resId);
        }
        return mSkinResources.getDrawable(skinId);
    }

    /**
     * 这里返回值写成 Object 是因为 background 可能是
     * color 也可能是 drawable
     */
    public Object getBackground(int resId){
        String resourceTypeName = mAppResources.getResourceTypeName(resId);
        if("color".equals(resourceTypeName)){
            return getColor(resId);
        }else{
            return getDrawable(resId);
        }
    }

    public String getString(int resId){
        if(isDefaultSkin){
            return mAppResources.getString(resId);
        }
        int skinId = getIdentifier(resId);
        if(skinId == 0){
            return mAppResources.getString(resId);
        }
        return mSkinResources.getString(skinId);
    }

    public void reset() {
        mSkinResources = null;
        packageName = "";
        isDefaultSkin = true;
    }
}
/**
 * 保存 Util
 */

public class SkinPreference {

    private static final String SKIN_SHARED = "skins";

    private static final String KEY_SKIN_PATH = "skin_path";

    private static SkinPreference instance;

    private SharedPreferences mPref;

    private SkinPreference(Context context){
        mPref = context.getSharedPreferences(SKIN_SHARED,Context.MODE_PRIVATE);
    }

    public static void init(Context context){
        if(instance == null){
            synchronized (SkinPreference.class){
                if(instance == null){
                    instance = new SkinPreference(context);
                }
            }
        }
    }

    public static SkinPreference getInstance(){
        return instance;
    }

    public void setSkin(String path){
        mPref.edit().putString(KEY_SKIN_PATH,path).apply();
    }

    public String getSkin(){
        return mPref.getString(KEY_SKIN_PATH,null);
    }
}
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import android.view.LayoutInflater;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
 * Actiity 声明周期回调监听
 */

public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {

    private Map<Activity,SkinLayoutFactory> mLayoutFactoryMap = new HashMap<>();

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        LayoutInflater layoutInflater = LayoutInflater.from(activity);
        try {
            /*
                这里需要注意一点,在调用 setFactory2的时候,源码有一个判断,
                if (mFactorySet) {
                    throw new IllegalStateException("A factory has already been set on this LayoutInflater");
                }
                这个意思就是 Android 布局加载器通过 mFactorySet 标记是否设置过 Factory,如果设置过就会抛出异常
                那么我们就强制这个 mFactorySet 是 false

             */
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(layoutInflater,false);
        } catch (Exception e) {
            e.printStackTrace();
        }
        SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory();
        layoutInflater.setFactory2(skinLayoutFactory);
        //注册观察者
        SkinManager.getInstance().addObserver(skinLayoutFactory);
        mLayoutFactoryMap.put(activity,skinLayoutFactory);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {

    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
        //删除观察者
        SkinLayoutFactory skinLayoutFactory = mLayoutFactoryMap.get(activity);
        SkinManager.getInstance().deleteObserver(skinLayoutFactory);
    }
}

应用皮肤包中的资源

  现在皮肤包中的资源已经能获取到了,需要换肤的 View 也有了,我们只需要将需要换肤的 View 中某个属性的值换成皮肤包中的资源,就行了。
  首先要初始化我们的框架,SkinManager.init(application);这个写到Application 的 onCreate()方法里面即可。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.tv_click).setOnClickListener(v -> {
            //开始换肤
            try {
                SkinManager.getInstance().loadSkin("/sdcard/app-skin-debug.apk");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        findViewById(R.id.tv_other).setOnClickListener(v -> {
            //还原
            try {
                SkinManager.getInstance().loadSkin("");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

  刚打开 app 我们的界面是这样的


换肤前

  点击换肤按钮


换肤后

  如果我们这时候关闭应用,重新打开,界面还是这样的,因为我们保存了。
  还原按钮点击后
还原

那么简单的初始化换肤就完成了。

下一篇文章地址:https://www.jianshu.com/p/30815e811c7c

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容