Android的换肤原理和Android的皮肤,装载机框架解析

作者 : 开心源码 本文共11137个字,预计阅读时间需要28分钟 发布时间: 2022-05-12 共162人阅读

image.png

一 前言

Android的换肤技术已经是很久之前就已经被成熟使用的技术了,然而我最近才在学习和接触热修复的时候才看到,在看了少量换肤的方法之后,并且对市面上比较认可的Android的皮肤,装载机换肤框架的源码进行了分析总结。再次记录一下祭奠自己逝去的时间。

二 换肤详情

换肤本质上是对资源的一中替换包括,字体,颜色,背景,图片,大小等等。当然这些我们都有成熟的API可以通过控制代码逻辑做到。比方查看的修改背景颜色setBackgroundColor,TextView中的setTextSize。修改字体等等但是作为程序员我们怎样能忍受对每个页面的每个元素一个行行代码做换肤解决呢?我们需要用最少的代码实现最容易维护和使用效果完美(动态切换,及时生效)的换肤框架。

1.换肤方式一:切换使用主题主题

使用相同的资源ID,但在不同的主题下边自己设置不同的资源。我们通过主动切换到不同的主题从而切换界面元素创立时使用的资源。这种方案的代码量不多发,而且有个很显著的缺点不支持已经创立界面的换肤,必需重新加载界面元素.GitHub Demo

2. 换肤方式二:加载资源包

加载资源包是各种应用程序都在使用的换肤方法,例如我们最常用的输入法皮肤,浏览器皮肤等等。我们可以将皮肤的资源文件放入安装包内部,也可以进行下载缓存到磁盘上.Android的应用程序可以使用这种方式进行换肤.GitHub上面有一个开始非常高的换肤框架Android的皮肤下载器就是通过加载资源包对应用程序进行换肤。对这个框架的分析这个也是这篇文章主要的讲述内容。

比照一下发现切换主题可以进行小幅度的换肤设置(比方某个自己设置组件的主题),而假如我们想要对整个应用程序做主题切换那么通过加载资源包的这种方式目前应该说是比较好的了。

三 Android的换肤知识点

1. 换肤相应的API

我们先来看一下Android的提供的少量基本的API,通过使用这些API可以在应用程序内部进行资源对象的替换。

公共类资源{    public String getString(int id)throws NotFoundException {        CharSequence res = mAssets.getResourceText(id);        if(res!= null){            返回资源;        }        抛出新的NotFoundException(“字符串资源ID#0x”                                    + Integer.toHexString(id));    }    public Drawable getDrawable(int id)throws NotFoundException {        / ********部分代码省略******* /    }    public int getColor(int id)throws NotFoundException {{        / ********部分代码省略******* /    }    / ********部分代码省略******* /}

这个是我们常用的资源类的API,我们通常可以使用在资源文件中定义的@+id字符串类型,而后在编译出的R.java中对应的资源文件生产的编号(INT类型),从而通过这个ID(INT类型)调用资源提供的这些API获取到对应的资源对象。这个在同一个应用程序下没有任何疑问,但是在皮肤包中我们怎样获取这个ID值呢。

公共类资源{    / ********部分代码省略******* /    / ***通过给的资源名称返回一个资源的标识id。* @paramname形容资源的名称* @ paramdefType资源的类型* @paramdefPackage包名** @返回返回资源ID,0标识未找到该资源* /    public int getIdentifier(String name,String defType,String defPackage){        if(name == null){            抛出新的NullPointerException(“name is null”);        }        尝试{            return Integer.parseInt(name);        } catch(例外e){            // 忽视        }        return mAssets.getResourceIdentifier(name,defType,defPackage);    }}

资源提供了可以通过@+id,类型,PACKAGENAME这三个参数即可以在AssetManager中寻觅相应的软件包名中有没有输入类型并且ID值都能与参数对应上的ID,进行返回。而后我们可以通过这个ID再调用资源的获取资源的API即可以得到相应的资源。

我们这里需要注意的一点一的英文getIdentifier(String name, String defType, String defPackage)方法状语从句:getString(int id)方法所调用资源对象的mAssets对象必需是同一个,并且包含有PACKAGENAME这个资源包。

2.AssetManager构造

怎样构造一个包含特定的packageName资源的AssetManager对象实例呢?

public final class AssetManagerimplements AutoCloseable {    / ********部分代码省略******* /    / ***创立仅包含基本系统资产的新AssetManager。*应用程序通常不会使用此方法,而是检索* {@ linkResources#getAssets}的适当资产经理。不是为了*由应用程序使用。* {@hide}* /    public AssetManager(){        synchronized(this){            if(DEBUG_REFS){                mNumRefs = 0;                incRefsLocked(this.hashCode());            }            INIT(假);            if(localLOGV)Log.v(TAG,“新资产经理:”+这个);            ensureSystemAssets();        }    }

从AssetManager构造的函数来看有{@hide}的朱姐,所以在其余类里面是直接创立AssetManager实例。但是不要不记得的Java中还有反射机制可以创立类对象。

1AssetManager assetManager = AssetManager.class.newInstance();

让创立的assetManager包含特定的PACKAGENAME的资源信息,怎样办?我们在AssetManager中找到相应的API可以调用。

public final class AssetManagerimplements AutoCloseable {    / ********部分代码省略******* /    / ***向资产经理增加一组额外资产。这可以*目录或者ZIP文件。不适用于应用程序。返回*增加资产的cookie,或者失败时为0。* {@hide}* /    public final int addAssetPath(String path){        synchronized(this){            int res = addAssetPathNative(path);            if(mStringBlocks!= null){                makeStringBlocks(mStringBlocks);            }            返回资源;        }    }}

同样改方法也不支持外部调用,我们只能通过反射的方法来调用。

/ *** apk路径* /String apkPath = Environment.getExternalStorageDirectory()+“/ skin.apk”;AssetManager assetManager = null;尝试{    AssetManager assetManager = AssetManager.class.newInstance();    AssetManager.class.getDeclaredMethod(“addAssetPath”,String.class).invoke(assetManager,apkPath);} catch(Throwable th){    th.printStackTrace();}

至此我们可以构造属于自己换肤的资源了。

3.换肤资源构造

public Resources getSkinResources(Context context){    / ***插件apk路径* /    String apkPath = Environment.getExternalStorageDirectory()+“/ skin.apk”;    AssetManager assetManager = null;    尝试{        AssetManager assetManager = AssetManager.class.newInstance();        AssetManager.class.getDeclaredMethod(“addAssetPath”,String.class).invoke(assetManager,apkPath);    } catch(Throwable th){        th.printStackTrace();    }    返回新资源(assetManager,context.getResources()。getDisplayMetrics(),context.getResources()。getConfiguration());}

4.使用资源包中的资源换肤

我们将上述所有的代码组合在一起即可以实现,使用资源包中的资源对应用程序进行换肤。

public Resources getSkinResources(Context context){    / ***插件apk路径* /    String apkPath = Environment.getExternalStorageDirectory()+“/ skin.apk”;    AssetManager assetManager = null;    尝试{        AssetManager assetManager = AssetManager.class.newInstance();        AssetManager.class.getDeclaredMethod(“addAssetPath”,String.class).invoke(assetManager,apkPath);    } catch(Throwable th){        th.printStackTrace();    }    返回新资源(assetManager,context.getResources()。getDisplayMetrics(),context.getResources()。getConfiguration());}@覆盖protected void onCreate(Bundle savedInstanceState){    super.onCreate(savedInstanceState);    的setContentView(R.layout.activity_main);    ImageView imageView =(ImageView)findViewById(R.id.imageView);    TextView textView =(TextView)findViewById(R.id.text);    / ***插件资源对象* /    Resources resources = getSkinResources(this);    / ***获取图片资源* /    Drawable drawable = resources.getDrawable(resources.getIdentifier(“night_icon”,“drawable”,“com.tzx.skin”));    / ***获取文本资源* /    int color = resources.getColor(resources.getIdentifier(“night_color”,“color”,“com.tzx.skin”));    imageView.setImageDrawable(绘制);    textView.setText(文本);}

通过上述详情,我们可以简单的对当前页面进行换肤了。但是想要做出一个一个成熟换肤框架那么仅仅这些还是不够的,提高一下我们的思维高度,假如我们在查看创立的时候就直接使用皮肤资源包中的资源文件,那么这无疑就使换肤更加的简单已维护。

5. LayoutInflater.Factory

我看过一篇前遇见LayoutInflater及工厂文章的这部分可以省略掉。

很幸运的Android给我们在查看生产的时候做修改提供了法门。

公共笼统类LayoutInflater {    / ***部分代码省略**** /    公共接口工厂{        public View onCreateView(String name,Context context,AttributeSet attrs);    }    public interface Factory2extends Factory {        public View onCreateView(查看父级,字符串名称,上下文上下文,AttributeSet attrs);    }    / ***部分代码省略**** /}

我们可以给当前的页面的窗口对象在创立的时候设置工厂,那么在窗口中的视图进行创立的时候就会先通过自定义的工厂进行创立.Factory方式使用相关状语从句:注意事项请移位到遇见LayoutInflater及工厂,关于工厂的相关知识点尽在其中。

四 Android的皮肤,装载机解析

1. 初始化

  • 初始化换肤框架,导入需要换肤的资源包(当前为一个APK文件,其中只有资源文件)。
公共类SkinApplicationextends Application {    public void onCreate(){        super.onCreate();        initSkinLoader();    }    / ***必需先调用init* /    private void initSkinLoader(){        。SkinManager.getInstance()的init(本);        SkinManager.getInstance()负载();    }}

2.构造换肤对象

导入需要换肤的资源包,并构造换肤的资源实例。

/ ***在asyc任务中从apk加载资源* @ paramskinPackagePath皮肤路径apk* @paramcallback回调通知客户* /public void load(String skinPackagePath,final ILoaderListener callback){        新的AsyncTask(){        protected void onPreExecute(){            if(callback!= null){                callback.onStart();            }        };        @覆盖        protected资源doInBackground(String ... params){            尝试{                if(params.length == 1){                    String skinPkgPath = params [0];                                        File file = new File(skinPkgPath);                     if(file == null ||!file.exists()){                        return null;                    }                                        PackageManager mPm = context.getPackageManager();                    //检索程序外的一个安装包文件                    PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath,PackageManager.GET_ACTIVITIES);                    //获取安装包报名                    skinPackageName = mInfo.packageName;                    //构建换肤的AssetManager实例                    AssetManager assetManager = AssetManager.class.newInstance();                    方法addAssetPath = assetManager.getClass()。getMethod(“addAssetPath”,String.class);                    addAssetPath.invoke(assetManager,skinPkgPath);                    //构建换肤的资源实例                    资源superRes = context.getResources();                    资源skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());                    //存储当前皮肤路径                    SkinConfig.saveSkinPath(context,skinPkgPath);                                        skinPath = skinPkgPath;                    isDefaultSkin = false;                    return skinResource;                }                return null;            } catch(例外e){                e.printStackTrace();                return null;            }        };        protected void onPostExecute(参考资料结果){            mResources =结果;            if(mResources!= null){                if(callback!= null)callback.onSuccess();                //升级多有可换肤的界面                notifySkinUpdate();            }其余{                isDefaultSkin = true;                if(callback!= null)callback.onFailed();            }        };    } .execute(skinPackagePath);}

定义基类
换肤页面的基类的通用代码实现基本换肤功能。

public class BaseFragmentActivityextends FragmentActivityimplements ISkinUpdate,IDynamicNewView {        / ***部分代码省略**** /        //自己设置LayoutInflater.Factory    private SkinInflaterFactory mSkinInflaterFactory;        @覆盖    protected void onCreate(Bundle savedInstanceState){        super.onCreate(savedInstanceState);            尝试{            //设置LayoutInflater的mFactorySet为真,表示还未设置mFactory,否则会抛出异常。            Field field = LayoutInflater.class.getDeclaredField(“mFactorySet”);            field.setAccessible(真);            field.setBoolean(getLayoutInflater(),false);            //设置LayoutInflater的MFactory            mSkinInflaterFactory = new SkinInflaterFactory();            getLayoutInflater()setFactory(mSkinInflaterFactory)。        } catch(NoSuchFieldException e){            e.printStackTrace();        } catch(IllegalArgumentException e){            e.printStackTrace();        } catch(IllegalAccessException e){            e.printStackTrace();        }             }    @覆盖    protected void onResume(){        super.onResume();        //注册皮肤管理对象        。SkinManager.getInstance()连接(本);    }        @覆盖    protected void onDestroy(){        super.onDestroy();        //反注册皮肤管理对象        。SkinManager.getInstance()分离(本);    }    / ***部分代码省略**** /}

3.SkinInflaterFactory

SkinInflaterFactory进行查看的创立并对视图进行换肤。

构造查看
公共类SkinInflaterFactoryimplements Factory {    / ***部分代码省略**** /    public View onCreateView(String name,Context context,AttributeSet attrs){        //读取查看的皮肤:使属性,假的为不需要换肤        //假如不允许进行优化,请简单地跳过它        boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE,SkinConfig.ATTR_SKIN_ENABLE,false);        if(!isSkinEnable){                return null;        }        //创立视图        View view = createView(context,name,attrs);        if(view == null){            return null;        }        //假如视图创立成功,对视图进行换肤        parseSkinAttr(context,attrs,view);        返回视图;    }    //创立视图,类比可以查看LayoutInflater的createViewFromTag方法    private View createView(Context context,String name,AttributeSet attrs){        View view = null;        尝试{            if(-1 == name.indexOf('。')){                if(“查看”.equals(name)){                    view = LayoutInflater.from(context).createView(name,“android.view。”,attrs);                }                 if(view == null){                    view = LayoutInflater.from(context).createView(name,“android.widget。”,attrs);                }                 if(view == null){                    view = LayoutInflater.from(context).createView(name,“android.webkit。”,attrs);                }             } else {                view = LayoutInflater.from(context).createView(name,null,attrs);            }            李(“即将创造”+名称);        } catch(例外e){             Le(“创立时出错”“+ +名+”:“+ e.getMessage());            view = null;        }        返回视图;    }}

4.对生产的景观进行换肤

公共类SkinInflaterFactoryimplements Factory {    //存储当前活动中的需要换肤的查看    private List mSkinItems = new ArrayList();    / ***部分代码省略**** /    private void parseSkinAttr(Context context,AttributeSet attrs,View view){        //当前查看的所有属性标签        List viewAttrs = new ArrayList();                for(int i = 0; i <attrs.getAttributeCount(); i ++){            String attrName = attrs.getAttributeName(i);            String attrValue = attrs.getAttributeValue(i);                        假如(!AttrFactory.isSupportedAttr(attrName)){                继续;            }            //过滤视图属性标签中属性的值的值为引用类型            假如(attrValue.startsWith( “@”)){                尝试{                    int id = Integer.parseInt(attrValue.substring(1));                    String entryName = context.getResources()。getResourceEntryName(id);                    String typeName = context.getResources()。getResourceTypeName(id);                    //构造SkinAttr实例,attrname,ID,entryName参数typeName                    //属性的名称(背景),属性的ID值(INT类型),属性的ID值(@ + ID,串类型),属性的值类型(颜色)                    SkinAttr mSkinAttr = AttrFactory.get(attrName,id,entryName,typeName);                    if(mSkinAttr!= null){                        viewAttrs.add(mSkinAttr);                    }                } catch(NumberFormatException e){                    e.printStackTrace();                } catch(NotFoundException e){                    e.printStackTrace();                }            }        }        //假如当前视图需要换肤,那么增加在mSkinItems中        假如(!ListUtils.isEmpty(viewAttrs)){            SkinItem skinItem = new SkinItem();            skinItem.view = view;            skinItem.attrs = viewAttrs;            mSkinItems.add(skinItem);            //能否是使用外部皮肤进行换肤            假如(SkinManager.getInstance()。isExternalSkin()){                skinItem.apply();            }        }    }}

5.资源获取

通过当前的资源ID,找到对应的资源名称。再从皮肤包中找到该资源名称所对应的资源ID。

公共类SkinManagerimplements ISkinLoader {    / ***部分代码省略**** /    public int getColor(int resId){        int originColor = context.getResources()。getColor(resId);        //能否没有下载皮肤或者者当前使用默认皮肤        if(mResources == null || isDefaultSkin){            return originColor;        }        //根据渣油值获取对应的XML的的@ + ID的字符串类型的值        String resName = context.getResources()。getResourceEntryName(resId);        //更具resName在皮肤包的mResources中获取对应的渣油        int trueResId = mResources.getIdentifier(resName,“color”,skinPackageName);        int trueColor = 0;        尝试{            //根据渣油获取对应的资源值            trueColor = mResources.getColor(trueResId);        } catch(NotFoundException e){            e.printStackTrace();            trueColor = originColor;        }                return trueColor;    }    public Drawable getDrawable(int resId){...}}
其余

除此之外再添加以下对于皮肤的管理API(下载,监听回调,应用,取消,异常解决,扩展板块等等)。

五 总结

换肤就是这么简单?!?!

视频→

hook源码实现阿里无闪烁换肤链接:https://pan.baidu.com/s/1E9gdeeLADBiszUU-DFRWvw

想学习更多Android知识,或者者获取相关资料请加入Android技术开发交流2群:935654177。本群可免费获取Gradle,RxJava,小程序,Hybrid,移动架构,NDK,React Native,性能优化等技术教程!

FMNX`NW45P@2{~8XII3B4VF.png

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android的换肤原理和Android的皮肤,装载机框架解析

发表回复