You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过继承AppCompatButton实现Android自定义XButton

问题描述

需要实现如下样式的自定义按钮:
按钮样式1
按钮样式2

已准备布局资源button.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="@dimen/button_horizontal"
        android:layout_marginVertical="@dimen/button_vertical"
        android:divider="@drawable/divider"
        android:orientation="horizontal"
        android:showDividers="middle"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:id="@+id/tv_button"
            style="@style/BodyLarge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

以及自定义按钮代码XButton.kt:

class XButton : AppCompatButton {
    constructor(context: Context) : super(context) {
        init(
            context = context,
            attrs = null,
        )
    }

    constructor(
        context: Context,
        attrs: AttributeSet?,
    ) : super(
        context,
        attrs,
    ) {
        init(
            context = context,
            attrs = attrs,
        )
    }

    constructor(
        context: Context,
        attrs: AttributeSet?,
        defStyleAttr: Int,
    ) : super(
        context,
        attrs,
        defStyleAttr,
    ) {
        init(
            context = context,
            attrs = attrs,
        )
    }

    private lateinit var attributes: TypedArray

    private fun init(
        context: Context,
        attrs: AttributeSet?,
    ) {
        setAttributes(
            context = context,
            attrs = attrs,
        )
    }

    private fun setAttributes(
        context: Context,
        attrs: AttributeSet?,
    ) {
        attributes = context.obtainStyledAttributes(
            attrs,
            R.styleable.XButton,
        )
    }
}

尝试将button.xml填充到XButton.kt中失败,之前继承LinearLayout实现自定义按钮时能正常使用布局中的视图,但现在必须继承AppCompatButton,使用findViewById返回空对象,View Binding也无法正常工作,请问如何将button.xml填充到XButton.kt中,实现继承AppCompatButton的自定义按钮?


解决方案

AppCompatButton本质是TextView的子类,属于单一视图控件,直接填充布局会因为视图层级问题找不到子视图,正确做法是通过LayoutInflater把布局填充到当前XButton内部,同时处理好布局参数:

  1. 使用View Binding实现填充(推荐)
    修改XButton.kt的init方法,加入布局填充逻辑:
class XButton : AppCompatButton {
    // 保留原有构造方法
    constructor(context: Context) : super(context) {
        init(context, null)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(context, attrs)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        init(context, attrs)
    }

    private lateinit var attributes: TypedArray
    private lateinit var binding: ButtonBinding // View Binding自动生成的类,对应button.xml

    private fun init(context: Context, attrs: AttributeSet?) {
        setAttributes(context, attrs)
        // 把布局填充到当前按钮,attachToRoot必须设为true,否则子视图不会关联到当前控件
        binding = ButtonBinding.inflate(LayoutInflater.from(context), this, true)
        
        // 读取自定义属性并设置文本
        attributes.getString(R.styleable.XButton_buttonText)?.let {
            binding.tvButton.text = it
        }
        attributes.recycle() // 记得回收TypedArray,避免内存泄漏
    }

    private fun setAttributes(context: Context, attrs: AttributeSet?) {
        attributes = context.obtainStyledAttributes(attrs, R.styleable.XButton)
    }

    // 对外提供设置文本的方法
    fun setButtonText(text: String) {
        binding.tvButton.text = text
    }
}
  1. 不使用View Binding的手动填充方式
    如果没开启View Binding,也可以手动用LayoutInflater加载布局:
private fun init(context: Context, attrs: AttributeSet?) {
    setAttributes(context, attrs)
    // 填充布局到当前按钮,attachToRoot设为true
    val rootView = LayoutInflater.from(context).inflate(R.layout.button, this, true)
    val tvButton = rootView.findViewById<TextView>(R.id.tv_button)
    
    // 处理自定义属性
    val buttonText = attributes.getString(R.styleable.XButton_buttonText)
    tvButton.text = buttonText ?: ""
    attributes.recycle()
}
  1. 关键注意点
  • 填充布局时attachToRoot必须设为true:这样布局的根视图会被添加到当前XButton的子视图列表中,否则子视图无法被找到。
  • 隐藏原生按钮文本:因为AppCompatButton本身自带文本显示,如果你不需要,可以在构造方法里调用setText(""),避免和自定义TextView的文本重叠。
  • 自定义属性处理:用完TypedArray一定要调用recycle(),防止内存泄漏。

内容的提问来源于stack exchange,提问作者Tmdhoon2

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 12:04:55