Android简易动态加载-批量替换资源包实现换肤

Android简易动态加载-批量替换资源包实现换肤

动态加载技术:
核心是想是动态调用外部的dex文件,极端情况下,Android APK自身带有的dex文件只是一个程序的入口(或者是空壳),所有的功能都是通过服务器下载最新的dex文件完成
应用在运行的适合去加载一些本地的可执行文件实现一些特定的功能
动态加载那些文件:
动态加载so库
动态加载dex ,jar,apk文件


简单实现方式:

1、获取包的管理器,获取资源包信息类,找到资源对象
2、通过反射获取AssetManager对象以及addAssetPath方法,去添加传进来的皮肤包路径
3、创建Resources对象,得到资源包里面的资源对象
4、通过Resources获取到资源id和外部传入的资源id对比,去加载资源
5、在页面Activity启动的时候,去实现LayoutInflater.Factory2,收集需要换肤的控件
6、在LayoutInflater.Factory2实现类中,调用onCreateView去实例化控件,并且收集需要换肤的控件
7、遍历所有控件的属性,拿到控件,控件资源id,资源id类型,资源id名字,然后去和通过Resources获取的资源id做对比,去加载资源包里面的资源

项目链接:https://github.com/renbin1990/DynamicLoad


效果

有点丑,实现效果就好
换肤前:
在这里插入图片描述
换肤后:
在这里插入图片描述


# 一、加载皮肤资源包 创建一个项目,创建一个Module:**skin_library**,创建加载皮肤资源对象类SkinManager

1.获取包的管理器,获取资源包信息类,找到资源对象

创建一个加载apk的方法,传入apk路径,创建初始化context类

public class SkinManager {

    private static  SkinManager sSkinManager;
    //shangxiawen
    private Context mContext;
    //资源包包名
    private String packageName;
    //资源包里面的资源对象
    private Resources mResources;

    public static  SkinManager getInstance(){
        if (sSkinManager ==null){
            sSkinManager = new SkinManager();
        }
        return sSkinManager;
    }

    public void setContext(Context context){
        this.mContext = context;
    }

    /**
     * 根据路径加载皮肤包
     * @param path
     */
    public void loadSkinApk(String path){
        //获取到包管理器
        PackageManager packageManager = mContext.getPackageManager();
        //获取资源包包信息类
        PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
        //获取资源对象
        packageName = packageArchiveInfo.packageName;
        }
 }

2.创建Resources对象,得到资源包里面的资源对象

一般Resources对象都是通过getResources这个方法获取的,但是这是获取本包里面的资源,要想获取别的包的资源,需要去new Resources(),这里面需要三个参数,AssetManager需要通过反射去获取,项目中所有的加载资源相关,都需要这个管理器,剩下两个参数DisplayMetrics则通过初始化的context获取就好了。

          try{
            //通过反射获取assetManager对象
            AssetManager assetManager = AssetManager.class.newInstance();
            //通过反射获取addAssetPath方向
            Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager,path);
            //得到了资源包里面的资源对象
            mResources = new Resources(assetManager,mContext.getResources().getDisplayMetrics(),mContext.getResources().getConfiguration());
        }catch (Exception e){
            e.printStackTrace();
        }

3.通过Resources获取到资源id和外部传入的资源id对比,去加载资源

先判断下,获取的资源包资源是否为空

    /**
     * 判断资源包的资源对象是否为空
     * @return
     */
    public boolean resourceIsNull(){
        if (mResources == null){
            return  true;
        }
        return false;
    }

从资源包获取颜色资源
(1)先判断,获取的资源包资源是否为空,如果为空,则返回传入的资源id

    public int getColor(int resid){
        if (resourceIsNull()){
            return resid;
        }
      }

(2)根据传入的资源id 和context获取资源id的类型和名字

     //获取资源id的类型
        String resourceTypeName = mContext.getResources().getResourceTypeName(resid);
        //获取资源id的名字
        String resourceEntryName = mContext.getResources().getResourceEntryName(resid);

(3)通过创建的mResources去和外部传入的资源id匹配,如果匹配上,就返回mResources对应的资源id
获取颜色资源完整代码:

    /**
     * 获取颜色资源
     * @param resid     当前app更换资源的资源id
     * @return 匹配到的资源对象的资源id
     */
    public int getColor(int resid){
        if (resourceIsNull()){
            return resid;
        }
        //获取资源id的类型
        String resourceTypeName = mContext.getResources().getResourceTypeName(resid);
        //获取资源id的名字
        String resourceEntryName = mContext.getResources().getResourceEntryName(resid);
        //去匹配,获取外部pak资源的id去匹配
        int identifier = mResources.getIdentifier(resourceEntryName, resourceTypeName, packageName);
        if (identifier == 0){
            return  resid;
        }
        return mResources.getColor(identifier);
    }

从资源包中获取资源id对应的资源(图片资源)

    /**
     * 从资源包中获取资源id对应的资源
     * @param id
     * @return
     */
    public Drawable getDrawable(int id){
        if (resourceIsNull()){
            return ContextCompat.getDrawable(mContext,id);
        }
        //获取资源id的类型
        String resourceTypeName = mContext.getResources().getResourceTypeName(id);
        //获取资源id的名字
        String resourceEntryName = mContext.getResources().getResourceEntryName(id);
        //去匹配,获取外部pak资源的id去匹配
        int identifier = mResources.getIdentifier(resourceEntryName, resourceTypeName, packageName);
        if (identifier ==0){
            return ContextCompat.getDrawable(mContext,id);
        }
        return mResources.getDrawable(identifier);
    }
    
/**
     * 从资源包中获取资源id对应的资源
     * @param id
     * @return
     */
    public int getDrawableID(int id){
        if (resourceIsNull()){
            return id;
        }
        //获取资源id的类型
        String resourceTypeName = mContext.getResources().getResourceTypeName(id);
        //获取资源id的名字
        String resourceEntryName = mContext.getResources().getResourceEntryName(id);
        //去匹配,获取外部pak资源的id去匹配
        int identifier = mResources.getIdentifier(resourceEntryName, resourceTypeName, packageName);
        if (identifier ==0){
            return id;
        }
        return identifier;
    }

如果还需要添加其他的资源,自己按照这个方法添加就可以了

二、批量收集项目中需要换肤的控件

通过实现LayoutInflater.Factory2,去获取当前项目加载的控件,然后获取控件需要加载的属性,比如textColoe 和background或者src等属性,然后在获取加载的资源id,去批量替换资源包中文资源文件。

1.创建BaseActivity

创建BaseActivity继承AppCompatActivity,去创建LayoutInflater.Factory2实现类,
这里有个问题,是在API <=28的时候,需要去反射获取mFactorySet这个属性,并且设置相关参数,否则会报异常,大于28会报异常,目前还没有解决方案,所以想跑起来这个项目,需要把targetSdkVersion改成 28

class BaseActivity extends AppCompatActivity {

    private SkinFactory mSkinFactory;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        try {
            //解决继承AppCompatActivity SkinFactory创建报错
            setLayoutInflaterFactory(getLayoutInflater());
            mSkinFactory = new SkinFactory();
            LayoutInflaterCompat.setFactory2(getLayoutInflater(), mSkinFactory);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 改变mFactorySet的值,使继承AppCompatActivity不报错
     * 需要设置targetSdkVersion 28才有效,29 30会报错,需要额外适配
     *
     * @param origonal
     */
    public void setLayoutInflaterFactory(LayoutInflater origonal) {
        LayoutInflater layoutInflater = origonal;
        try {
            Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
            mFactorySet.setAccessible(true);
            mFactorySet.set(layoutInflater, false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.实现LayoutInflater.Factory2

class SkinFactory implements LayoutInflater.Factory2{
    @Nullable
    @Override
    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        Log.e("------>","11111111111111111111  "+name);
        }

    /**
     * 控件实例化
     * @param name
     * @param context
     * @param attrs
     * @return
     */
    @Nullable
    @Override
    public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {

	}
}

通过打印发现,上边的方法,可以打印出当前页面控件初始化时候的所有控件在这里插入图片描述
在这个方法里面就可以做控件的收集处理工作了,配个第二个方法,实例化控件

3.创建处理控件的实体类

class SkinItem{
        //属性名字 textcocor text background
        String name;
        //属性的值类型 color mipmap
        String typeName;
        //属性的值的名字
        String entryName;
        //属性的资源id
        int resId;

        public SkinItem(String name, String typeName, String entryName, int resId) {
            this.name = name;
            this.typeName = typeName;
            this.entryName = entryName;
            this.resId = resId;
        }


        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getTypeName() {
            return typeName;
        }

        public void setTypeName(String typeName) {
            this.typeName = typeName;
        }

        public String getEntryName() {
            return entryName;
        }

        public void setEntryName(String entryName) {
            this.entryName = entryName;
        }

        public int getResId() {
            return resId;
        }

        public void setResId(int resId) {
            this.resId = resId;
        }
    }
    /**
     * 需要换肤的控件封装对象
     */
    class SkinView{
        View view;
        List<SkinItem> skinItems;

        public SkinView(View view, List<SkinItem> skinItems) {
            this.view = view;
            this.skinItems = skinItems;
        }

        public void apply(){
        }
      }  

4.收集需要换肤的控件

Android所有的控件都是在
“android.widget.”,
“android.view.”,
“android.webkit”
这三个包下,因为获取的控件有的是全路径名字的,类似于:androidx.constraintlayout.widget.ConstraintLayout
有的是单独名字的,需要我们去给拼接包名,类似于这些控件:
Button,LinearLayout,ImageView,TextView
所以收集需要换肤的控件是两部操作,一步是全路径控件,一部分是需要拼接包名
实现代码如下:


   //安卓所有控件都是在这三种下
    private static  final  String[] prxfixList = {
      "android.widget.",
      "android.view.",
      "android.webkit"
    };
    //收集需要换肤的控件容器
    private List<SkinView> mViewList = new ArrayList<>();

  @Nullable
    @Override
    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        Log.e("------>","11111111111111111111  "+name);
        //收集需要换肤的控件

        View view = null;
        if (name.contains(".")){
            //带包名控件 类似于androidx.constraintlayout.widget.ConstraintLayout
            view = onCreateView(name, context, attrs);
        }else {
            //不带包名控件,类似于TextView LinearLayout
            for (String s : prxfixList){
                String viewName = s+name;
                view = onCreateView(viewName, context, attrs);
                if (view!=null){
                    break;
                }
            }
        }

        //收集需要换肤的控件
        if (view!= null){
            paserView(view,name,attrs);
        }
        return view;
    }
    /**
     * 收集需要换肤的控件
     * @param view
     * @param name
     * @param attrs
     */
    private void paserView(View view, String name, AttributeSet attrs) {
        List<SkinItem> skinItems = new ArrayList<>();
        //遍历的是这个当前进来的控件的所有属性
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            //得到属性名字
            String attributeName = attrs.getAttributeName(i);
            if (attributeName.contains("background") || attributeName.contains("textColor")|| attributeName.contains("src")){
                //认为是要收集的换肤的控件
                String attributeValue = attrs.getAttributeValue(i);
                //获取资源文件id
                int resId = Integer.parseInt(attributeValue.substring(1));
                //获取资源id的类型
                String resourceTypeName = view.getResources().getResourceTypeName(resId);
                //获取资源id的名字
                String resourceEntryName = view.getResources().getResourceEntryName(resId);
                SkinItem skinItem = new SkinItem(attributeName,resourceTypeName,resourceEntryName,resId);
                skinItems.add(skinItem);
            }
        }

        /**
         * 如果一个控件集合大小大于0 说明需要换肤
         */
        if (skinItems.size() >0){
            //需要更换,则添加到集合中
            SkinView skinView = new SkinView(view,skinItems);
            mViewList.add(skinView);
        }
    }

    /**
     * 控件实例化
     * @param name
     * @param context
     * @param attrs
     * @return
     */
    @Nullable
    @Override
    public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        View view = null;
        try {
            Class<?> aClass = context.getClassLoader().loadClass(name);
            //获取到第二个构造方法
            Constructor<? extends View> constructor = (Constructor<? extends View>) aClass.getConstructor(Context.class, AttributeSet.class);
            view = constructor.newInstance(context, attrs);
        }catch (Exception e){
            e.printStackTrace();
        }
        return view;
    }

5.批量替换控件属性

去遍历拿到的所有控件,去获取控件设置相关参数的属性,然后将资源id传给传给SkinManager去进行资源匹配,如果匹配到了,就直接设置给控件
在SkinView内的apply方法处理

        public void apply(){
            for (SkinItem skinItem : skinItems){
                //判断这条属性是background吗?
                if (skinItem.getName().equals("background")){
                    //如果是backound,设置背景有两种方式一种是color设置颜色,一种是drawable和mipmap设置背景
                    if (skinItem.getTypeName().equals("color")){
                        //将资源id 传给SkinManager 去进行资源匹配,如果匹配到了,就直接设置给控件
                        //如果没有匹配到,就把之前的资源id设置给控件
                        if (SkinManager.getInstance().resourceIsNull()){
                            view.setBackgroundResource(SkinManager.getInstance().getColor(skinItem.getResId()));
                        }else {
                            view.setBackgroundColor(SkinManager.getInstance().getColor(skinItem.getResId()));
                        }
                    }else if (skinItem.getTypeName().equals("drawable")||skinItem.getTypeName().equals("mipmap")){
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN){
                            view.setBackground(SkinManager.getInstance().getDrawable(skinItem.getResId()));
                        }else {
                            view.setBackgroundDrawable(SkinManager.getInstance().getDrawable(skinItem.getResId()));
                        }
                    }
                }else if (skinItem.getName().equals("src")){
                    if (skinItem.getTypeName().equals("drawable")||skinItem.getTypeName().equals("mipmap")){
                        ((ImageView)view).setImageResource(SkinManager.getInstance().getDrawableID(skinItem.getResId()));
                    }else if (skinItem.getTypeName().equals("color")){
                        ((ImageView)view).setImageResource(SkinManager.getInstance().getColor(skinItem.getResId()));
                    }
                }else if (skinItem.getName().equals("textColor")){
                    ((TextView)view).setTextColor(SkinManager.getInstance().getColor(skinItem.getResId()));
                }
            }
        }

外部SkinFactory在创建批量换肤方法,调用

    //批量换肤
    public void apply(){
        for (SkinView skinView :mViewList){
            skinView.apply();
        }
    }

在BaseActivity添加换肤方法

    public void apply() {
        mSkinFactory.apply();
    }
    @Override
    protected void onResume() {
        super.onResume();
        //其他隐藏的activiry进行资源替换
        mSkinFactory.apply();
    }

到此,动态加载相关代码以及编写完毕了,接下来,测试一下把。

三、测试相关代码

1.初始化SkinManager

public class MyApp  extends Application {
    public static MyApp appContext;

    @Override
    public void onCreate() {
        super.onCreate();
        appContext = this;
        //在application初始化context
        SkinManager.getInstance().setContext(this);
    }
}

2.创建资源包

调用换肤操作之前,需要去创建一个资源APK包,放一些和当前项目一模一样的资源文件,名字相同就行,资源可以不相同
项目资源文件:
在这里插入图片描述
在这里插入图片描述

创建一个项目skin:创建替换资源
在这里插入图片描述
在这里插入图片描述
通过这个生成apk包在这里插入图片描述
在这里插入图片描述
然后改名skin,长传到SD卡路径下:在这里插入图片描述
在这里插入图片描述

3.调用换肤操作

     button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SkinManager.getInstance().loadSkinApk(Environment.getExternalStorageDirectory()+"/skin.apk");
                //换肤
                apply();
            }
        });

自此,已经实现的批量替换皮肤功能


总结

项目代码:https://github.com/renbin1990/DynamicLoad

接下来就可以根据实际项目情况去做各种处理了,指定下载资源包路径,资源包不建议用apk后缀名,防止用户误装,setFactory 高于28报异常,肯定有解决方案,只是我还没有找到

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值