Android 插件化换肤方案

article/2025/6/10 9:34:29

效果

在这里插入图片描述

实现流程

  1. 实现LayoutInflater.Factory2这个接口,实现onCreateView方法(主要仿照系统原来LayoutInflater.createView()方法的实现),此处可以拿到页面中所有的View,判断有没有需要换肤的View,并且保存下来;
  2. 在Activity创建成功后,setContentView()前把我们自定义的Factory2设置给当前Activity的LayoutInflater,这样LayoutInflater在加载View的时候就会使用我们自定义的Factory2来加载;
  3. 在点击换肤按钮时,使用AssetManager加载皮肤包里面的资源文件得到皮肤包里面的资源;
  4. 循环遍历需要换肤的View,设置需要换肤的属性的值为皮肤包里面的资源,换肤完成;
  1. 此处需要了解Activity的页面View的创建流程,主要就是LayoutInflater里面的 createView()方法;
  2. Android 资源文件的加载,主要就是AssetManager里面的addAssetPath()的方法,APP里面的所有资源文 件都是通过AssetsManager来获取的,Resources类只是包装了一下,最终都是使用AssetsManager来获取的
    对这两块感兴趣的同学,可以自己去了解一下

步骤一:实现LayoutInflater.Factory2接口,保存需要换肤的View

class SkinLayoutInflaterFactory(val activity: Activity) :LayoutInflater.Factory2, Observer {//所有View的类包名private val mClassPrefixList =arrayOf("android.widget.", "android.webkit.", "android.app.", "android.view.")//View的两个参数的构造函数格式private val mConstructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)//缓存View的构造函数private val mConstructorMap = HashMap<String, Constructor<out View?>>()// 当选择新皮肤后需要替换View与之对应的属性// 页面属性管理器private var skinAttribute: SkinAttribute? = SkinAttribute()/*** 创建View的方法*/override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {var view: View? = createSDKView(name, context, attrs)if (null == view) {view = createView(name, context, attrs)}//这就是我们加入的逻辑if (null != view) {//判断这个view需不需要换肤(有没有需要换肤的属性),如果有,则记录下来//使用skinAttribute 类来保存相关属性skinAttribute!!.look(view, attrs)}return view}private fun createSDKView(name: String, context: Context, attrs: AttributeSet): View? {//如果包含 . 则不是SDK中的view 可能是自定义view包括support库中的Viewif (-1 != name.indexOf('.')) {return null}//不包含就要在解析的 节点 name前,拼上: android.widget. 等尝试去反射for (i in mClassPrefixList.indices) {val view = createView(mClassPrefixList.get(i) + name,context, attrs)if (view != null) {return view}}return null}/*** 根据View的构造方法,反射创建对应的View*/private fun createView(name: String, context: Context, attrs: AttributeSet): View? {//反射获取View 两个参数的构造方法val constructor = findConstructor(context, name)constructor?.let {try {//反射创建对象return it.newInstance(context, attrs)} catch (e: Exception) {}}return null}/*** 反射获取View的两个参数的构造方法,并且缓存起来,仿照系统写法*/private fun findConstructor(context: Context, name: String): Constructor<out View?>? {var constructor: Constructor<out View?>? = mConstructorMap.get(name)if (constructor == null) {try {val clazz = context.classLoader.loadClass(name).asSubclass(View::class.java)constructor = clazz.getConstructor(*mConstructorSignature)mConstructorMap.put(name, constructor)} catch (e: Exception) {}}return constructor}override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {return  null}/*** 接收到换肤事件,修改所有view的皮肤*/override fun update(o: Observable?, arg: Any?) {//修改状态栏的颜色//SkinThemeUtils.updateStatusBarColor(activity)//换肤skinAttribute!!.applySkin()}/*** 清楚数据*/fun clear(){skinAttribute = null}}

这个类实现了Observer 接口,在皮肤需要改变时进行换肤

SkinAttribute类里面保存了需要换肤的相关View和属性:

class SkinAttribute {//需要换肤的属性private val mAttributes: MutableList<String> = ArrayList()init {mAttributes.add("background")mAttributes.add("src")mAttributes.add("textColor")mAttributes.add("drawableLeft")mAttributes.add("drawableTop")mAttributes.add("drawableRight")mAttributes.add("drawableBottom")}//记录换肤需要操作的View与属性信息private val mSkinViews: MutableList<SkinView> = ArrayList()/*** 记录下一个VIEW身上哪几个属性需要换肤*/ fun look(view: View, attrs: AttributeSet) {val mSkinPars: MutableList<SkinPair> = ArrayList()for (i in 0 until attrs.attributeCount) {//获得属性名  textColor/backgroundval attributeName = attrs.getAttributeName(i)if (mAttributes.contains(attributeName)) {// #// ?722727272// @722727272val attributeValue = attrs.getAttributeValue(i)// 比如color 以#开头表示写死的颜色 不可用于换肤if (attributeValue.startsWith("#")) {continue}var resId: Int// 以 ?开头的表示使用 属性resId = if (attributeValue.startsWith("?")) {val attrId = attributeValue.substring(1).toInt()getResId(view.context, intArrayOf(attrId)).get(0)} else {// 正常以 @ 开头attributeValue.substring(1).toInt()}val skinPair = SkinPair(attributeName, resId)mSkinPars.add(skinPair)}}if (!mSkinPars.isEmpty() || view is SkinViewSupport) {val skinView = SkinView(view, mSkinPars)// 如果选择过皮肤 ,调用 一次 applySkin 加载皮肤的资源skinView.applySkin()mSkinViews.add(skinView)}}/*对所有的view中的所有的属性进行皮肤修改*/fun applySkin() {for (mSkinView in mSkinViews) {mSkinView.applySkin()}}/*** 获得theme中的属性中定义的 资源id* @param context* @param attrs* @return*/fun getResId(context: Context, attrs: IntArray): IntArray {val resIds = IntArray(attrs.size)val a = context.obtainStyledAttributes(attrs)for (i in attrs.indices) {resIds[i] = a.getResourceId(i, 0)}a.recycle()return resIds}}internal class SkinView(var view: View,//这个View的能被 换肤的属性与它对应的id 集合var skinPairs: List<SkinPair>) {/*** 对一个View中的所有的属性进行修改* 最终换肤的方法*/fun applySkin() {//对实现了SkinViewSupport接口的自定义View进行换肤applySkinSupport()//对当前View进行换肤for (skinPair in skinPairs) {var left: Drawable? = nullvar top: Drawable? = nullvar right: Drawable? = nullvar bottom: Drawable? = nullwhen (skinPair.attributeName) {"background" -> {val background: Any? = SkinResources.getBackground(skinPair.resId)background?.let {//背景可能是 @color 也可能是 @drawableif (it is Int) {view.setBackgroundColor(it)} else {ViewCompat.setBackground(view, it as Drawable)}}}"src" -> {val background: Any? = SkinResources.getBackground(skinPair.resId)background?.let {if (it is Int) {(view as ImageView).setImageDrawable(ColorDrawable((it as Int?)!!))} else {(view as ImageView).setImageDrawable(it as Drawable?)}}}"textColor" -> (view as TextView).setTextColor(SkinResources.getColorStateList(skinPair.resId))"drawableLeft" -> left = SkinResources.getDrawable(skinPair.resId)"drawableTop" -> top = SkinResources.getDrawable(skinPair.resId)"drawableRight" -> right = SkinResources.getDrawable(skinPair.resId)"drawableBottom" -> bottom = SkinResources.getDrawable(skinPair.resId)else -> {}}if (null != left || null != right || null != top || null != bottom) {(view as TextView).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom)}}}/*** 对实现了SkinViewSupport接口的自定义View进行换肤*/private fun applySkinSupport() {if (view is SkinViewSupport) {(view as SkinViewSupport).applySkin()}}
}/*** 需要换肤的属性名和资源id*/
internal class SkinPair(//属性名var attributeName: String,//对应的资源idvar resId: Int)

look() 方法是查找这个View 有没有需要换肤的属性
applySkin() 换肤的方法

SkinViewSupport是一个接口,View实现了这个接口,也会执行换肤,主要用于自定义View的情况

interface SkinViewSupport {//换肤方法fun applySkin()
}

步骤二:在Activity创建成功后,setContentView()前把我们自定义的Factory2设置给当前Activity的LayoutInflater,这样LayoutInflater在加载View的时候就会使用我们自定义的Factory2来加载

我们需要在setContentView()前设置Factory2,有两种方法:

  1. 使用BaseActvity
  2. 我们可以使用ActivityLifecycleCallbacks这个接口,这个接口会可以监听所有Activity 的 onCreate、onRestart、onResume等生命周期

执行顺序:onCreate方法先与onActivityCreated调用。onCreate方法里super之前的代码先执行,其次执行onActivityCreated的代码,最后执行onCreate方法里super之后的代码。

此处我们采用方案二
新建一个类ApplicationActivityLifecycle,实现ActivityLifecycleCallbacks接口:

class ApplicationActivityLifecycle(val mObserable: Observable) : ActivityLifecycleCallbacks {//保存当前Activity 和 LayoutInflaterFactory 的对应关系private val mLayoutInflaterFactories = ArrayMap<Activity, SkinLayoutInflaterFactory>()/*** Activity 创建完成回调*/override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {//使用factory2 设置布局加载工程val skinLayoutInflaterFactory = SkinLayoutInflaterFactory(activity)/*** 更新布局视图*///获得Activity的布局加载器val layoutInflater = activity.layoutInflaterif (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {try {//Android 布局加载器 使用 mFactorySet 标记是否设置过Factory//如设置过则会抛出异常//设置 mFactorySet 标签为falseval field = LayoutInflater::class.java.getDeclaredField("mFactorySet")field.isAccessible = truefield.setBoolean(layoutInflater, false)LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory)} catch (e: Exception) {e.printStackTrace()}} else {//安卓9以上版本,mFactorySet 不存在了,所有直接mFactory2设置反射mFactory2try {val field = LayoutInflater::class.java.getDeclaredField("mFactory2")field.isAccessible = truefield[layoutInflater] = skinLayoutInflaterFactory} catch (e: Exception) {e.printStackTrace()}}//保存Activity 和 skinLayoutInflaterFactory的对应关系mLayoutInflaterFactories[activity] = skinLayoutInflaterFactory//添加观察者mObserable.addObserver(skinLayoutInflaterFactory)}override fun onActivityStarted(activity: Activity) {}override fun onActivityResumed(activity: Activity) {}override fun onActivityPaused(activity: Activity) {}override fun onActivityStopped(activity: Activity) {}override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}/*** 页面推出时,清除数据*/override fun onActivityDestroyed(activity: Activity) {val observer = mLayoutInflaterFactories.remove(activity)  as SkinLayoutInflaterFactoryobserver.clear()SkinManager.deleteObserver(observer)}}

步骤三:在点击换肤按钮时,使用AssetManager加载皮肤包里面的资源文件得到皮肤包里面的资源;

SkinManager 是皮肤管理类,主要用来注册ApplicationActivityLifecycle,加载皮肤等

object SkinManager: Observable() {/*** Activity生命周期回调*/private var skinActivityLifecycle: ApplicationActivityLifecycle? = nullprivate var mContext: Application? = null/*** 初始化 必须在Application中先进行初始化* @param application* @param skinPath 默认的皮肤路径*/fun init(application: Application,skinPath:String?) {mContext = application//资源管理类 用于从 app/皮肤 中加载资源SkinResources.init(application.applicationContext)//注册Activity生命周期,并设置被观察者skinActivityLifecycle = ApplicationActivityLifecycle(this)application.registerActivityLifecycleCallbacks(skinActivityLifecycle)//加载上次使用保存的皮肤loadSkin(skinPath)}/*** 记载皮肤并应用** @param skinPath 皮肤路径 如果为空则使用默认皮肤*/fun loadSkin(skinPath: String?) {if (TextUtils.isEmpty(skinPath)) {//还原默认皮肤SkinResources.reset()} else {try {//宿主app的 resources;val appResource = mContext!!.resources////反射创建AssetManager 与 Resourceval assetManager = AssetManager::class.java.newInstance()//资源路径设置 目录或压缩包val addAssetPath =assetManager.javaClass.getMethod("addAssetPath", String::class.java)addAssetPath.invoke(assetManager, skinPath)//根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建Resourcesval skinResource = Resources(assetManager, appResource.displayMetrics,appResource.configuration)//获取外部Apk(皮肤包) 包名val mPm = mContext!!.packageManagerval info = mPm.getPackageArchiveInfo(skinPath!!, PackageManager.GET_ACTIVITIES)val packageName = info!!.packageNameSkinResources.applySkin(skinResource, packageName)} catch (e: Exception) {e.printStackTrace()}}//通知采集的View 更新皮肤//被观察者改变 通知所有观察者setChanged()notifyObservers(null)}
}

最后还有一个皮肤资源管理类,保存当前默认的资源与皮肤的资源,获取皮肤资源里面的颜色、图片资源ID

object SkinResources {//皮肤包的包名private var mSkinPkgName: String? = null//当前是否默认皮肤private var isDefaultSkin = true// app原始的resourceprivate var mAppResources: Resources? = null// 皮肤包的resourceprivate var mSkinResources: Resources? = null/*** 初始化方法*/fun init(context: Context) {mAppResources = context.resources}/*** 重置为默认*/fun reset() {mSkinResources = nullmSkinPkgName = ""isDefaultSkin = true}/*** 换肤* 设置当前皮肤的属性*/fun applySkin(resources: Resources?, pkgName: String?) {mSkinResources = resourcesmSkinPkgName = pkgName//是否使用默认皮肤isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null}/*** 1.通过原始app中的resId(R.color.XX)获取到自己的 名字* 2.根据名字和类型获取皮肤包中的ID*/fun getIdentifier(resId: Int): Int {if (isDefaultSkin) {return resId}val resName = mAppResources!!.getResourceEntryName(resId)val resType = mAppResources!!.getResourceTypeName(resId)return mSkinResources!!.getIdentifier(resName, resType, mSkinPkgName)}/*** 输入主APP的ID,到皮肤APK文件中去找到对应ID的颜色值* @param resId* @return*/fun getColor(resId: Int): Int {if (isDefaultSkin) {return mAppResources!!.getColor(resId)}val skinId = getIdentifier(resId)return if (skinId == 0) {mAppResources!!.getColor(resId)} else mSkinResources!!.getColor(skinId)}fun getColorStateList(resId: Int): ColorStateList {if (isDefaultSkin) {return mAppResources!!.getColorStateList(resId)}val skinId = getIdentifier(resId)return if (skinId == 0) {mAppResources!!.getColorStateList(resId)} else mSkinResources!!.getColorStateList(skinId)}fun getDrawable(resId: Int): Drawable? {if (isDefaultSkin) {return mAppResources!!.getDrawable(resId)}//通过 app的resource 获取id 对应的 资源名 与 资源类型//找到 皮肤包 匹配 的 资源名资源类型 的 皮肤包的 资源 IDval skinId = getIdentifier(resId)return if (skinId == 0) {mAppResources!!.getDrawable(resId)} else mSkinResources!!.getDrawable(skinId)}/*** 可能是Color 也可能是drawable** @return*/fun getBackground(resId: Int): Any? {val resourceTypeName = mAppResources!!.getResourceTypeName(resId)return if ("color" == resourceTypeName) {getColor(resId)} else {// drawablegetDrawable(resId)}}
}

最后我们在Application进行初始化,在Activity里面添加一个按钮,进行换肤:
初始化:

class MyApplication: Application() {override fun onCreate() {super.onCreate()SkinManager.init(this,null)}
}

换肤:

fun change(view: View?) {//换肤,皮肤包是独立的apk包,可以来自网络下载if(isDark){SkinManager.loadSkin(null)}else{SkinManager.loadSkin("/data/data/com.ping.xskin/darktheme-debug.apk")}isDark = !isDark}

皮肤包就是一个APK,在里面加上和默认资源相同名字的图片、颜色和其它的资源文件,打包APK后,放到指定的路径使用SkinManager.loadSkin 进行加载就可以了

项目代码:XSink

后续修改:
1.状态栏的适配
2.皮肤包的压缩


http://chatgpt.dhexx.cn/article/DiDa7OVI.shtml

相关文章

墨迹天气桌面挂件换肤分析

该篇文章需要准备如下工具&#xff1a; 1、墨迹皮肤文件&#xff0c;下载地址。 Andorid换肤在网上搜索出来的结果&#xff0c;大概有三种&#xff1a; 1、应用本身带有写好的布局 优点&#xff1a;开发难度低 缺点&#xff1a;灵活性低&#xff0c;用户不能自定义皮肤。 2、使…

前端换肤的一些思考

先看看大家怎么做的。下面是两篇别人写的文章&#xff0c;最后是我自己的方法。 第一篇&#xff1a;聊一聊前端换肤 之前在做网站换肤&#xff0c;所以想谈谈网站换肤的实现。网页换肤就是修改颜色值&#xff0c;因此重点就在于怎么来替换。 一般实现 如上图&#xff0c;我们…

Visio保存为网页出错

visio 试图保存文档时出错。已创建的页面可能无效。 试图保存文档时出错。以创建的页面可能无效。最近在用Visio作图的时候时长发生的问题。 经过摸索&#xff0c;不覆盖保存没有问题&#xff0c;如果覆盖保存&#xff0c;有时会有这个问题。解决办法就是把以前生成的网页和相关…

Visio 2016软件

Visio 2016是微软官方最新发布的一款领先的图表解决方案&#xff0c;它可以帮助企业制作定义流程、编辑最佳方案的同时还可以建立可视化计划变革的一款实用工具。这款软件目前提供了&#xff1a;上手图例&#xff08;starter diagrams&#xff09;、成百上千的智能形状、一步数…

解决visio和office365冲突,无法安装visio问题

本教程用于解决office365和visio冲突&#xff0c;无法安装visio问题 目前系统中已经安装Office365 尚未安装visio专业版本 1、下载office增强工具。 office增强工具下载地址 右键解压&#xff0c;此处要记得解压的目录&#xff08;建议在当前目录下&#xff09;。 根据自…

Visio软件

Visio使用小技巧 上标、下标快捷键 上标&#xff1a;CtrlShift“”下标&#xff1a;Ctrl“” 自定义图形旋转角度 当点击菜单栏上的视图——任务窗格——大小和位置选项&#xff0c;在页面编辑区的左下方出现一个大小和位置窗口。 大小和位置窗口如下&#xff1a; 自…

Visio文件编辑查看工具Visio Viewer for Mac

Visio Viewer for Mac可以打开和查看Visio文件&#xff08;.vsd、.vdx和.vsdm文件&#xff09;。它具有简单易用的用户界面&#xff0c;可以快速加载和显示Visio文件。此外&#xff0c;它还支持导出文件为PDF、PNG、JPEG等格式&#xff0c;方便用户进行文件转换和共享。 Visio…

与你一起学习Microsoft Visio——基础篇

Hi&#xff0c;你好&#xff01;我是大黄蜂&#xff0c;非常高兴借此机会与你一起学习MS Visio的相关知识和技能。这一次的分享主要是结合本人在实际使用Visio过程中的一些方法技巧并总结整理其他人分享的知识&#xff0c;其中有一些材料则来源于互联网&#xff0c;期待通过我的…

【Visio】 windows Visio 画图

1、Visio如何画圆形 画圆用 里面的第二个画圆工具 &#xff0c;用这个工具可以画出圆或者椭圆。如果想得到一个正圆&#xff0c;在画的时候按住shift键即可。 画好一个圆之后单击它&#xff0c;再单击 填充选项&#xff0c;选择你想填充的颜色&#xff0c;选黑色的话就会出现一…

visio2019 专业版,两种方法

安装好visio后&#xff0c;有两种方法jihuo&#xff1a;方法2中自带安装包&#xff0c;有需要可以下载 注意:visio版本要和电脑上带的其他office版本保持一致&#xff0c;否则会不兼容。 1.参考这篇文章&#xff0c;使用代码jihuo https://blog.csdn.net/qq_39400113/article…

Microsoft Visio-Microsoft Visio下载

Microsoft Visio 2013可以帮助你以更直观的方式创建图表的新功能。Microsoft Visio 2013提供共同编写功能&#xff0c;可使团队协作变得更加容易。你也可以通过Microsoft Visio 2013软件增强图表的动态性&#xff0c;方法是将形状链接到实时数据&#xff0c;然后使用 SharePoin…

网页草图利器:Visio Stencils for Information Architects

以前&#xff0c;一直为如何在正式编码开发前与用户确认一个需求而苦恼&#xff0c;因为在互联网企业做内部系统开发&#xff0c;开发周期实在太短&#xff0c;以至于几乎不太可能先给用户一个原型已确认前端UI的需求&#xff0c;因此多数情况下我们会选择画一个Web界面草图给用…

visio对象放入word显示不全_这个可以代替Visio的流程图绘制软件,你值得拥有,还有网页版的~...

一、开篇前言 大家好&#xff0c;大飞鸽就是我&#xff0c;我就是大飞鸽。 流程图大家都熟悉&#xff0c; 像化工专业工艺流程图、 实验方案技术路线图等等。 流程图的优势也显而易见&#xff0c; 不但可以帮助自己梳理思路&#xff0c; 而且也可以让读者一目了然。 常用的绘制…

用Python将音频内容转换为文本格式,方言可以吗?

当对一个或多个人的谈话进行记录时&#xff0c;采用一种高度准确和自动化的方式将口语提取为文本非常有用。转换成文字后&#xff0c;便可以将其用于进一步分析或用作其他功能。 在本教程中&#xff0c;我们将使用称为AssemblyAI&#xff08;https://www.assemblyai.com/&…

python批量转换音频格式,flac转mp3等

可以批量的将一种格式的音频文件转化成指定的格式&#xff0c; 如&#xff1a;mp3&#xff0c;wav , ogg ,flac 的格式之间可以相互转换&#xff0c; 转换后的文件名与原文件相同。 注意&#xff1a;运行代码除了需要安装库pydub之外&#xff0c;还需安装软件ffmpeg.exe&…

Python如何将仅包含音频内容的Mp4,提取并转换为Mp3

关于如何将Mp4转换为Mp3的文章很多&#xff0c;方案也都很有效。但是这其中的大部分方法&#xff0c;并不适用于该Mp4文件中仅包含音频内容的情况&#xff0c;比如&#xff1a;有人从YouTube&#xff0c;下载了仅包含音频内容的文件&#xff0c;这个文件其实还是Mp4格式的&…

PCM和WAV音频格式的区别,以及python自动转换

目录 WAV和PCM的简单介绍PCMWAV 关于音频的基础知识声道数channels采样位数bits采样频率sample_rate 进阶内容互相转换代码 WAV和PCM的简单介绍 PCM pcm&#xff1a;pulse code modulation&#xff0c;脉冲编码调制。将声音等模拟信号变成符号化的脉冲列&#xff0c;予以记录…

音频文件常用格式

目录 一、前言二、音频文件格式1、MP32、WAV3、WMA4、FLAC5、MIDI6、RA7、APE8、AAC9、CDA10、MOV 三、总结 一、前言 在复习备考《多媒体技术》时整理的多媒体技术音频文件的常用格式。本文完全足以应对《多媒体技术》中音频的相关考点后续还有一份多媒体技术相关的文档整理会…

Python处理音频文件的实用姿势

每天叫醒我的不是理想&#xff0c;是楼下广场舞的音乐。 音乐是人类的通用语言&#xff0c;不分国界不分种族。 抖音短视频爆火的关键因素之一&#xff0c;就是普通人也能便捷地使用BGM表达自我。 从感性角度看&#xff0c;音乐可以有很多种解释&#xff0c;如&#xff1a; …

用 Python 将音频内容转换为文本格式

当对一个或多个人的谈话进行记录时&#xff0c;采用一种高度准确和自动化的方式将口语提取为文本非常有用。转换成文字后&#xff0c;便可以将其用于进一步分析或用作其他功能。 在本教程中&#xff0c;我们将使用称为AssemblyAI&#xff08;https://www.assemblyai.com/&#…