PAYATHON 2026

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。

备注:内容仅供参考。