Android 手写实现插件化换肤 兼容Android10 Android11
Posted 安卓开发-顺
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 手写实现插件化换肤 兼容Android10 Android11相关的知识,希望对你有一定的参考价值。
目录
二、统一为所有Activity设置工厂(兼容Android9以上)
实现插件化换肤,有以下几个关键问题要处理
- 收集所有需要换肤的view及相关属性
- 统一处理所有Activity的换肤工作(每一个Activity都要进行换肤处理)
- 加载皮肤包资源
- 处理支持库或者自定义view的换肤
- 处理状态栏换肤
- 对代码动态设置颜色、背景的业务场景进行单独处理
接下来,我们将完成以上6步的内容,继续阅读前,建议先阅读我的另外两篇文章作为基础知识:
Activity setContentView背后的一系列源码分析
先看下效果
换肤前:
换肤后:
所有源码会上传到github,建议下载源码,对照源码来阅读本篇文章,下载地址:
(稍后上传)
一、收集所有需要换肤的view及相关属性
通过系统预留的用于创建view的Factory来接管view的创建,接管的目的:
1、创建出view后对其进行相关颜色、背景的属性的设置,从而完成换肤操
2、把需要换肤的view缓存起来 用户动态的触发换肤时,直接对这些view进行换肤操作。
说明:所有的view都会通过LayoutInflater类来完成创建,而在创建之前系统会先判断是否有预设的
工厂,如果有则会先通过工厂来创建,详见其tryCreateView方法(代码是android-31 Android12)
因此我们写一个工厂SkinLayoutInflaterFactory来实现Factory2,实现其onCreateView方法,在这个方法里接管view的创建工作
/**
* view 生产工厂类,用于代替系统进行view生产 生产过程参考系统源码即可
* 1、在生成过程中对需要换肤的view进行缓存
* 2、如果已经设置了相关皮肤,生成完view后立刻对其进行相应皮肤的设置
*/
class SkinLayoutInflaterFactory : LayoutInflater.Factory2, Observer
var activity : Activity ? = null
var skinCache : SkinCache ? = null
//记录View的构造函数结构,通过两个参数的构造函数去反射view 这里参考系统源码
private val mConstructorSignature = arrayOf(
Context::class.java, AttributeSet::class.java
)
//缓存view的构造函数 参考系统源码
private val sConstructorMap = HashMap<String, Constructor<out View>>()
private val mClassPrefixList = arrayOf(
"android.widget.",
"android.webkit.",
"android.app.",
"android.view."
)
constructor(activity: Activity)
this.activity = activity
skinCache = SkinCache()
/**
* 对于Factory2 系统会回调4个参数的方法 在源码中可以看到
* 详见 LayoutInflater --- tryCreateView 方法
*/
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View?
//先尝试按照系统的view("android.widget.xxx", "android.webkit.xxx", "android.app.xxx", "android.view.xxx")去创建
var view: View? = tryCreateView(name, context, attrs)
if (null == view)
//如果不是这几个包下的view 直接通过反射创建
view = createView(name, context, attrs)
if (null != view)
LogUtil.i("success create view $name ")
//1.如果是可以换肤的view则缓存相关信息
val skinView = skinCache?.checkAndCache(view, attrs)
//2.判断是否有设置的皮肤,有则对支持换肤的view进行换肤
if (!ResourcesManager.isDefaultSkin)
skinView?.applySkin()
else
LogUtil.i("view is null $attrs.getAttributeName(0)")
return view
private fun tryCreateView(name: String, context: Context, attrs: AttributeSet): View?
//如果包含 . 可能是自定义view,或者谷歌出的一些支持库或者Material Design里面的view等
//总之 就是 xxx.xxx.xxx这种格式的view
if (-1 != name.indexOf('.'))
return null
//不包含就要在解析的 节点 name前,拼上: android.widget. 等尝试去反射
for (i in mClassPrefixList.indices)
val view = createView(mClassPrefixList[i].toString() + name, context, attrs)
if (view != null)
return view
return null
/**
* 通过反射创建view 参考系统源码
*/
private fun createView(name: String, context: Context, attrs: AttributeSet): View?
val constructor: Constructor<out View>? = findConstructor(context, name)
try
return constructor?.newInstance(context, attrs)
catch (e: Exception)
return null
private fun findConstructor(context: Context, name: String): Constructor<out View>?
var constructor: Constructor<out View>? = sConstructorMap[name]
if (constructor == null)
try
val clazz = context.classLoader.loadClass(name).asSubclass(View::class.java)
constructor =
clazz.getConstructor(mConstructorSignature[0], mConstructorSignature[1])
sConstructorMap[name] = constructor
catch (e: Exception)
return constructor
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View?
return null
/**
* 观察者模式 有通知来就执行 用户手动点击换肤 会通过
* SkinManager.notifyObservers通知过来
*/
override fun update(o: Observable?, arg: Any?)
SkinThemeUtils.updateStatusBarColor(activity!!)
skinCache?.applySkin()
//MainActivity 里面动态设置底部tab样式需要单独处理
if (activity is MainActivity)
(activity as MainActivity).applySkin()
/**
* 释放当前缓存的资源
*/
fun destroy()
sConstructorMap.clear()
skinCache?.clear()
这里实现了observer,这样每一个工厂都做为一个观察者,用户点击换肤时只需要通知一下这些观察者让他们执行换肤任务即可。
二、统一为所有Activity设置工厂(兼容Android9以上)
因为每一个Activity都需要一个Factory来接管view的创建工作,因此我们可以利用系统的ActivityLifecycleCallbacks机制来对Activity统一添加Factory。
说明:在ActivityLifecycleCallbacks的onActivityCreated中执行Factory的设置工作,刚好能赶在Activity的setContentView方法前面, 因为setContentView方法最终会调到LayoutInflater里面去创建view,所以我们这样操作不耽误当前页面的换肤工作。
我们写一个SkinActLifeCallback继承自Application.ActivityLifecycleCallbacks
class SkinActLifeCallback : Application.ActivityLifecycleCallbacks
private val mLayoutInflaterFactories: ArrayMap<Activity, SkinLayoutInflaterFactory> = ArrayMap()
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?)
//在此处对Activity进行view生产工厂的设置,将会在setContentView之前执行此处代码
//原理是:Activity的源码中在onCreate方法中会执行dispatchActivityCreated方法
//然后就会回调到这里来,这一步在我们写的Activity的onCreate方法中的super.onCreate(savedInstanceState)里来执行的
//而setContentView是在super.onCreate(savedInstanceState)之后调用
//因为系统只允许设置一次factory 所以这里通过反射修改对应的系统变量 实现多次设置 但是 9.0以上不允许在反射修改mFactorySet 的值
//因此我们这里直接通过反射给进行赋值 下面这个方法就是源码设置Factory2的地方 我们反射实现mFactorySet = true后面部分的代码
// fun setFactory2(factory: Factory2?)
// check(!mFactorySet) "A factory has already been set on this LayoutInflater"
// if (factory == null)
// throw NullPointerException("Given factory can not be null")
//
// mFactorySet = true
// if (mFactory == null)
// mFactory2 = factory
// mFactory = mFactory2
// else
// mFactory2 = LayoutInflater.FactoryMerger(factory, factory, mFactory, mFactory2)
// mFactory = mFactory2
//
//
val factory2 = SkinLayoutInflaterFactory(activity)
LogUtil.i("cur sdk version is $Build.VERSION.SDK_INT")
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P)
//因为系统只允许设置一次factory 所以这里通过反射修改对应的系统变量 实现多次设置
try
val javaClass = LayoutInflater::class.java
val declaredField = javaClass.getDeclaredField("mFactorySet")
declaredField.isAccessible = true
declaredField.set(activity.layoutInflater, false)
catch (e: java.lang.Exception)
e.printStackTrace()
//此种方式不兼容5.0以下
// activity.layoutInflater.factory2 = null
//这种设置方式更好,系统对5.0以下做了兼容处理
LayoutInflaterCompat.setFactory2(activity.layoutInflater, factory2)
else
//兼容Android9.0以上
val layoutInflater = activity.layoutInflater
val javaClass = LayoutInflater::class.java
try
val mFactory2 = javaClass.getDeclaredField("mFactory2")
val mFactory = javaClass.getDeclaredField("mFactory")
mFactory2.isAccessible = true
mFactory.isAccessible = true
val mFactoryValue = mFactory.get(layoutInflater)
val mFactory2Value = mFactory2.get(layoutInflater)
if (mFactoryValue == null)
mFactory2.set(layoutInflater, factory2)
mFactory.set(layoutInflater, factory2)
else
val clazz = Class.forName("android.view.LayoutInflater\\$FactoryMerger")
val size2 = clazz.declaredConstructors.size
LogUtil.i("size2=$size2")
val constructor = clazz.getConstructor(
LayoutInflater.Factory::class.java,
Factory2::class.java,
LayoutInflater.Factory::class.java,
Factory2::class.java
)
constructor.isAccessible = true
val newInstance = constructor.newInstance(
factory2,
factory2,
mFactoryValue as LayoutInflater.Factory,
mFactory2Value as Factory2
)
mFactory2.set(layoutInflater, newInstance)
mFactory.set(layoutInflater, newInstance)
catch (e: Exception)
try
val mFactory2 = javaClass.getDeclaredField("mFactory2")
val mFactory = javaClass.getDeclaredField("mFactory")
mFactory2.isAccessible = true
mFactory.isAccessible = true
mFactory2.set(layoutInflater, factory2)
mFactory.set(layoutInflater, factory2)
catch (e: Exception)
e.printStackTrace()
//设置完工厂 还要将工厂作为观察者缓存起来,动态调用换肤功能时,可以通知所有工厂进行换肤工作
//这里利用系统自带的观察者类Observable来实现 SkinManager 继承自Observable
//SkinLayoutInflaterFactory 实现 Observal接口
mLayoutInflaterFactories[activity] = factory2
SkinManager.addObserver(factory2)
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 factory2: SkinLayoutInflaterFactory? = mLayoutInflaterFactories.remove(activity)
//释放资源
factory2?.destroy()
SkinManager.deleteObserver(factory2)
(1)这里的关键是通过反射来把第一步创建的Factory2设置到LayoutInflater里面去,9.0以上不允许在反射修改mFactorySet 的值 ,因此我们这里针对9.0以上直接通过反射把我们的工厂赋值给Factory2,和Factory
(2)同时将每个Activity的工厂作为观察者保存起来
暂时写到这里,明天继续写 2022 01 27 22:00
三、加载皮肤包资源
四、处理支持库或者自定义view的换肤
五、处理状态栏换肤
六、对代码动态设置颜色、背景的业务场景进行单独处理
以上是关于Android 手写实现插件化换肤 兼容Android10 Android11的主要内容,如果未能解决你的问题,请参考以下文章