一拉馋、背景
公司的業(yè)務(wù)需要使用換膚功能實(shí)現(xiàn)白天/黑夜模式镐牺,調(diào)研了市場(chǎng)主流換膚框架吵护,主要采用了LayoutInflater.Factory
接口干涉Xml中View解析的過(guò)程远剩,將創(chuàng)建View的過(guò)程由自己來(lái)接手列赎。但本項(xiàng)目大量使用自定義View及動(dòng)態(tài)創(chuàng)建View宏悦,Xml中描述界面的情況不多,針對(duì)這種情況粥谬,我設(shè)計(jì)了一套輕量級(jí)的實(shí)時(shí)換膚框架肛根。
二、使用
項(xiàng)目UI框架是單Activity+多Fragment的結(jié)構(gòu)漏策,為滿足實(shí)時(shí)刷新派哲,不出現(xiàn)頁(yè)面閃爍的需求,所以從三個(gè)方面來(lái)實(shí)現(xiàn)換膚功能掺喻。
1.Activity
當(dāng)前項(xiàng)目中需要實(shí)時(shí)刷新的Activity只有首頁(yè)和設(shè)置頁(yè)芭届,所以單獨(dú)對(duì)這兩個(gè)Activity進(jìn)行處理。首先需要實(shí)現(xiàn)Skinable
接口感耙,在頁(yè)面創(chuàng)建和銷毀時(shí)添加監(jiān)聽褂乍,這樣,主題發(fā)生改變時(shí)即硼,就會(huì)通知到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中實(shí)現(xiàn)了Skinable
接口只酥,所有的Fragment需要覆寫applySkin()
方法褥实,在里面處理自己的換膚邏輯呀狼,即設(shè)置頁(yè)面控件的顏色屬性(如背景色、字體顏色损离、圖標(biāo)等)哥艇。
3.View
View是每個(gè)頁(yè)面最基礎(chǔ)的元素,出于方便使用和易維護(hù)的角度僻澎,對(duì)這層當(dāng)中的自定義控件和系統(tǒng)控件做了區(qū)別處理貌踏。
系統(tǒng)控件
直接在Fragment的applySkin()
里調(diào)用設(shè)置相關(guān)屬性的方法。
showQrCode.background = resources.getDrawable(R.drawable.selector_with_ripple)
addEnigma.setTextColor(resources.getColor(R.color.text_color_tips))
自定義控件
對(duì)于自定義控件窟勃,直接讓控件實(shí)現(xiàn)Skinable
接口祖乳,在自定義控件內(nèi)部處理控件自己的換膚邏輯,這樣外部不用考慮內(nèi)部控件的邏輯拳恋,只需在 Fragment的applySkin()
方法中直接調(diào)用該自定義控件的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.資源定義
這里拿黑夜模式進(jìn)行舉例
添加資源目錄
首先需要在build.gralde
文件的android節(jié)點(diǎn)中添加res-nigit
。
android {
...
sourceSets {
main {
res.srcDirs = ['src/main/res', 'src/main/res-night']
}
...
}
}
定義資源名
黑夜模式的資源需要在res_night
中加入同名的_night
后綴谬运,如果未添加隙赁,默認(rèn)會(huì)取白天模式的。
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>
在這里伞访,推薦大家如果使用圖標(biāo),最好用SVG圖轰驳,除了占用空間小厚掷、縮放無(wú)質(zhì)量損失以外,添加對(duì)于黑夜模式的時(shí)候级解,也只需要修改SVG文件色值即可達(dá)到冒黑。
三、原理
其實(shí)核心思想上面也提到了勤哗,就是繼承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中通過(guò)SkinResources獲取相應(yīng)主題的資源芒划。
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)
}
}
...
}