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

news2024/11/24 7:46:14

本系列计划3篇:

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

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

本篇实现效果:

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

回顾

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

  try (
    // 创建AssetManager
    AssetManager assetManager = AssetManager.class.newInstance()
  ) {
    // 反射调用 创建AssetManager#addAssetPath
    Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);// 获取到当前apk在手机中的路径
    String path = getApplicationContext().getPackageResourcePath();
  
    /// 反射执行方法
    method.invoke(assetManager, path);
  
    // 创建自己的Resources
    Resources 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:

 <Button
     android: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.java
 public void onCreate(){
 SkinManager.init(this);  
 }

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

例如这样:

 public enum SkinReplace {
     ANDROID_BACKGROUND("background") {
         @Override
         void 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即可

 // 通过反射替换掉系统的factory
 private 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);
         // 改变factory
         mFactory2.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加载完成的时候在尝试刷新一下

 @Override
 public 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.coloradmin.cn/o/144303.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决第三方图片403问题

第三方平台怎么处理图片资源保护的? 服务端一般使用 Referer 请求头识别访问来源&#xff0c;然后处理资源访问。 Referer 是什么东西? 扩展参考: http://www.ruanyifeng.com/blog/2019/06/http-referer.html Referer是 HTTP 请求头的一部分&#xff0c;当浏览器向 Web 服务…

HTML实现舔狗日记

演示 css html, body {background: radial-gradient(#181818, #000000);margin: 0;padding: 0;border: 0;-ms-overflow-style: none;}::-webkit-scrollbar {width: 0.5em;height: 0.5em;background-color: #c7c7c7;}/*定义滚动条轨道 内阴影圆角*/::-webkit-scrollbar-track {…

不会写代码?也不懂技术?3分钟搭建电商cps系统搞副业

大家好&#xff0c;我是小悟 唠唠家常 以前见面聊天&#xff0c;大家都习惯性会问“你吃饭了吗”&#xff0c;现在大家一出口就是“你阳了吗”。2023年元旦过去了&#xff0c;你还阳着么&#xff1f;不出意外的话就会出意外&#xff0c;小悟也已经中招过了&#xff0c;在家躺…

【Linux】tcpdump命令详解

1、列出本机所有的网卡接口 tcpdump -D2、捕获特定网口的数据包 tcpdump -i bond0.1083、捕获具体数量的数据包 tcpdump -c 5 -i eth04、捕获的数据包保存到指定的文件 tcpdump -w 0001.pcap -i eth05、捕获的数据包显示IP而不

E4402B频谱分析仪

18320918653 E4402B E4402B|Agilent|3G|频谱分析仪|安捷伦|9kHz至3GHz 品牌&#xff1a;安捷伦 Agilent 惠普 HP 测量速度&#xff1a;28次更新/秒 测量精度&#xff1a;1dB 可选用的10Hz分辨事宽滤波器 机箱可容纳6插槽选件卡 97dB三阶动态范围 能在现场使用的坚固&a…

(1分钟速览)SLAM问题中一般方程和超定方程的求解

今天在学习的过程中偶然看到了一个博客&#xff0c;总结Axb的&#xff0c;那么我也写一篇。首先就是判断A的秩和(A|b)的秩之间的关系&#xff0c;然后通过这个关系来进行进一步地判断。编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;求解方…

RabbitMQ通配符模式

&#x1f341;博客主页&#xff1a;&#x1f449;不会压弯的小飞侠 ✨欢迎关注&#xff1a;&#x1f449;点赞&#x1f44d;收藏⭐留言✒ ✨系列专栏&#xff1a;&#x1f449;Linux专栏 &#x1f525;欢迎大佬指正&#xff0c;一起学习&#xff01;一起加油&#xff01; 目录&…

Jenkins安装方式之war包及相关环境配置

持续创作&#xff0c;加速成长&#xff01;这是我参与「掘金日新计划 10 月更文挑战」的第4天&#xff0c;点击查看活动详情 最近总有小伙伴发私信问我jenkins如何以war形式运行&#xff1f;以及运行后如何添加相关的环境配置&#xff0c;这里我就给大家贴出我的解决方案&…

Bandit算法学习[网站优化]04——UCB(Upper Confidence Bound) 算法

Bandit算法学习[网站优化]04——UCB(Upper Confidence Bound) 算法 参考资料 White J. Bandit algorithms for website optimization[M]. " O’Reilly Media, Inc.", 2013.https://github.com/johnmyleswhite/BanditsBookUCB算法原理及其在星际争霸比赛中的应用Aue…

Springboot 接口为null的值不返回对应的key

偶然听到两个应届生一段对话&#xff0c;一个后端&#xff0c;一个前端 。 前端&#xff1a; 大哥&#xff0c;你没有值就不要返回那个key行不行&#xff1f; 后端&#xff1a; 什么我看看。 后端&#xff1a; 这是本来返回值实体有的&#xff0c;不是必填&#xff0c;所以n…

Lua 元表及常见元方法

一、什么是元表 Lua 中的 table 使用起来有点像c中的 map 或者 unordered_map &#xff0c;都是通过对应的key 获取对应的value。如果访问了表中不存在的key时&#xff0c;就会触发Lua的一种机制&#xff0c;Lua也正是凭借这个机制可以用来模拟类似“继承”的行为&#xff0c;…

低代码能够为企业带来什么?

目录 1、为企业快速开发应用赋能 2、低成本使用数字化工具 3、满足企业定制化需求 大数据时代的快速发展下&#xff0c;传统的应用开发技术手段渐渐地无法满足企业的高需求。并且&#xff0c;企业想在应用开发的基础上同时实现个性化定制&#xff0c;而传统的技术条件所需要…

Linux 进程概念(一)

目录 一、冯诺伊曼体系结构 二、操作系统&#xff08;Operator System) 2.1 操作系统如何管理硬件&#xff1f; 2.2 操作系统如何管理软件&#xff1f; 2.3 一张图带你直观了解OS管理过程 三、进程&#xff08;启示录&#xff09; 3.1 进程的基本概念 3.1.1 进程PCB 3…

寒假本科创新——机器学习(二)

绪论1.3归纳偏好 一般原则&#xff1a;奥卡姆剃刀 什么样的算法比较好&#xff1f;1.4NFL定理 NFL定理的前提&#xff1a; NFL定理的寓意&#xff1a;1.3归纳偏好 归纳偏好&#xff08;lnductive Bias&#xff09;&#xff1a; 机器学习算法在学习过程中对某种类型假设的偏好…

sql语句练习2

1、列出至少有一个员工的所有部门编号、名称&#xff0c;并统计出这些部门的平均工资、最低工资、最高工资 做法&#xff1a; 第一步&#xff1a;找出至少有一个员工的部门编号mysql>select deptno,count(empno)from empgroup by deptnohaving count(empno)>0; 第二步: …

node.js快速入门指南

Node.js迅速蹿红&#xff0c;衍生了一个强大的开源社区、支持企业&#xff0c;甚至还拥有属于自己的技术大会。我把这种成功归结于它的简介&#xff0c;高校&#xff0c;同时提高了编程生产力。 Node.js 的前置知识很多&#xff0c;例如以下知识 JavaScriptES6Ajax 还不会的…

Java JVM:虚拟机性能监控、故障处理工具(三)

目录标题一、基础故障处理工具二、可视化故障处理工具三、其他故障相关一、基础故障处理工具 JMC&#xff08;Java Mission Control&#xff09;以及JFR&#xff08;Java Flight Recorder&#xff09; JMC 从 Java7 以后包含在 JDK 中&#xff0c;直接输入 jmc 就能启动在使用…

JEECGboot数据规则篇

使用 一、功能说明 列表数据权限&#xff0c;主要通过数据权限控制行数据&#xff0c;让不同的人有不同的查看数据规则&#xff1b; 比如&#xff1a; 销售人员只能看自己的数据&#xff1b;销售经理可以看所有下级销售人员的数据&#xff1b;财务只看金额大于5000的数据等等…

js事件高级

文章目录一、注册事件&#xff08;绑定事件&#xff09;1、注册事件概述&#xff08;1&#xff09;传统注册方式&#xff08;2&#xff09;方法监听注册方式2、addEventListener 事件监听方式3、attachEvent 事件监听方式4、注册事件兼容性解决方案二、删除事件&#xff08;解绑…

C库函数:string.h

string.h C 标准库 – <string.h> | 菜鸟教程 (runoob.com) 1void *memchr(const void *str, int c, size_t n) 在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c&#xff08;一个无符号字符&#xff09;的位置。在这个函数中&#xff0c;可以看到有void …