Feature Request: 支持 Synthetic AttributeSet
功能概述
在生成代码中构造 synthetic AttributeSet 对象,将编译时提取的 XML 属性传递给 ViewFactory,以支持完全兼容依赖 AttributeSet 的第三方 inflater 生态。
动机
当前 LayoutX2C 1.3.0 的 ViewFactory 机制传入的 attrs 参数为 null,这导致:
-
第三方 inflater 无法无缝接入
- 换肤框架(需要从 attrs 读取
app:skin_* 属性)
- 自定义 inflater(强依赖
AttributeSet 解析逻辑)
- 需要修改第三方库代码才能兼容,迁移成本高
-
自定义 View 的属性驱动逻辑失效
class PriceView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.PriceView)
textColor = a.getColor(R.styleable.PriceView_priceColor, Color.BLACK)
format = a.getString(R.styleable.PriceView_priceFormat)
a.recycle()
}
}
当 attrs == null 时,所有自定义属性丢失
-
生成代码需要更多补偿逻辑
- 当前需要为每个属性生成 setter 调用
- 有 synthetic AttributeSet 后,View 构造时自动处理,代码更简洁
提议的解决方案
编译时
在 KSP processor 中:
- 提取 XML 标签的所有属性(name、namespace、value、type)
- 生成
SyntheticAttributeSet 实例化代码
运行时
实现 SyntheticAttributeSet 类,满足 AttributeSet 接口:
class SyntheticAttributeSet(
private val attributes: Map<String, AttributeValue>
) : AttributeSet {
data class AttributeValue(
val namespace: String?,
val name: String,
val value: String,
val resourceId: Int = 0
)
override fun getAttributeCount(): Int = attributes.size
override fun getAttributeName(index: Int): String =
attributes.values.elementAt(index).name
override fun getAttributeValue(namespace: String?, name: String): String? =
attributes.values.firstOrNull {
it.namespace == namespace && it.name == name
}?.value
override fun getAttributeResourceValue(namespace: String?, name: String, defaultValue: Int): Int {
val attr = attributes.values.firstOrNull {
it.namespace == namespace && it.name == name
}
return attr?.resourceId ?: defaultValue
}
// ... 实现其他 20+ 个方法
}
生成代码示例
// 编译前的 XML
<com.example.PriceView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/primary"
app:priceColor="@color/red"
app:priceFormat="%.2f" />
// 生成的代码
val attrs = SyntheticAttributeSet(
mapOf(
"layout_width" to AttributeValue("http://schemas.android.com/apk/res/android", "layout_width", "match_parent"),
"layout_height" to AttributeValue("http://schemas.android.com/apk/res/android", "layout_height", "wrap_content"),
"textColor" to AttributeValue("http://schemas.android.com/apk/res/android", "textColor", "@color/primary", R.color.primary),
"priceColor" to AttributeValue("http://schemas.android.com/apk/res-auto", "priceColor", "@color/red", R.color.red),
"priceFormat" to AttributeValue("http://schemas.android.com/apk/res-auto", "priceFormat", "%.2f")
)
)
val view = ViewFactoryCompat.createView(context, "com.example.PriceView", attrs)
?: PriceView(context, attrs)
收益
1. 完全兼容现有 inflater 生态
- 换肤框架、自定义 inflater 无需任何修改
- 零迁移成本
2. 保留属性驱动的初始化逻辑
- 自定义 View 的
init 块正常工作
- 不需要生成额外的 setter 调用
3. 减少生成代码体积
- 属性打包在
AttributeSet 里,而不是逐个 setter
- 对于属性很多的 View,代码更简洁
4. 更接近原生 XML inflate 语义
实现挑战
1. AttributeSet 接口方法众多
需要实现 20+ 个方法,包括:
getAttributeValue() / getAttributeName()
getAttributeResourceValue() / getAttributeIntValue() / getAttributeFloatValue()
getAttributeBooleanValue() / getAttributeUnsignedIntValue()
getStyleAttribute() / getIdAttribute() / getClassAttribute()
2. 资源引用解析
需要正确处理:
- 直接资源引用:
@color/xxx → 解析为 resource ID
- 主题属性引用:
?attr/xxx → 从当前主题解析
- 颜色字面量:
#FF0000 → 解析为 int 值
- 尺寸单位:
16dp / 14sp → 转换为像素
3. 命名空间处理
android: → http://schemas.android.com/apk/res/android
app: → http://schemas.android.com/apk/res-auto
- 自定义包名 →
http://schemas.android.com/apk/res/<package>
4. 性能考虑
- 避免每次创建 View 都构造新的 Map
- 考虑缓存/复用 AttributeSet 实例
- 属性解析的开销
替代方案
方案 A:扩展 ViewFactory 签名
interface ViewFactory {
fun createView(
context: Context,
name: String,
attrs: AttributeSet?,
// 新增:编译时提取的属性 Map
xmlAttrs: Map<String, Any?>?
): View?
}
优点:简单直接,不需要完整实现 AttributeSet
缺点:第三方 inflater 仍需修改代码适配
方案 B:按需提供
添加配置选项,让用户选择是否生成 synthetic AttributeSet:
layoutX2C {
enableSyntheticAttributeSet = true // 默认 false
}
优点:兼顾性能和兼容性,按需开启
缺点:增加配置复杂度
方案 C:仅支持常用方法
先实现最常用的方法(getAttributeValue, getAttributeResourceValue 等),其他方法抛出 UnsupportedOperationException
优点:快速 MVP,覆盖 80% 场景
缺点:边界情况可能崩溃
实现计划
Phase 1: 基础实现
Phase 2: 资源解析
Phase 3: 性能优化
Phase 4: 配置化
相关 Issue
- #xxx - ViewFactory receives null AttributeSet causing NPE
期望效果
实现后,换肤框架等依赖 AttributeSet 的 inflater 可以无缝接入:
// 业务代码无需修改
ViewFactoryRegistry.setViewFactory { context, name, attrs ->
skinInflater.createView(context, name, attrs) // attrs 不再是 null
}
所有自定义属性在 View 构造时就能正确读取,保持与原生 XML inflate 一致的行为。
Feature Request: 支持 Synthetic AttributeSet
功能概述
在生成代码中构造 synthetic
AttributeSet对象,将编译时提取的 XML 属性传递给ViewFactory,以支持完全兼容依赖AttributeSet的第三方 inflater 生态。动机
当前 LayoutX2C 1.3.0 的
ViewFactory机制传入的attrs参数为null,这导致:第三方 inflater 无法无缝接入
app:skin_*属性)AttributeSet解析逻辑)自定义 View 的属性驱动逻辑失效
当
attrs == null时,所有自定义属性丢失生成代码需要更多补偿逻辑
提议的解决方案
编译时
在 KSP processor 中:
SyntheticAttributeSet实例化代码运行时
实现
SyntheticAttributeSet类,满足AttributeSet接口:生成代码示例
收益
1. 完全兼容现有 inflater 生态
2. 保留属性驱动的初始化逻辑
init块正常工作3. 减少生成代码体积
AttributeSet里,而不是逐个 setter4. 更接近原生 XML inflate 语义
实现挑战
1. AttributeSet 接口方法众多
需要实现 20+ 个方法,包括:
getAttributeValue()/getAttributeName()getAttributeResourceValue()/getAttributeIntValue()/getAttributeFloatValue()getAttributeBooleanValue()/getAttributeUnsignedIntValue()getStyleAttribute()/getIdAttribute()/getClassAttribute()2. 资源引用解析
需要正确处理:
@color/xxx→ 解析为 resource ID?attr/xxx→ 从当前主题解析#FF0000→ 解析为 int 值16dp/14sp→ 转换为像素3. 命名空间处理
android:→http://schemas.android.com/apk/res/androidapp:→http://schemas.android.com/apk/res-autohttp://schemas.android.com/apk/res/<package>4. 性能考虑
替代方案
方案 A:扩展 ViewFactory 签名
优点:简单直接,不需要完整实现 AttributeSet
缺点:第三方 inflater 仍需修改代码适配
方案 B:按需提供
添加配置选项,让用户选择是否生成 synthetic AttributeSet:
layoutX2C { enableSyntheticAttributeSet = true // 默认 false }优点:兼顾性能和兼容性,按需开启
缺点:增加配置复杂度
方案 C:仅支持常用方法
先实现最常用的方法(
getAttributeValue,getAttributeResourceValue等),其他方法抛出UnsupportedOperationException优点:快速 MVP,覆盖 80% 场景
缺点:边界情况可能崩溃
实现计划
Phase 1: 基础实现
SyntheticAttributeSet类,支持核心方法Phase 2: 资源解析
@color,@dimen,@string等资源引用解析?attr主题属性解析Phase 3: 性能优化
Phase 4: 配置化
相关 Issue
期望效果
实现后,换肤框架等依赖
AttributeSet的 inflater 可以无缝接入:所有自定义属性在 View 构造时就能正确读取,保持与原生 XML inflate 一致的行为。