- 如何访问 XML 文件内的 View?
- 如何解析 XML 文件每个 View 定义的属性?
- 解析出来的属性有什么使用场景?
带着上述问题我们逐一分析。
1、如何访问 View?
我们通过 LayoutInflater 加载布局文件,并获取到布局文件对应的 View;因此切入点可以考虑从 LayoutInflater 入手,在 LayoutInflater 内通过 Factory 创建 View,所以我们可以代理 LayoutInflater 内的 Factory,这样就可以访问到 XML 文件定义的所有 View;
// 自定义 Factory 类
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
// 通过 activity 代理方式创建 View
var view = activity?.delegate?.createView(parent, name, context, attrs)
if (null == view) {
Log.d(TAG, "onCreateView name: $name, count: ${++viewCount}")
if (!name.contains(".")) {
for (prefix in sClassPrefixList) {
try {
// 通过 inflater 方式创建 View
view = inflater.createView(context, name, prefix, attrs)
if (null != view) break
} catch (e: Exception) {
Log.d(TAG, "createView e: $e")
}
}
} else {
// 通过 inflater 方式创建 View
view = inflater.createView(context, name, null, attrs)
}
}
if (null != view) {
// 解析View的属性
parseViewAttrs(view, attrs)
}
return view
}
将定义的代理 Factory 类,设置到 LayoutInflater 内,代理 Factory 类不需要关注如何创建View,我们只需要关注访问创建后的 View。
如何将代理 Factory 设置到 LayoutInflater,可以在 Activity.onCreate 父类方法调用之前设置;
fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 = XXXFactory(layoutInflater, this)
super.onCreate(savedInstanceState)
}
2、如何解析View内的属性?
通过代码来分析属性内含义的字段信息
// parseViewAttrs attrName: foreground, attrValue: ?16842641, nameSpace: http://schemas.android.com/apk/res/android
// parseViewAttrs 9, attrName: style, attrValue: @2131820816, nameSpace:
fun parseViewAttrs(view: View, attrs: AttributeSet) {
// 通过解析 View 内定义的所有属性
for (index in 0 until attrs.attrbuteCounte) {
// 属性名 foreground/style etc
val attrName = attrs.getAttributeName(index)
// 属性值,R.java 文件内定义的资源ID ?16842641/@2131820816
val attrValue = attrs.getAttrbuteValue(index)
val nameSpace = attrs.getAttributeNamespace(index)
// 解析 View style 属性内定义的特定 item,intArrayOf 内定义
if (attrName == "style") {
parseViewStyle(view, attrs, intArrayOf(android.R.atrt.foreground), attrValue.substring(1).toInt)
}
// 解析 View 普通属性,通过 filter 方法过滤解析特定 attr 属性
if (filter(attrName)) {
parseViewAttr(view, attrName, attrValue)
}
}
}
// 过滤方法
private fun filter(attrName: String): Boolean {
return attrName == "textColor" || attrName == "src" || attrName == "background"
}
解析 View style 属性资源
// parseViewStyle [ index: 1, count: 2, value: res/color/hint_foreground_material_light.xml, resId: 17170805, entryName: hint_foreground_material_light, typeName: color, attrItemName: textColorHint ]
parseViewStyle(view: View, attrs: AttributeSet, attrArray: IntArray, styleId: Int) {
view.context.obtainStyledAttributes(styledId, attrArray).use {
// 逐条解析 style 内每个属性 item 内容
for (i in 0 until it.indexCount) {
// 资源文件地址
val value = it.getString(i)
// R.java 文件内定义的资源ID
val resId = it.getResourceId(i, -1)
if (resId < 0 || value.isNullOrEmpty) continue
// 资源名称 hint_foreground_material_light
val entryName = view.resource.getResourceEntryName(resId)
// 资源类型 color
val typeName = view.resource.getResourceTypeName(resId)
// 属性名 textColorHint
val itemAttrName = view.resource.getResourceEntryName(attrArray[i])
}
}
}
解析出来的 itemAttrName -> resId,可以用于动态加载该属性的资源,当在不同的资源包内包含该资源时,可以实现主题的切换;
解析 View 普通属性资源
// parseViewAttr theme [ themeId: 16842841, attrName: foreground, attrValue: ?16842841, < entryName: windowContentOverlay, typeName: attr, resolve: true, typed: { resourceId: 0, sourceResourceId: 0, type: 1, data: 0 } > ]
// parseViewAttr resource [ attrName: textColor, attrValue: @2131099911, resId: 2131099911, < entryName: textColorNormalAlpha, typeName: color > ]
fun parseViewAttr(view: View, attrName: String, attrValue: String) {
if(attrValue.startWith("?")) {
// parse theme
val themeId = attrValue.substring(1).toInt()
if(0 != themeId) {
// 定义的资源名称,windowContentOverlay
val entryName = view.resource.getResourceEntryName(themeId)
// attr/color etc
val typeName = view.resource.getResourceTypeName(themeId)
// 解析资源值引用
val typedValue = TypedValue()
view.context.theme.resolveAttribute(themeId, typedValue)
// R.java 文件内定义的资源ID
val resId = typedValue.getResourceId()
}
} else if(attrValue.startWith("@")) {
// parse resource
val resId = attrValue.substring(1).toInt()
if (0 != resId) {
// 定义的资源名称,textColorNormalAlpha
val entryName = view.resource.getResourceEntryName(resId)
// color/boolean etc
val typeName = view.resource.getResourceTypeName(resId)
}
}
}
解析出来的 attrName -> resId,可以用于动态加载该属性的资源,当在不同的资源包内包含该资源时,可以实现主题的切换;
3、View 属性的使用场景-主题切换
将需要换肤的属性记录到每个 View 内,当换肤事件发生时,遍历需要换肤的View,并设置需要更新的属性,从而触发加载新的主题包内的资源;
换肤框架可以分为3个部分,注册换肤属性拦截器、解析与记录 View 换肤属性的工厂、监听换肤事件、记录需要换肤的根View;
通过上述流程,实现了整个应用的换肤操作;
整体思路非常简单,简单梳理如下:
首先注册需要换肤的属性,主要涉及 color/drawable 等资源的属性; 然后通过View工厂,解析每个View的换肤属性,并记录该属性资源ID; 最后监听主题切换事件,从根View开始遍历所有需要换肤的View,并重新加载相关属性的资源即可;