android封装一个Header小组件(kotlin)

170 阅读2分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

我在实战过程中发现,很多重复的组件,于是我就封装一个。

android 中封装组件,需要以下几步:

  1. 定义 props
  2. props 与“组件”联系起来;
  3. 集成 xml 布局;
  4. props 中的值设置到 xml 中,如果需要暴露方法,在“组件”中定义即可。

1. 定义 props

attrs.xml 文件中添加下面的内容:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="HeaderComponent">
        <attr name="title" format="string" />
        <attr name="rightText" format="string" />
        <attr name="leftText" format="string" />
    </declare-styleable>
</resources>

其中 HeaderComponent 相当于 props 的类型名称,这里的名称最好也跟“组件名称相同”;其中 attr 就是每一个属性,对应的 name 就是属性名, format 对应的就是属性的类型。

2. 将 props 与“组件”联系起来

新建一个 HeaderComponent 的类:

class HeaderComponent: ConstraintLayout {
    private var title: String? = null
    private var rightText: String? = null
    private var leftText: String? = null
    var titleView: TextView? = null
    var rightTextView: TextView? = null
    var leftTextView: TextView? = null
    constructor(context: Context): this(context, null)
    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
        init(context, attrs)
    }
    private fun init(context: Context, attrs: AttributeSet?) {
        // 拿到对应的属性名称,记得在最后执行 types.recycle()
        val types = context.obtainStyledAttributes(attrs, R.styleable.HeaderComponent)
        title = types.getString(R.styleable.HeaderComponent_title)
        rightText = types.getString(R.styleable.HeaderComponent_rightText)
        leftText = types.getString(R.styleable.HeaderComponent_leftText)

        // 拿到我们编写的布局文件,这部分就相当于固定的部分
        val view = LayoutInflater.from(getContext()).inflate(R.layout.header, this)
        titleView = view.findViewById(R.id.title)
        rightTextView = view.findViewById(R.id.rightText)
        leftTextView = view.findViewById(R.id.leftText)

        // 根据 xml 中设置的值设置
        titleView?.text = title
        rightTextView?.text = rightText
        leftTextView?.text = leftText
        // 这个必须调用,不然会报错
        types.recycle()
    }

    // 提供方便快捷的方法
    fun setTitle(title: String) {
        titleView?.text = title
    }
}

需要说明的是获取属性名的时候记得带上类型名,如 title 属性,在这里应该是 HeaderComponent_title ,不然找不到:

// 得到 props 的类型名
val types = context.obtainStyledAttributes(attrs, R.styleable.HeaderComponent)
// 拿到传进来的 props 值
title = types.getString(R.styleable.HeaderComponent_title)
rightText = types.getString(R.styleable.HeaderComponent_rightText)
leftText = types.getString(R.styleable.HeaderComponent_leftText)

3. 集成 xml 布局

看上面的代码可以知道 header.xml 文件,就是封装的布局,我定义如下:

<?xml version="1.0" encoding="utf-8"?>
<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="标题"
        android:textColor="@color/black"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/leftText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:text=""
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:visibility="gone" />

    <TextView
        android:id="@+id/rightText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""
        android:layout_marginEnd="16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/button"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_marginStart="16dp"
        android:background="@drawable/ic_baseline_arrow_back_ios_24"
        android:backgroundTint="@color/black"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</merge>

5. 根据 props 设置对应的值

在上面的 kotlin 代码中有:

// 拿到我们编写的布局文件,这部分就相当于固定的部分
val view = LayoutInflater.from(getContext()).inflate(R.layout.header, this)
titleView = view.findViewById(R.id.title)
rightTextView = view.findViewById(R.id.rightText)
leftTextView = view.findViewById(R.id.leftText)

// 根据 xml 中设置的值设置
titleView?.text = title
rightTextView?.text = rightText
leftTextView?.text = leftText

6. 使用封装的组件

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#f2f2f2"
    tools:context=".MainActivity">

    <com.wu.learn.HeaderComponent
        android:layout_width="0dp"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/white"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:title="标题"
        app:rightText="消息"
        tools:ignore="MissingConstraints" />

</androidx.constraintlayout.widget.ConstraintLayout>

效果如下:

1883633-c282d4237e1539a4.webp