安卓APP应用内实现插件式换肤

需求背景

之前的某一天我们项目经理给我说,客户想要APP有换肤的功能,我们的OA项目要支持换肤功能,于是我就去 Google搜索 学习了一波~

大家应该都知道,我们常用的手机QQ是支持换肤功能的,而且皮肤主题可以在线下载使用!

没错,QQ不是把皮肤主题写死在APP内部的,而是通过插件形式进行换肤功能实现的,而且整个过程不需要重启APP或Activity。

也就是说,我们需要做到以下几点:

  • 资源的动态加载(可以加载外部的皮肤包插件)
  • 实时换肤(无需重启APP或Activity)
  • 换肤后的状态保存(下次进入时还是上一次换肤后的效果:持久化加载皮肤插件的配置)

换肤效果图

app插件换肤演示

什么是皮肤插件包?

皮肤插件包其实就是不包含 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 类的一系列方法进行获取的。

image-20211119215208595

创建 Android Library Module

  • New
image-20211119175741354
  • New Module…
image-20211119175850952
  • Create New Module
image-20211119180158825

导入依赖

  • 在 skin Module 中的 build.gradle 文件内导入必要的依赖(你也可以使用你自己的 Log 日志打印工具类)
1
2
3
4
5
6
7
8
9
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 支持换肤的扩展函数

  • 项目目录

创建好的项目目录如下图所示

image-20211119172939599

实现源码

SupportSkinActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2021/11/19
* desc : 换肤监听器
*/
interface ISkinChangedListener {

/**
* 皮肤变了
*/
fun onSkinChanged()
}

ISkinChangingCallback.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 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 中,例:

1
2
3
dependencies {
implementation project(':skin')
}

使用前初始化

  • Application

在 Application 中调用 SkinManager.instance.init(this) 初始化皮肤管理器。

1
2
3
4
5
6
7
class App: Application() {

override fun onCreate() {
super.onCreate()
SkinManager.instance.init(this)
}
}

别忘了在 app module 的 AndroidManifest.xml 文件中注册该 Application 哦~

1
2
3
4
5
6
7
8
9
10
<?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 ,让其作为基类,其它代码的按照正常业务进行编写即可,例:

1
2
3
4
5
6
7
8
9
10
11
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、在编写的基类中添加如下代码即可实现换肤功能,例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

欢迎同学们点赞、评论、打赏+关注啦~

首发于阳光沙滩,允许转载至《阳光沙滩》微信公众号,其他转载请联系:[email protected]


安卓APP应用内实现插件式换肤
https://blog.funkt.cn/2021/11/19/1461734891350679553/
作者
anjiemo
发布于
2021年11月20日
许可协议
未经作者授权,禁止转载