我在实战过程中发现,很多重复的组件,于是我就想封装一个,咨询了我们的同事,结果果然可以按照我想的那样实现,非常棒。
在 android
中封装组件,需要以下几步:
- 定义
props
; - 将
props
与“组件”联系起来; - 集成
xml
布局; - 将
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
就是属性名。
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
}
}
需要说明的是获取属性名的时候记得带上类型名,不然找不到:
// 得到 props 的类型名
val types = context.obtainStyledAttributes(attrs, R.styleable.HeaderComponent)
// 拿到传进来的 props 值,属性名的规则是:类型名_xml文件中的attr标签中的name值
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>
效果如下:
更新日志(2021-05-27)
为了减少布局嵌套,封装的组件我采用了 <merge>
;把原先的 LinearLayout
换成了 ConstraintLayout
,现在我全力拥抱约束布局。