动态更换theme需求多种多样,解决方案也多种多样。目前我了解的有如下三种:
- 固定一个或者多个主题,仅更换主题色等,可以直接通过setTheme(Style)的方式去做。这种方案实现起来较为简单,侵入性也很小。缺点是灵活性不够,而且setTheme要在onCreateView之前调用,所以需要重启Activity才能生效,比较流行的解决方案是将当前页面截图显示给用户,之后重启Activity后再切换成真正的Activity。
因为style中只能有预设的属性,可以通过下面的方式自定义一些属性
//attrs.xml中自定义参数名
<resources>
<attr name="app_background" format="reference"/>
</resources>
<!-- app theme中赋值 -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
....
<item name="app_background">@drawable/ic_launcher_background</item>
</style>
<!--布局文件中引用-->
<TextView
....
android:background="?attr/app_background"/>
- 比上面更灵活一些,比如可以后台新配置一些简单主题或者仅更换一小部分内容,可以通过自定义控件或者json串的方式自己做一些处理,下面是我写的一个demo的解决方案。
通过如下这种json串的方式去动态更换theme,主要思路是通过LayoutInflater.Factory2接口hook所有view的初始化,通过自定义的一个属性判断是否需要支持动态theme,之后解析json拿到最终需要的资源ID赋值即可。
{
"textColor":{
"colorPrimary":"colorAccent"
},
"buttonDrawable":{
"button_background_default":"button_background_theme_1"
},
“root”:{
"background_light":"background_dark"
}
}
//LayoutInflater.Factory2接口
override fun onCreateView(parent: View?, name: String?, context: Context?, attrs: AttributeSet?): View? {
// if this is NOT enable to be skined , simplly skip it
val isSkinEnable = attrs?.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false) ?: false
if (isSkinEnable) {
val view = createView(context, name, attrs)
val attrsMap = copyAttributeSet(attrs)
SkinManager.applyTheme(name, view, attrsMap)
SkinManager.addDynamicView(name, view, pageName, attrsMap)
return view
}
return null
}
//通过json串拿到替换后的资源ID
private fun handlerDynamicAttr(attrsMap: HashMap<String, String>, attributeName: String, jsonKey: String, resourceTypeName: String): Int {
val originValue: String = attrsMap[attributeName] ?: ""
//only handler reference value now
if (originValue.contains("@")) {
val originReferName = getResourceNameById(getIdByAttributeValue(originValue))
//the value in dynamic json
val dynamicReferName = try {
mDynamicTheme?.getJSONObject(jsonKey)?.getString(originReferName)
} catch (e: JSONException) {
""
}
//getId by reference name
if (!dynamicReferName.isNullOrEmpty()) {
return getIdByName(dynamicReferName, resourceTypeName)
}
}
return getIdByAttributeValue(originValue)
}
- 类似网易云音乐等APP可以从主题商店下载主题,样式多种多样,图片背景等也需要remote加载,这种基本都是通过下载资源APK去实现的。Github有不少优秀的换肤框架可以比较方便的接入。
基本实现原理:构建一个只包含资源的APK,需要替换的资源命名为相同的名字,之后通过反射调用AssetManager的addAssetPath构建新的assetManager,进而得到新的Resource对象。如何实时换肤以及哪些控件需要换肤,则可以参考2中的实现方式。
这种方案灵活性很高,不需要预先知道哪些控件需要支持动态theme,但是需要反射调用系统方法,不知道兼容性上是否会有问题。
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
网友评论