如何通过继承AppCompatButton实现Android自定义XButton
问题描述
需要实现如下样式的自定义按钮:

已准备布局资源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内部,同时处理好布局参数:
- 使用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 } }
- 不使用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() }
- 关键注意点
- 填充布局时
attachToRoot必须设为true:这样布局的根视图会被添加到当前XButton的子视图列表中,否则子视图无法被找到。 - 隐藏原生按钮文本:因为AppCompatButton本身自带文本显示,如果你不需要,可以在构造方法里调用
setText(""),避免和自定义TextView的文本重叠。 - 自定义属性处理:用完TypedArray一定要调用
recycle(),防止内存泄漏。
内容的提问来源于stack exchange,提问作者Tmdhoon2
相关产品推荐
相关产品推荐

