需求背景
之前的某一天我们项目经理给我说,客户想要APP有换肤的功能,我们的OA项目要支持换肤功能,于是我就去 ~~Google搜索~~ 学习了一波~
大家应该都知道,我们常用的手机QQ是支持换肤功能的,而且皮肤主题可以在线下载使用!
没错,QQ不是把皮肤主题写死在APP内部的,而是通过插件形式进行换肤功能实现的,而且整个过程不需要重启APP或Activity。
也就是说,我们需要做到以下几点:
- 资源的动态加载(可以加载外部的皮肤包插件)
- 实时换肤(无需重启APP或Activity)
- 换肤后的状态保存(下次进入时还是上一次换肤后的效果:持久化加载皮肤插件的配置)
换肤效果图
什么是皮肤插件包?
皮肤插件包其实就是不包含 Java 代码(当然,如果你不嫌皮肤插件包 apk 的体积变大,你也可以留着)的一个 apk 安装包。
换肤功能介绍
我们APP内换肤针对的一般有以下几种资源:
- View 的背景(background:color、drawable、mipmap)
- 图片资源(src:drawable、mipmap)
- 文字的颜色(textColor:color)
插件换肤原理(动态加载皮肤资源)
关键点(系统源码)
- AssetManager.java
- Resources.java
- AppCompatDelegate.java
- AppCompatDelegateImpl.java
- LayoutInflater.Factory2.java
- AppCompatViewInflater.java
换肤流程
我们加载插件apk皮肤中的资源,其实就是要解析 apk 包,那么我们怎么解析呢?
平时我们获取 app 的资产文件时是通过 AssetManager 这个类进行加载的,我们的换肤插件 apk 也是通过这个类进行加载的。
获取 color、drawable、mipmap 资源则是通过 Resources 类的一系列方法进行获取的。
创建 Android Library Module
- New
- New Module...
- Create New Module
导入依赖
- 在 skin Module 中的 build.gradle 文件内导入必要的依赖(你也可以使用你自己的 Log 日志打印工具类)
dependencies {
// AndroidX 库:https://github.com/androidx/androidx
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
// 日志打印框架(可选):https://github.com/JakeWharton/timber
implementation 'com.jakewharton.timber:timber:4.7.1'
}
新建 Kotlin 类文件
- 新建 activity、attr、callback、factory、manager、util 这几个包
- 新建如下 kotlin 类
SupportSkinActivity.kt:支持换肤的 Activity 基类,使用者可以继承该类以获得换肤的实现
SkinAttr.kt:皮肤属性
SkinAttrSupport.kt:皮肤属性支持
SkinAttrType.kt:皮肤属性类型
SkinView.kt:皮肤 View,包括需要换肤的 View 以及需要换肤的皮肤属性信息
ISkinChangedListener.kt:换肤监听器
ISkinChangingCallback.kt:换肤回调
DefaultSkinConfigFactory.kt:默认皮肤配置工厂
SkinConfigFactory.kt:皮肤配置工厂
SkinFactory.kt:皮肤工厂
SkinManager.kt:皮肤管理器,在 Application 中进行 init 初始化
SkinResourcesManager.kt:皮肤资源管理器
AppCompatActivity.kt:AppCompatActivity 支持换肤的扩展函数
- 项目目录
创建好的项目目录如下图所示
实现源码
SupportSkinActivity.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 支持换肤的 Activity 基类,使用者可以继承该类以获得换肤的实现
*/
open class SupportSkinActivity: AppCompatActivity(), ISkinChangedListener {
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
hookActivity(this)
super.onCreate(savedInstanceState)
}
override fun onSkinChanged() {
}
@CallSuper
override fun onDestroy() {
super.onDestroy()
removeActivityHook(this)
}
}
SkinAttr.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 皮肤属性
*/
class SkinAttr(private val resName: String, private val resType: String, var type: SkinAttrType) {
fun apply(view: View?) {
type.apply(view, resType, resName)
}
}
SkinAttrSupport.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 皮肤属性支持
*/
object SkinAttrSupport {
/**
* 获取皮肤属性
*/
fun getSkinAttrs(resources: Resources, attrs: AttributeSet): List<SkinAttr> {
val skinAttrs: MutableList<SkinAttr> = ArrayList()
var i = 0
val n = attrs.attributeCount
while (i < n) {
// 例:id、layout_width、layout_height、background、color、drawable
val attrName = attrs.getAttributeName(i)
// 例:@2131296320、@2131297009、@2131230904
val attrValue = attrs.getAttributeValue(i)
Timber.d("getSkinAttrs:===> attrName is %s attrValue is %s", attrName, attrValue)
if (attrValue.startsWith("@")) {
var resId = 0
try {
resId = attrValue.substring(1).toInt()
} catch (e: NumberFormatException) {
e.printStackTrace()
} catch (e: IndexOutOfBoundsException) {
e.printStackTrace()
}
// 如果是无效的资源 id 则跳过
if (resId == Resources.ID_NULL) {
i++
continue
}
// 获取资源类型名称
val resType = resources.getResourceTypeName(resId)
// 获取资源的名称
val resName = resources.getResourceEntryName(resId)
Timber.d("getSkinAttrs:===> resType is %s resName is %s", resType, resName)
// 具有换肤的前缀,是支持的换肤资源类型
if (resName.startsWith(SkinConfigFactory.SKIN_PREFIX)) {
val attrType = getSupportAttrType(attrName)
// 如果不是支持换肤的属性类型,则跳过
if (attrType == null) {
i++
continue
}
skinAttrs.add(SkinAttr(resName, resType, attrType))
}
}
i++
}
return skinAttrs
}
/**
* 通过资源属性名称,获取支持换肤的属性类型
*/
private fun getSupportAttrType(attrName: String): SkinAttrType? {
for (attrType in SkinAttrType.values()) {
if (attrType.resType == attrName) {
return attrType
}
}
return null
}
}
SkinAttrType.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 皮肤属性类型
*/
enum class SkinAttrType(val resType: String) {
/**
* background
*/
BACKGROUND("background") {
override fun apply(view: View?, resType: String, resName: String) {
val drawable: Drawable? = getResourceManager().getDrawableByName(resType, resName)
if (view != null && drawable != null) {
view.background = drawable
}
}
},
/**
* src
*/
SRC("src") {
override fun apply(view: View?, resType: String, resName: String) {
val drawable: Drawable? = getResourceManager().getDrawableByName(resType, resName)
if (view is ImageView && drawable != null) {
view.setImageDrawable(drawable)
}
}
},
/**
* textColor
*/
TEXT_COLOR("textColor") {
override fun apply(view: View?, resType: String, resName: String) {
val colorStateList: ColorStateList? =
getResourceManager().getColorByResName(resType, resName)
if (view is TextView && colorStateList != null) {
view.setTextColor(colorStateList)
}
}
};
fun getResourceManager() = SkinManager.instance.resourcesManager
abstract fun apply(view: View?, resType: String, resName: String)
}
SkinView.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 皮肤 View,包括需要换肤的 View 以及需要换肤的皮肤属性信息
*/
class SkinView(private val view: View, private val attrs: List<SkinAttr>) {
fun apply() {
attrs.forEach {
it.apply(view)
}
}
}
ISkinChangedListener.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 换肤监听器
*/
interface ISkinChangedListener {
/**
* 皮肤变了
*/
fun onSkinChanged()
}
ISkinChangingCallback.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 换肤回调
*/
interface ISkinChangingCallback {
/**
* 开始更换皮肤
*/
fun onStartChangeSkin()
/**
* 更换皮肤错误
*/
fun onChangeSkinError(e: Exception)
/**
* 更换皮肤完成
*/
fun onChangeSkinComplete()
/**
* 默认的换肤回调
*/
class DefaultSkinChangingCallbackImpl : ISkinChangingCallback {
override fun onStartChangeSkin() {}
override fun onChangeSkinError(e: Exception) {}
override fun onChangeSkinComplete() {}
}
}
DefaultSkinConfigFactory.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 默认皮肤配置工厂
*/
class DefaultSkinConfigFactory(appContext: Application) : SkinConfigFactory {
private val sp = appContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
override fun savePluginPath(path: String?) {
sp.edit {
putString(KEY_PLUGIN_PATH, path)
}
}
override fun savePluginPkg(pkg: String?) {
sp.edit {
putString(KEY_PLUGIN_PKG, pkg)
}
}
override fun saveSuffix(suffix: String?) {
sp.edit {
putString(KEY_PLUGIN_SUFFIX, suffix)
}
}
override fun getPluginPath(): String = sp.getString(KEY_PLUGIN_PATH, null) ?: ""
override fun getPluginPkg(): String = sp.getString(KEY_PLUGIN_PKG, null) ?: ""
override fun getSuffix(): String = sp.getString(KEY_PLUGIN_SUFFIX, null) ?: ""
override fun clearSkinConfig() {
sp.edit {
clear()
}
}
}
SkinConfigFactory.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 皮肤配置工厂
*/
interface SkinConfigFactory {
/**
* 保存皮肤插件的路径
*/
fun savePluginPath(path: String?)
/**
* 保存皮肤插件的包名
*/
fun savePluginPkg(pkg: String?)
/**
* 保存皮肤的后缀
*/
fun saveSuffix(suffix: String?)
/**
* 获取皮肤插件的路径
*/
fun getPluginPath(): String
/**
* 获取皮肤插件的包名
*/
fun getPluginPkg(): String
/**
* 获取皮肤的后缀
*/
fun getSuffix(): String
/**
* 清空皮肤配置
*/
fun clearSkinConfig()
companion object {
/**
* 插件换肤相关
*/
const val SKIN_PREFIX = "skin_"
const val PREF_NAME = "skin_plugin"
const val KEY_PLUGIN_PATH = "plugin_path"
const val KEY_PLUGIN_PKG = "plugin_pkg"
const val KEY_PLUGIN_SUFFIX = "plugin_suffix"
/**
* 插件包的路径(默认在设备 sdcard 的根目录下,可根据实际情况进行设置)
*/
val SKIN_PLUGIN_PATH = Environment.getExternalStorageDirectory()
.toString() + File.separator + "sunnybeach.skin"
/**
* 插件包的包名
*/
const val SKIN_PLUGIN_PKG = "cn.android52.sunnybeach.skin"
}
}
SkinFactory.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 皮肤工厂
*/
class SkinFactory(
private val delegate: AppCompatDelegate,
private val listener: ISkinChangedListener?
) : LayoutInflater.Factory2 {
private val sConstructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)
private val sClassPrefixList = arrayOf("android.widget.", "android.view.", "android.webkit.")
private val sConstructorMap: MutableMap<String, Constructor<out View?>> = arrayMapOf()
private val mConstructorArgs = arrayOfNulls<Any?>(2)
private var mCreateViewMethod: Method? = null
private val sCreateViewSignature = arrayOf(
View::class.java,
String::class.java,
Context::class.java,
AttributeSet::class.java
)
private val mCreateViewArgs = arrayOfNulls<Any>(4)
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null
}
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {
// 系统有没有使用setFactory
// 完成 AppCompat factory的工作
var view: View? = null
// 请参阅 AppCompatDelegateImpl 类,使用反射调用 createView 方法
try {
if (mCreateViewMethod == null) {
mCreateViewMethod =
delegate.javaClass.getMethod("createView", *sCreateViewSignature)
}
// mCreateViewArgs
mCreateViewArgs[0] = parent
mCreateViewArgs[1] = name
mCreateViewArgs[2] = context
mCreateViewArgs[3] = attrs
view = mCreateViewMethod!!.invoke(delegate, *mCreateViewArgs) as View?
} catch (e: Exception) {
e.printStackTrace()
}
// 收集当前 View 需要换肤的属性集合
val skinAttrs: List<SkinAttr> = SkinAttrSupport.getSkinAttrs(context.resources, attrs)
// 如果当前 View 需要换肤的属性集合为空,则代表该 View 不需要换肤,直接返回该 View
if (skinAttrs.isEmpty()) {
return null
}
if (view == null) {
view = createViewFromTag(context, name, attrs)
}
if (view != null) {
// 当前 View 具有需要换肤的属性,将该 View 和 需要换肤的属性集合保存起来
injectSkin(view, skinAttrs)
}
return view
}
/**
* 请参阅 AppCompatViewInflater 类中的实现
*/
private fun createViewFromTag(context: Context, name: String, attrs: AttributeSet): View? {
var attrName = name
if (attrName == "view") {
attrName = attrs.getAttributeValue(null, "class")
}
return try {
mConstructorArgs[0] = context
mConstructorArgs[1] = attrs
if (-1 == attrName.indexOf('.')) {
for (i in sClassPrefixList.indices) {
val view: View? = createViewByPrefix(context, attrName, sClassPrefixList[i])
if (view != null) {
return view
}
}
null
} else {
createViewByPrefix(context, name, null)
}
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
null
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null
mConstructorArgs[1] = null
}
}
/**
* 请参阅 AppCompatViewInflater 类中的实现
*/
@Throws(ClassNotFoundException::class, InflateException::class)
private fun createViewByPrefix(context: Context, name: String, prefix: String?): View? {
var constructor = sConstructorMap[name]
return try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
val clazz = Class.forName(
if (prefix != null) prefix + name else name,
false,
context.classLoader
).asSubclass(View::class.java)
constructor = clazz.getConstructor(*sConstructorSignature)
sConstructorMap[name] = constructor
}
constructor!!.isAccessible = true
constructor.newInstance(*mConstructorArgs)
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
null
}
}
/**
* 将该 View 和 需要换肤的属性集合保存起来
*/
private fun injectSkin(view: View, skinAttrs: List<SkinAttr>) {
val manager = SkinManager.instance
var skinViews: MutableList<SkinView>? = manager.getSkinView(listener)
if (skinViews == null) {
skinViews = ArrayList<SkinView>()
manager.addSkinView(listener, skinViews)
}
skinViews.add(SkinView(view, skinAttrs))
// 检测当前是否需要自动换肤,如果需要则换肤
if (manager.isNeedChangeSkin()) {
manager.skinChange(listener)
}
}
}
SkinManager.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 皮肤管理器,在 Application 中进行 init 初始化
*/
class SkinManager private constructor() : LifecycleObserver {
private lateinit var mAppContext: Application
private var mSkinResourcesManager: SkinResourcesManager? = null
private val mListeners: MutableList<ISkinChangedListener> = arrayListOf()
private val mSkinViewMaps: MutableMap<ISkinChangedListener?, MutableList<SkinView>> = arrayMapOf()
private lateinit var mSkinConfigFactory: SkinConfigFactory
private var mCurrentPath: String = ""
private var mCurrentPkg: String = ""
// 后缀
private var mSuffix: String = ""
val resourcesManager: SkinResourcesManager
get() = if (!usePlugin()) {
SkinResourcesManager(
mAppContext.resources,
mAppContext.packageName,
mSuffix
)
} else mSkinResourcesManager!!
/**
* 请在 Application 中初始化 SkinManager
* SkinConfigFactory 是可选项,若未指定皮肤配置工厂则使用默认皮肤工厂,该工厂使用 SharedPreferences 存储皮肤插件信息
*/
@JvmOverloads
fun init(
appContext: Application,
factory: SkinConfigFactory = DefaultSkinConfigFactory(appContext)
) {
mAppContext = appContext
mSkinConfigFactory = factory
try {
val pluginPath = mSkinConfigFactory.getPluginPath()
val pluginPkg = mSkinConfigFactory.getPluginPkg()
mSuffix = mSkinConfigFactory.getSuffix()
val file = File(pluginPath)
if (file.exists()) {
loadPlugin(pluginPath, pluginPkg)
}
} catch (e: Exception) {
e.printStackTrace()
mSkinConfigFactory.clearSkinConfig()
}
}
/**
* 是否已经初始化
*/
private fun isInitialized() = ::mAppContext.isInitialized && ::mSkinConfigFactory.isInitialized
/**
* 加载插件
*/
@Throws(Exception::class)
fun loadPlugin(skinPluginPath: String, skinPluginPkg: String) {
if (skinPluginPath == mCurrentPath && skinPluginPkg == mCurrentPkg) {
return
}
val assetManager = AssetManager::class.java.newInstance()
// 获取addAssetPath方法
val addAssetPathMethod =
assetManager.javaClass.getMethod("addAssetPath", String::class.java)
// 调用addAssetPath方法,第一个参数是当前对象,第二个参数是插件包的路径
addAssetPathMethod.invoke(assetManager, skinPluginPath)
val superResources = mAppContext.resources
val displayMetrics = superResources.displayMetrics
val configuration = superResources.configuration
val resources = Resources(assetManager, displayMetrics, configuration)
mSkinResourcesManager = SkinResourcesManager(resources, skinPluginPkg)
mCurrentPath = skinPluginPath
mCurrentPkg = skinPluginPkg
}
/**
* 获取皮肤视图
*/
fun getSkinView(listener: ISkinChangedListener?): MutableList<SkinView>? {
return mSkinViewMaps[listener]
}
/**
* 添加皮肤视图
*/
fun addSkinView(listener: ISkinChangedListener?, views: MutableList<SkinView>) {
mSkinViewMaps[listener] = views
}
/**
* 注册监听器
*/
fun registerListener(listener: ISkinChangedListener) {
mListeners.add(listener)
}
/**
* 取消注册监听器,避免内存泄漏
* 1、移除监听回调
* 2、移除 View 集合
*/
fun unRegisterListener(listener: ISkinChangedListener) {
mListeners.remove(listener)
mSkinViewMaps.remove(listener)
}
/**
* 换皮肤
*/
fun changeSkin(suffix: String): SkinManager {
clearPluginInfo()
mSuffix = suffix
mSkinConfigFactory.saveSuffix(suffix)
notifyChangedListener()
return this
}
/**
* 重置皮肤状态
*/
fun resetSkin() {
clearPluginInfo()
notifyChangedListener()
}
/**
* 清除皮肤插件信息
*/
private fun clearPluginInfo() {
mCurrentPath = ""
mCurrentPkg = ""
mSuffix = ""
mSkinConfigFactory.clearSkinConfig()
updatePluginInfo(mCurrentPath, mCurrentPkg)
}
/**
* 使用默认换肤配置
*/
fun changeSkin(lifecycle: Lifecycle, callback: ISkinChangingCallback?) {
changeSkin(
SkinConfigFactory.SKIN_PLUGIN_PATH,
SkinConfigFactory.SKIN_PLUGIN_PKG,
lifecycle,
callback
)
}
/**
* 改变皮肤
*/
fun changeSkin(
skinPluginPath: String,
skinPluginPkg: String,
lifecycle: Lifecycle,
callback: ISkinChangingCallback?
) {
check(isInitialized()) { "Please initialize in Application!" }
performChangeSkin(this, skinPluginPath, skinPluginPkg, lifecycle, callback)
}
/**
* 执行换肤
*/
private fun performChangeSkin(
skinManager: SkinManager,
pluginPath: String,
pluginPkg: String,
lifecycle: Lifecycle,
callback: ISkinChangingCallback?
) {
lifecycle.coroutineScope.launchWhenResumed {
callback?.onStartChangeSkin()
try {
withContext(Dispatchers.IO) { skinManager.loadPlugin(pluginPath, pluginPkg) }
} catch (e: Exception) {
e.printStackTrace()
callback?.onChangeSkinError(e)
return@launchWhenResumed
}
skinManager.notifyChangedListener()
skinManager.updatePluginInfo(pluginPath, pluginPkg)
callback?.onChangeSkinComplete()
}
}
/**
* 保存插件信息
*/
private fun updatePluginInfo(path: String?, pkg: String?) {
mSkinConfigFactory.savePluginPath(path)
mSkinConfigFactory.savePluginPkg(pkg)
}
/**
* 通知更新
*/
private fun notifyChangedListener() {
mListeners.forEach {
skinChange(it)
it.onSkinChanged()
}
}
/**
* 皮肤变了
*/
fun skinChange(listener: ISkinChangedListener?) {
val skinViews = mSkinViewMaps[listener]
if (skinViews == null) {
Timber.d("skinChange:===> skinViews is null")
//此处必须返回,否则无法换肤成功
return
}
skinViews.forEach {
it.apply()
}
}
/**
* 是否需要换肤
*/
fun isNeedChangeSkin(): Boolean = usePlugin() || useSuffix()
/**
* 使用插件
*/
private fun usePlugin(): Boolean = mCurrentPath.trim().isNotEmpty()
/**
* 使用后缀
*/
private fun useSuffix(): Boolean = mSuffix.trim().isNotEmpty()
companion object {
@JvmStatic
val instance by lazy { SkinManager() }
@JvmStatic
fun hookActivity(activity: AppCompatActivity, listener: ISkinChangedListener?) {
val inflater = LayoutInflater.from(activity)
hookFactorySet(inflater)
val factory = SkinFactory(activity.delegate, listener)
LayoutInflaterCompat.setFactory2(inflater, factory)
}
private fun hookFactorySet(inflater: LayoutInflater) {
// 利用反射去修改 mFactorySet 的值为 false ,防止抛出异常
// IllegalStateException: A factory has already been set on this LayoutInflater
try {
@SuppressLint("SoonBlockedPrivateApi") val mFactorySet =
LayoutInflater::class.java.getDeclaredField("mFactorySet")
mFactorySet.isAccessible = true
mFactorySet[inflater] = false
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
}
SkinResourcesManager.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 皮肤资源管理器
*/
class SkinResourcesManager(
private val resources: Resources,
private val pkgName: String,
private val suffix: String = ""
) {
@SuppressLint("UseCompatLoadingForDrawables")
fun getDrawableByName(resType: String?, name: String): Drawable? {
Timber.d("getDrawableByName:===> pkgName is %s", pkgName)
try {
val fixName = appendSuffix(name)
val resId = resources.getIdentifier(fixName, resType, pkgName)
Timber.d(
"getDrawableByName:===> resType is %s name is %s resId is %s",
resType,
fixName,
resId
)
return resources.getDrawable(resId)
} catch (e: NotFoundException) {
e.printStackTrace()
}
return null
}
@SuppressLint("UseCompatLoadingForColorStateLists")
fun getColorByResName(resType: String?, name: String): ColorStateList? {
Timber.d("getColorByResName:===> pkgName is %s", pkgName)
try {
val fixName = appendSuffix(name)
val resId = resources.getIdentifier(fixName, resType, pkgName)
Timber.d(
"getColorByResName:===> resType is %s name is %s resId is %s",
resType,
fixName,
resId
)
return resources.getColorStateList(resId)
} catch (e: NotFoundException) {
e.printStackTrace()
}
return null
}
/**
* 追加后缀
*/
private fun appendSuffix(name: String): String {
var fixName = name
if (!TextUtils.isEmpty(suffix)) {
fixName += "_$suffix"
}
return fixName
}
}
AppCompatActivity.kt
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : AppCompatActivity 支持换肤的扩展函数
*/
/**
* 劫持 AppCompatActivity ,使其拥有换肤功能,必须在 AppCompatActivity 的 onCreate 方法之前调用
*/
fun AppCompatActivity.hookActivity(action: ISkinChangedListener) {
val manager = SkinManager.instance
manager.registerListener(action)
SkinManager.hookActivity(this, action)
}
/**
* 取消 AppCompatActivity 的监听,避免内存泄漏,请在 AppCompatActivity 的 onDestroy 方法中调用
*/
fun AppCompatActivity.removeActivityHook(action: ISkinChangedListener) {
val manager = SkinManager.instance
manager.unRegisterListener(action)
}
引入方式
将 skin 这个 Module 以 Library 的形式引入 app 的 Module 中,例:
dependencies {
implementation project(':skin')
}
使用前初始化
- Application
在 Application 中调用 SkinManager.instance.init(this)
初始化皮肤管理器。
class App: Application() {
override fun onCreate() {
super.onCreate()
SkinManager.instance.init(this)
}
}
别忘了在 app module 的 AndroidManifest.xml 文件中注册该 Application 哦~
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cn.android52.sunnybeach.skin">
<application
android:name=".App">
</application>
</manifest>
- Activity
1、最简单的使用方式就是直接继承自 SupportSkinActivity
,让其作为基类,其它代码的按照正常业务进行编写即可,例:
class TestActivity: SupportSkinActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test_activity)
// 获取 SkinManager 实例
val manager = SkinManager.instance
// 执行
manager.changeSkin(lifecycle, this)
}
}
2、在编写的基类中添加如下代码即可实现换肤功能,例:
class SupportSkinActivity: AppCompatActivity(), ISkinChangedListener {
override fun onCreate(savedInstanceState: Bundle?) {
// 该函数的调用必须在 super.onCreate(savedInstanceState) 之前
hookActivity(this)
super.onCreate(savedInstanceState)
}
override fun onSkinChanged() {
// 皮肤变了
}
override fun onStartChangeSkin() {
// 开始更换皮肤
}
override fun onChangeSkinError(e: Exception) {
// 更换皮肤错误
}
override fun onChangeSkinComplete() {
// 更换皮肤完成
}
override fun onDestroy() {
super.onDestroy()
// 移除 Activity 的 Hook ,避免内存泄漏
removeActivityHook(this)
}
}
- layout 布局文件
在布局文件中只需要在需要换肤的资源名称前添加 skin_
前缀即可,插件包中的资源名称与当前需要换肤的 app 中使用的资源名称保持一致。
皮肤插件包(新建一个普通的 app module)
Tips:去除新建 module 时默认添加的依赖可以减小皮肤插件包的体积。
将需要换肤的资源 copy 到该 module 中,然后直接签名打包成 apk 文件即可。为了避免用户误安装,可以将该 apk 的后缀进行修改,例:sunnybeach.skin
欢迎同学们点赞、评论、打赏+关注啦~
首发于阳光沙滩,允许转载至《阳光沙滩》微信公众号,其他转载请联系:2695734816@qq.com