一、背景
公司的業(yè)務需要使用換膚功能實現(xiàn)白天/黑夜模式,調研了市場主流換膚框架,主要采用了LayoutInflater.Factory接口干涉Xml中View解析的過程,將創(chuàng)建View的過程由自己來接手。但本項目大量使用自定義View及動態(tài)創(chuàng)建View,Xml中描述界面的情況不多,針對這種情況,我設計了一套輕量級的實時換膚框架。
二、使用
項目UI框架是單Activity+多Fragment的結構,為滿足實時刷新,不出現(xiàn)頁面閃爍的需求,所以從三個方面來實現(xiàn)換膚功能。
1.Activity
當前項目中需要實時刷新的Activity只有首頁和設置頁,所以單獨對這兩個Activity進行處理。首先需要實現(xiàn)Skinable接口,在頁面創(chuàng)建和銷毀時添加監(jiān)聽,這樣,主題發(fā)生改變時,就會通知到applySkin()方法。
class MainActivity : RootActivity(), MainContract.View, Skinable {
...
private fun initView() {
SkinManager.instance.register(this)
}
override fun onDestroy() {
super.onDestroy()
SkinManager.instance.unregister(this)
}
override fun applySkin() {
container.setBackgroundColor(resources.getColor(R.color.bg_color_primary))
navigation.setBackgroundColor(resources.getColor(R.color.bg_color_primary))
for (fragment in supportFragmentManager.fragments) {
if (fragment is RootFragment && fragment.isAdded && !fragment.isDetached) {
fragment.refreshStatusBar()
if (fragment is Skinable) {
fragment.applySkin()
}
}
}
changeStatusBarTheme()
}
...
}
2.Fragment
Fragment是UI界面的承載體,所以在RootFragment中實現(xiàn)了Skinable接口,所有的Fragment需要覆寫applySkin()方法,在里面處理自己的換膚邏輯,即設置頁面控件的顏色屬性(如背景色、字體顏色、圖標等)。
3.View
View是每個頁面最基礎的元素,出于方便使用和易維護的角度,對這層當中的自定義控件和系統(tǒng)控件做了區(qū)別處理。
系統(tǒng)控件
直接在Fragment的applySkin()里調用設置相關屬性的方法。
showQrCode.background = resources.getDrawable(R.drawable.selector_with_ripple)
addEnigma.setTextColor(resources.getColor(R.color.text_color_tips))
自定義控件
對于自定義控件,直接讓控件實現(xiàn)Skinable接口,在自定義控件內部處理控件自己的換膚邏輯,這樣外部不用考慮內部控件的邏輯,只需在 Fragment的applySkin()方法中直接調用該自定義控件的applySkin()方法。
class PasswordEditText: AppCompatEditText, Skinable {
...
override fun applySkin() {
mTextPaint.color = resources.getColor(R.color.text_color_input_hint)
background = resources.getDrawable(R.drawable.selector_login_edit_bkg)
setTextColor(resources.getColor(R.color.text_color_minor))
setHintTextColor(resources.getColor(R.color.text_color_input_hint))
}
...
}
4.資源定義
這里拿黑夜模式進行舉例
添加資源目錄
首先需要在build.gralde文件的android節(jié)點中添加res-nigit。
android {
...
sourceSets {
main {
res.srcDirs = ['src/main/res', 'src/main/res-night']
}
...
}
}
定義資源名
黑夜模式的資源需要在res_night中加入同名的_night后綴,如果未添加,默認會取白天模式的。

colors.xml也需要這樣定義。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary_night">#333333</color>
<color name="colorPrimaryDark_night">#00574B</color>
<color name="colorAccent_night">#26B36D</color>
<!-- text colors -->
<color name="text_color_primary_night">#ffffff</color>
<color name="text_color_minor_night">#cccccc</color>
<color name="text_color_tips_night">#909399</color>
...
<!-- background colors -->
<color name="bg_color_primary_night">#333333</color>>
<color name="bg_color_pressed_night">#2d2d2d</color>
...
<!-- default avatar colors -->
<color name="default_avatar_red_night">#ED4E4E</color>
<color name="default_avatar_blue_night">#6C9DD9</color>
...
<!-- bottom sheet colors -->
<color name="colorSheetText_night">#DE000000</color>
<color name="colorSheetTitle_night">#8A000000</color>
<color name="colorSheetDivider_night">#3f717171</color>
<color name="bg_color_status_bar_night">#333333</color>
</resources>
在這里,推薦大家如果使用圖標,最好用SVG圖,除了占用空間小、縮放無質量損失以外,添加對于黑夜模式的時候,也只需要修改SVG文件色值即可達到。
三、原理
其實核心思想上面也提到了,就是繼承Resource,覆寫了getColor()和getDrawable()方法。
class CustomResources(val resources: Resources) :
Resources(resources.assets, resources.displayMetrics, resources.configuration) {
override fun getColor(id: Int): Int {
return SkinManager.instance.getColor(id)
}
override fun getDrawable(id: Int): Drawable {
return SkinManager.instance.getDrawable(id)
}
fun updateConfig(config: Configuration?, metrics: DisplayMetrics?) {
resources.updateConfiguration(config, metrics)
}
}
然后在SkinManager中通過SkinResources獲取相應主題的資源。
class SkinResources {
...
fun getSkinColor(context: Context, id:Int): Int {
val resources = context.resources
val type = resources.getResourceTypeName(id)
val color = resources.getResourceEntryName(id)
val identifier = getIdentifier(context, nameConvert(color), type)
return when {
identifier != 0 -> resources.getColor(identifier)
else -> resources.getColor(id)
}
}
fun getSkinDrawable(context:Context,id:Int): Drawable {
val resources = context.resources
val type = resources.getResourceTypeName(id)
val drawable = resources.getResourceEntryName(id)
val identifier = getIdentifier(context, nameConvert(drawable), type)
return when {
identifier != 0 -> resources.getDrawable(identifier)
else ->resources.getDrawable(id)
}
}
...
}