Android Kotlin 自定义 CardView 如何将 XML 子视图放入指定容器?
可以。保留现有的内部 XML 布局,在自定义 CardView 中重写 addView,将外部子视图转交给 childrenContainer。同时要处理方法签名和初始化顺序,否则即使解决了重写错误,加载内部布局时仍可能出错。
为什么原来的 addView 无法重写?
addView 的布局参数类型是 ViewGroup.LayoutParams。原代码直接写了 LayoutParams,但没有提供 import 和完整编译错误,因此无法仅凭片段判断它实际解析成了哪个类型。
在 CardView 的继承体系中,要留意它是否解析成了 FrameLayout.LayoutParams。这是 ViewGroup.LayoutParams 的子类,重写方法时不能用更具体的参数类型替代父类方法的参数类型。
显式写出 ViewGroup.LayoutParams 可以消除歧义。这里采用三参数重载,以保留插入位置:
override fun addView(
child: View?,
index: Int,
params: ViewGroup.LayoutParams?
)
对应的方法签名可参见 ViewGroup.addView API 文档。
加载内部布局时,先允许视图添加到 CardView
构造函数中的这段代码也会向当前控件添加子视图:
LayoutInflater.from(context).inflate(
R.layout.element_tarjeta_title,
this,
true
)
由于内部布局使用 <merge>,其中的根 LinearLayout 会直接添加到当前 CardView。此时 childrenContainer 还没有赋值,不能立即将所有视图转交给它。
可以先将 childrenContainer 初始化为 null。加载内部布局时调用 super.addView,找到内容容器后再启用转发。
下面的实现沿用已有的布局、ID 和 R.styleable.TarjetaTitle 定义,可以替换原来的类实现:
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
class TarjetaTitle @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : CardView(context, attrs, defStyleAttr) {
private var childrenContainer: FrameLayout? = null
private val title: TextView
init {
LayoutInflater.from(context).inflate(
R.layout.element_tarjeta_title,
this,
true
)
title = findViewById(R.id.title)
childrenContainer = findViewById(R.id.childrenContainer)
val attributes = context.obtainStyledAttributes(
attrs,
R.styleable.TarjetaTitle,
defStyleAttr,
0
)
try {
title.text = attributes.getString(
R.styleable.TarjetaTitle_title
)
val titleStyle = attributes.getResourceId(
R.styleable.TarjetaTitle_titleStyle,
-1
)
if (titleStyle != -1) {
title.setTextAppearance(titleStyle)
}
} finally {
attributes.recycle()
}
}
override fun addView(
child: View?,
index: Int,
params: ViewGroup.LayoutParams?
) {
val container = childrenContainer
if (container == null) {
// 内部布局尚未加载完成,先建立 CardView 的基础结构。
super.addView(child, index, params)
} else {
// 后续添加的业务内容进入标题下方的容器。
container.addView(child, index, params)
}
}
}
局部变量 container 保存可空属性的当前值,方便 Kotlin 在分支中判断类型。原来的内部 XML 可以继续使用,TextView 和 FrameLayout 的层级也不必改变。
在外部 XML 中声明内容
假设控件的完整类名为 com.example.ui.TarjetaTitle,可以这样使用。实际使用时,请将包名替换为项目中的包名;app:title 依赖已有的自定义属性定义。
<com.example.ui.TarjetaTitle
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="示例标题">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="卡片内容" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="操作" />
</LinearLayout>
</com.example.ui.TarjetaTitle>
外部声明的 LinearLayout 会被转交给 childrenContainer,其中的内容也会显示在标题下方。
使用时需要注意什么?
childrenContainer是FrameLayout,多个直接子视图可能重叠。如需纵向排列,可以像示例一样传入一个纵向LinearLayout,或调整内部内容容器。- 转发后,视图的实际父容器是
childrenContainer。动态删除内容时,应对内容容器调用removeView,或为自定义控件提供专门的内容操作方法。直接调用外层的removeAllViews()会删除卡片的内部结构。 - 示例沿用了原代码中的单参数调用
title.setTextAppearance(titleStyle),要求 API 23 或以上。若项目支持更低版本,需要采用兼容写法。 - 如果显式使用
ViewGroup.LayoutParams后仍出现overrides nothing,应进一步核对完整编译错误,以及CardView、View和ViewGroup的实际 import。
备注:内容仅供参考。
