android 换肤框架搭建及使用 (3 完结篇)

news/2024/11/9 1:53:44/

本系列计划3篇:

  1. Android 换肤之资源(Resources)加载(一)
  2. setContentView() / LayoutInflater源码分析(二)
  3. 换肤框架搭建(三) — 本篇

tips: 本篇只说实现思路,以及使用,具体细节请下载代码查看!

本篇实现效果:

fragment换肤recyclerView换肤自定义view属性换肤
打开打开打开
动态换肤dialog换肤
打开打开

回顾

在第一篇中: 我们可以通过这段代码来创建自己的Resource来加载另一个apk中的资源

  try (// 创建AssetManagerAssetManager assetManager = AssetManager.class.newInstance()) {// 反射调用 创建AssetManager#addAssetPathMethod method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);// 获取到当前apk在手机中的路径String path = getApplicationContext().getPackageResourcePath();/// 反射执行方法method.invoke(assetManager, path);// 创建自己的ResourcesResources resources = new Resources(assetManager, createDisplayMetrics(), createConfiguration());// 根据id来获取图片Drawable drawable = resources.getDrawable(R.drawable.ic_launcher_background, null);// 设置图片mImageView.setImageDrawable(drawable);} catch (Exception e) {e.printStackTrace();}// 这些关于屏幕的就用原来的就可以public DisplayMetrics createDisplayMetrics() {return getResources().getDisplayMetrics();}public Configuration createConfiguration() {return getResources().getConfiguration();}

在第二篇中: 我们分析了setContentView() 加载流程, 并且分析了LayoutInflater加载view流程

并且我们知道了如何通过Factory来拦截View创建

第二篇不是最近写的,是很早之前写的.这里正好适合,就当作第二篇来使用!

拦截代码:

 class CustomParseActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {val layoutInflater = LayoutInflater.from(this)// 如果factory2 == null就创建if (layoutInflater.factory2 == null) {LayoutInflaterCompat.setFactory2(layoutInflater, object : LayoutInflater.Factory2 {// SystemAppCompatViewInflater 是粘贴自系统源码 [AppCompatViewInflater]val compatInflater = SystemAppCompatViewInflater()override fun onCreateView(parent: View?,name: String,context: Context,attrs: AttributeSet,): View? {// 在这里就可以拦截view的创建// Factory创建view val view = compatInflater.createView(parent, name, context, attrs, false,true,  true, false)return view}... })}// 必须在super 之前super.onCreate(savedInstanceState)setContentView(activity_custom_parse)}}

项目搭建思路

要想达到换肤效果,其实就是加载另一个APK中的资源文件,然后实现替换

现在我们已经知道了如何加载另一个APK中的资源,我们只需要保存起来需要替换的view即可,然后再特定的时机去调用它

在点击换肤的时候,刷新所有保存的view对象,让它自己去加载另一个APK中的资源即可

首先我们需要规定替换哪些资源:

例如有一个view:

 <Buttonandroid:id="@+id/bt1"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="@color/global_background"android:text="@string/global_re_skin"android:textSize="@dimen/global_def_text_font"android:textColor="@color/global_text_color" />

这里我们就可以替换

  • background
  • text
  • textSize
  • textColor

因为这些属性是经常用的,并且是引用的资源文件中的资源,我想没人需要替换width / height

知道了需要替换哪些资源后,我们就可以在解析view的时候来保存起来这些属性,然后在某个时机的时候手动刷新即可

整个框架搭建我是采用的 Application.ActivityLifecycleCallbacks 这个类可以监听到activity所有的生命周期

并且采用了观察者设计模式,单例等设计模式,来实现点击的时候刷新需要改变属性的view

在使用的时候 只需要 一行代码就可以搞定

 #Application.javapublic void onCreate(){SkinManager.init(this);  }

在解析属性的时候,我采用了enum的特性 方便解析给view对应属性赋值

例如这样:

 public enum SkinReplace {ANDROID_BACKGROUND("background") {@Overridevoid loadResource(View view, SkinAttr attr) {view.setBackgroundColor(XXX);}};private final String mName;SkinReplace(String value) {mName = value;}abstract void loadResource(View view, SkinAttr value);}

框架小细节

初始化factory

Application.ActivityLifecycleCallbacks#onActivityCreated() 执行时机为:

  • AppCompatActivity.super.onCreate() 之后
  • setContentView() 之前

我们由第二篇知道,Factory是在super.onCreate()中初始化的,并且Factory只能初始化一次,

在android28之前一般通过反射 LayoutInflater.mFactorySet 属性为false来实现加载我们的Factory

但是android28之后就不行了

那么android28之后版本我们可以通过反射来直接替换掉系统的Factory即可

 // 通过反射替换掉系统的factoryprivate SkinLayoutInflaterFactory forceSetFactory2(LayoutInflater inflater, Activity activity) {Class<LayoutInflater> inflaterClass = LayoutInflater.class;try {String mFactoryStr = "mFactory";Field mFactory = inflaterClass.getDeclaredField(mFactoryStr);mFactory.setAccessible(true);String mFactory2Str = "mFactory2";Field mFactory2 = inflaterClass.getDeclaredField(mFactory2Str);mFactory2.setAccessible(true);SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory(activity);// 改变factorymFactory2.set(inflater, skinLayoutInflaterFactory);mFactory.set(inflater, skinLayoutInflaterFactory);return skinLayoutInflaterFactory;} catch (Exception e) {e.printStackTrace();}return null;}

一定创建View成功

我们粘贴出来 AppCompatViewInflater.java的时候,只能创建系统的view

image-20230106140416691

我们必须创建view,因为我们需要通过view上的属性来判断它是否需要"换肤"

那么我们需要在这里的时候自己反射创建view[粘贴自LayoutInflater源码]

image-20230106140610061

这里看不懂没关系,如果单纯的使用来说一点也不重要!

使用框架前提

  1. 有一个皮肤包, 在一篇中皮肤包如何制作我说的很详细了!

image-20230106132303825

  1. 将皮肤包放入到手机内存中
  2. 记得读写权限,保证能够正常访问手机内存中的数据
  3. 引入lib-skin
  4. 在 Application.onCreate() 中初始化: SkinManager.init(this);

可以想像一下网易云,QQ等大厂的换肤, 点击一个按钮,然后下载一个皮肤包存储到手机中,然后我们去读取这个皮肤包的内容

最终我们只需要生成对应的皮肤包给到后台,然后我们就实现了动态的更换皮肤!

在Activity中换肤

如果你已经将皮肤包放入到了手机内存中,并且已经初始化了SkinManager

那么替换皮肤只需要一行代码:

 SkinManager.getInstance().loadSkin("皮肤包的在手机中的路径");

如果你不想使用皮肤包,那么也只需要一行代码:

  SkinManager.getInstance().reset();

现在你已经可以实现

  • src
  • text
  • text_color
  • text_size
  • background

换肤了!

如果还需要其他属性换肤,下面会提到,别急!

在Fragment中使用换肤

在fragment中使用皮肤包只需要注意一点:

在view创建完成的时候调用:

public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {super.onViewCreated(view, savedInstanceState);SkinManager.getInstance().tryInitSkin();
}

这是为了避免第一次初始化的时候加载不到皮肤

其他任何改变都不需要!

在RecyclerView中使用换肤

不需要任何处理

换肤:

SkinManager.getInstance().loadSkin("皮肤包的在手机中的路径"); // 换肤

恢复默认:

 SkinManager.getInstance().reset();	

自定义属性换肤

首先我们需要随便自定义一个view

image-20230106135201104

  1. 皮肤包中设置需要替换的资源

image-20230106135410339

  1. 编写改变属性的方法:

image-20230106135551412

4.在SkinReplace中规定需要改变的属性,并且通过反射调用对应方法

image-20230106135929195

反射方法:

 /** 作者:史大拿* 创建时间: 1/4/23 8:07 PM* TODO 自定义反射,反射具体方法属性* @param view: 需要反射的对象* @param methodName: 反射的方法名字* @param SkinReflectionMethod: 反射具体数据 [类型和参数]*/public void setCustomAttr(View view, String methodName, SkinReflectionMethod... data) {try {Class<?>[] cls = new Class<?>[data.length];Object[] objects = new Object[data.length];for (int i = 0; i < data.length; i++) {cls[i] = data[i].getCls();objects[i] = data[i].getObj();}Method method = view.getClass().getDeclaredMethod(methodName, cls);method.setAccessible(true);method.invoke(view, objects);} catch (Exception e) {e.printStackTrace();SkinLog.e("反射失败;" + e.getMessage() + "\t" + SkinConfig.SKIN_ERROR_7);}}

到此还是通过

SkinManager.getInstance().loadSkin(“皮肤包的在手机中的路径”);

换肤即可

动态换肤

动态换肤只需要在

SkinManager.getInstance().loadSkin(“皮肤包的在手机中的路径”);

之后调用对应方法即可

  • drwable SkinManager.getInstance().getDrawable(String)
  • string SkinManager.getInstance().getString(String)
  • color SkinManager.getInstance().getColor(String)
  • dimen SkinManager.getInstance().getFontSize(String)

例如这样:

 findViewById(R.id.bt_re_skin).setOnClickListener(v -> {// 换肤SkinManager.getInstance().loadSkin(PATH);mTextView.setBackground(SkinManager.getInstance().getDrawable("global_skin_drawable_background"));mTextView.setText(SkinManager.getInstance().getString("global_custom_view_text"));});

如果app中有一个A资源, 皮肤包中没有A资源,现在已经换肤了 那么还是默认使用app中的A资源

但是如果app中没有A资源,并且皮肤包中也没有A资源,那么就报错了

就是一句话:

如果当前是换肤状态,那么优先使用皮肤包中的资源,

如果皮肤包中的资源不存在,则使用app中的资源,如果都不存在,那么就报错

Dialog换肤

AlertDialog

 private AlertDialog alertDialog;private void showAlertDialog(View v) {// 避免重复解析皮肤包if (alertDialog == null) {View view = getLayoutInflater().inflate(R.layout.item_alert_dialog, null);alertDialog = new AlertDialog.Builder(this).setView(view).create();}if (!alertDialog.isShowing()) {alertDialog.show();}// 初始化第一次,避免第一次的时候没有换肤效果SkinManager.getInstance().tryInitSkin();}

dialog换肤也是非常简单,只需要Dialog.show()

的时候去

SkinManager.getInstance().tryInitSkin();

即可

DialogFragment换肤

这个dialog当作一个fragment用即可

和fragment注意事项相同,需要当view加载完成的时候在尝试刷新一下

 @Overridepublic void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {super.onViewCreated(view, savedInstanceState);SkinManager.getInstance().tryInitSkin();}

最后一点:换肤只能替换View的属性,因为Factory只能拦截View,不能拦截ViewGroup

完整项目地址

原创不易,您的点赞与关注就是对我最大的支持!

本篇结束,耗时15天从框架搭建到一行代码换肤,新年前最后一篇,最后祝大家新年快乐~ 年后见 🫡🫡

本系列计划3篇:

  1. Android 换肤之资源(Resources)加载(一)
  2. setContentView() / LayoutInflater源码分析(二)
  3. 换肤框架搭建(三) – 本篇

http://www.ppmy.cn/news/9899.html

相关文章

Java Bean Validation

JSR303 是一套JavaBean参数校验的标准&#xff0c;它定义了很多常用的校验注解&#xff0c;我们可以直接将这些注解加在我们JavaBean的属性上面&#xff0c;就可以在需要校验的时候进行校验了。校验框架注解如下&#xff1a; 注解解释Null被注释的元素必须为nullNotNull被注释…

成为有钱人的终极秘诀:做到这7步,你也可以成为富人!

经常有人问&#xff1a;互联网有什么快速赚钱的方法?大多数人内心浮躁&#xff0c;总想以最快的方式搞到钱。因为浮躁&#xff0c;所以沉不下心来去搞钱。做一个项目赚不到钱&#xff0c;然后又开始找项目&#xff0c;换项目&#xff0c;做项目&#xff0c;一直恶性循环中。最…

whistle的使用【前端抓包】

前言 抓包工具看起来只是测试要用的东西&#xff0c;其实对前端作用也很多&#xff0c;因为我们也要模拟请求、mock数据、调试。站在巨人肩膀上永远不亏! whistle能解决的痛点 一、看请求不方便 跳页、支付时候上一页的请求结果看不到&#xff0c;h5、小程序newWork不能在电…

SQL用法详解

1.SQL语言是什么?有什么作用?SQL:结构化查询语言&#xff0c;用于操作数据库&#xff0c;通用于绝大多数的数据库软件2.SQL的特征大小写不敏感需以;号结尾支持单行、多行注释3操作数据库的SQL语言基于功能可以划分为4类:数据定义:DDL ( Data Definition Language)&#xff1a…

GO——函数(一)

函数函数声明多返回值错误错误处理策略文件结尾错误(EOF)函数值函数声明 函数声明包括函数名、形式参数列表、返回值列表&#xff08;可省略&#xff09;以及函数体。 func name(parameter-list) (result-list) {body }返回值也可以像形式参数一样被命名。在这种情况下&#…

[VP]河南第十三届ICPC大学生程序竞赛 L.手动计算

前言 传送门 : 题意 : 给定两个椭圆 的方程 , 要求 求出椭圆并集的面积之和 思路 : 本题很显然是积分 或者 计算几何的问题 对于积分的做法, 无非就是根据积分公式求出第一象限的面积 之后拓展到后面四个象限。(奈何我懒, 连两个椭圆的焦点都不想求更别提后面的积分公式了)…

舆情监测技术方案,网络舆情分析技术手段有哪些?

网络舆情分析技术手段着力于利用技术实现对海量的网络舆情信息进行深度挖掘与分析&#xff0c;以快速汇总成舆情信息&#xff0c;从而代替人工阅读和分析网络舆情信息的繁复工作&#xff0c;接下来TOOM舆情监测小编带您简单了解舆情监测技术方案&#xff0c;网络舆情分析技术手…

Vue--》Vue3生命周期以及其它组合API的讲解

目录 生命周期 自定义hook函数 toRef shallowReactive与shallowRef readonly与shallReadonly toRaw与markRaw 生命周期 Vue3.0中可以继续使用Vue2.x中的生命周期钩子&#xff0c;但是有两个被更名&#xff1a;Vue2中的beforeDestroy改名为&#xff1a;beforeUnmount&…