写在前面
最近在写一个项目,里面有一个常见的需求就是:多项选中 RecyclerView 的 item 进行批量处理(例如批量删除)。虽然可以自行实现,但感觉这么常见的功能或许有什么比较常用的库可以方便使用,四处查查,最后被我发现了这个库 RecyclerView-Selection !官方是这么说明的:
借助
recyclerview-selection
库,用户可以通过触摸或鼠标输入来选择 RecyclerView列表中的项。您仍然可以控制所选项的视觉呈现效果。您也仍然可以控制用于约束选择行为的政策,例如符合入选条件的项以及可以选择的项数。
翻了下相关文档,实现起来相对于自己实现还是方便简单多,但相关教程比较少和零散,所以这篇文章就此诞生。
参考文章
官方文档:
自定义 RecyclerView | Android 开发者 | Android Developers (google.cn)
其他优秀文章:
recyclerview-selection简化RecyclerView复选及状态持久化 - 掘金 (juejin.cn)
Android-RecyclerView 的 SelectionTracker 选择器使用 | JiaoPeng`s Blogs (hijiaopeng.github.io)
A guide to recyclerview-selection | by Marcos Holgado | ProAndroidDev
准备
布局
这是一个简单的通讯录demo,只有一个RecyclerView用于展示数据
xml
<androidx.constraintlayout.widget.ConstraintLayout
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="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_telephone"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
RV内的item布局,两个TextView分别用于展示联系人姓名和联系人电话
xml
<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="wrap_content">
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_telephone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Name" />
<TextView
android:id="@+id/tv_telephone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
android:textSize="16sp"
android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name"
tools:text="Telephone" />
</androidx.constraintlayout.widget.ConstraintLayout>
数据类
创建Person
实体类用于存储展示数据
kotlin
data class Person(
val id: Long, // 唯一id
val name: String, // 通讯录姓名
val telephone: String, // 通讯录电话
)
数据的来源不是本文的重点,所以事先写死几个数据用于展示测试用
kotlin
val list: List<Person> = ArrayList(
listOf(
Person(1L, "Mike", "86100100"),
Person(2L, "Jane", "86100101"),
Person(3L, "John", "86100102"),
Person(4L, "Amy", "86100103"),
)
)
adapter
创建 recyclerview 的 adapter
kotlin
class TestAdapter(private val list:List<Person>)
: RecyclerView.Adapter<TestAdapter.MyViewHolder>() {
inner class MyViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tv_name)
private val tvTelephone: TextView = itemView.findViewById(R.id.tv_telephone)
fun bind(person:Person, isActivated: Boolean){
tvName.text = person.name
tvTelephone.text = person.telephone
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.item_telephone,parent,false))
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val data = list[position]
holder.bind(data)
}
override fun getItemCount(): Int = list.size
}
运行效果
基础使用
依赖引入
查看版本:Recyclerview | Android 开发者 | Android Developers (google.cn)
写本文的时候 RecyclerView-Selection 最新版本为1.1.0
,所以在项目中添加依赖如下
gradle
implementation("androidx.recyclerview:recyclerview-selection:1.1.0")
key值的选择
在开始学习使用 RecyclerView-Selection 前,我们要先进行 key 值的选择。这个 key 值将贯穿我们整个 RecyclerView-Selection 的实现,所以正确选择 key 值的类型非常重要。
对于 key 值的选择,RecyclerView-Selection 库只支持三种类
String: 基于字符串的稳定标识符
Long: 当 RecyclerView 的 long stable Id 已经在使用时可以使用 Long,但是会有一些限制,在运行时访问一个稳定的 id 会被限定。需要确保 id 是稳定的,将setHasStableIds
设置为 true,将该选项设置为 true 只会告诉 RecyclerView 数据集中的每个项目都可以用 Long 类型的唯一标识符表示
Parcelable: 任何 Parcelable 都可以用作 selection 的 key,如果 view 中的内容与稳定的content:// uri
相关联,就是用 uri 作为 key 的类型
我个人比较推荐使用 Parcelable,因为我们实现选中功能大多数是为了获取我们选中的值以便后续功能处理,而在 RV 中每个 item 都绑定着一个数据类,而数据类通常有主键也可以保证数据的唯一性 ,如果 key 值选为 Parcelable,则最后 RecyclerView-Selection 就可以直接将所选中的 item 返回给用户处理。
在查找 RecyclerView-Selection 的使用过程中,也有许多优秀文章采用 Long 作为 key 值类型,即返回选中 item 的 position 值,我觉得根据自己的项目需求进行灵活选择。如果存在 String 值可以作为唯一识别的标识符也可以选用 String 类型。但在本案例中和后续的讲解,我会选用 Parcelable 作为 key 值类型进行讲解。
回到本案例,首先需要实现我们的数据类并将其 Parcelable 序列化,因为本文使用的是Kotlin,所以可以导入插件通过注解快速实现 Parcelable 序列化。
gradle
plugins {
......
id 'kotlin-parcelize'
}
现在就可以通过注解快速实现数据类的序列化,这里要注意包的导入要正确
kotlin
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Person(
val id: Long,
val name: String,
val telephone: String,
) : Parcelable
ItemKeyProvider
官方文档:ItemKeyProvider | Android Developers (google.cn)
现在我们选定了我们 key 的类型,现在就可以构造 ItemKeyProvider 来告知我们整个 RecyclerView-Selection 键的类型。先看 ItemKeyProvider 的构造函数:
kotlin
ItemKeyProvider(@ItemKeyProvider.Scope scope: Int)
可以看出构建 ItemKeyProvider 需要一个int类型的参数scope,而 RecyclerView-Selection为我们提供了对应的常量作为参数供我们选择,我们根据项目要求自行选择:
- SCOPE_CACHED = 1 为视图中最近绑定的项提供对缓存数据的访问。使用此提供程序将减少功能集,因为某些功能(如SHIFT+单击范围选择和标注栏选择)依赖于映射访问。
- SCOPE_MAPPED = 0 提供对所有数据的访问,无论数据是否绑定到视图。具有此访问类型的密钥提供程序支持增强功能,如:SHIFT+单击范围选择和波段选择。
了解了如何选key和构造 ItemKeyProvider,我们现在就可以来写我们项目对应的 ItemKeyProvider,实现 ItemKeyProvider 需要我们重写两个方法
getKey(position: Int)
:获取指定位置 item 所对应的 key 值getPosition(key: Person)
:获取指定 key 所对应的位置
kotlin
// ItemKeyProvider的代码量很少,所以我个人是直接把这部分代码放在adapter内部
class MyKeyProvider(private val adapter: TestAdapter):ItemKeyProvider<Person>(SCOPE_CACHED){
override fun getKey(position: Int): Person = adapter.getItem(position)
override fun getPosition(key: Person): Int = adapter.getPosition(key)
}
fun getItem(position: Int): Person = list[position]
fun getPosition(person: Person): Int = list.indexOf(person)
ItemDetailsLookup
官方文档:ItemDetailsLookup | Android Developers (google.cn)
ItemDetailsLookup 使选择功能库能够访问给定 MotionEvent 对应的 RecyclerView 项的相关信息。它实际上是由 RecyclerView.ViewHolder 实例支持(或从中提取)的 ItemDetails 实例的工厂。
可以看出我们实现 ItemDetailsLookup 是为了接受 RecyclerView 的 item 上发生的 MotionEvent 事件,获取我们所点击的 ViewHolder 的具体信息,从而将数据返回给用户以供后续操作。
翻阅官方文档不难发现在实现 ItemDetailsLookup 需要重写以下方法,而方法的返回值是 ItemDetails ,所以我们需要先在 ViewHolder 实例中实现能够获取 ItemDetails 实例的函数。
kotlin
abstract fun getItemDetails(e: MotionEvent): ItemDetailsLookup.ItemDetails<K!>?
获取 ItemDetails
官方文档:ItemDetailsLookup.ItemDetails | Android Developers (google.cn)
首先在 ViewHolder 中实现getItemDetails()
函数返回 ItemDetailsLookup 所需的 ItemDetails 值。 在这里我们需要重写两个方法:
getPosition()
:返回 item 在适配器中的位置getSelectionKey()
:返回被选择 item 的 key 值 根据函数的名称我们也能很快知道其含义并实现:
kotlin
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tv_name)
private val tvTelephone: TextView = itemView.findViewById(R.id.tv_telephone)
fun bind(person: Person, isActivated: Boolean) {
tvName.text = person.name
tvTelephone.text = person.telephone
}
fun getItemDetails() = object : ItemDetailsLookup.ItemDetails<Person>() {
override fun getPosition(): Int = absoluteAdapterPosition
override fun getSelectionKey(): Person = list[absoluteAdapterPosition]
}
}
实现 ItemDetailsLookup
现在我们来实现 ItemDetailsLookup。重写方法getItemDetails(e: MotionEvent)
返回给我们用户的 MotionEvent 事件,我们就可以通过 ReyclerView 的 findChildView(int,int)
方法来判断具体点击的是哪一个 Item,然后强转成我们的 ViewHolder 类型。获取到 ViewHolder 后再配合刚刚在 ViewHolder 内实现的获取 ItemDetails 函数,我们就完成了 ItemDetailsLookup 部分的实现。
kotlin
class MyItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Person>(){
override fun getItemDetails(e: MotionEvent): ItemDetails<Person>? {
val view = recyclerView.findChildViewUnder(e.x,e.y)
return if(view != null)
(recyclerView.getChildViewHolder(view) as MyViewHolder).getItemDetails()
else null
}
}
SelectionTracker.Builder
我们之前写了那么多,是不是感觉都是零散的,感觉跟我们的选择器有关却没有任何效果。所以现在开始学习使用 SelectionTracker.Builder,通过 SelectionTracker.Builder 可以把前面所有内容汇总起来,最后实现我们对通讯录的选择。
设置 adapter
首先在 adapter 中创建我们的 SelectionTracker,后续我们会在 activity 内实现并传入。
kotlin
var tracker: SelectionTracker<Person>? = null
tracker 可以告诉我们指定 key 值是否被选择,我们可以借此来设置我们 ViewHolder 选择与否的 UI 样式。本案例是通过给布局设置 selector
,将布局的activated
状态与选择器直接关联,当然也可以通过简单的 if else 进行样式修改。
xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/darker_gray" android:state_activated="true" />
<item android:drawable="@android:color/white" />
</selector>
设置 item 的background
属性为我们刚刚创建的selector
xml
<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:background="@drawable/bg_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content">
......
</androidx.constraintlayout.widget.ConstraintLayout>
修改我们的bind()
函数,将activated
状态与选择器直接关联
kotlin
// 增加 isActivated 参数
fun bind(person: Person, isActivated: Boolean) {
tvName.text = person.name
tvTelephone.text = person.telephone
itemView.isActivated = isActivated
}
与此同时需要修改 onBindViewHolder()
内的代码
kotlin
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val data = list[position]
tracker?.let {
holder.bind(data, it.isSelected(data))
}
}
设置 activity/fragment
显而易见 adapter 内的实例还未初始化,这需要我们在 activity/fragment 内初始化后绑定到 adapter内部。而我们通过使用 SelectionTracker.Builder 来实现我们 SelectionTracker 实例。 Builder需要五个参数:
- selectionId:在 activity 或 fragment 的上下文中标识此选择的唯一字符串 说人话就是设置一个唯一的 String 类型的 id 值,因为 activity 或 fragment 可能具有多个不同的可选择列表,而所有这些列表都需要保持其已保存的状态,所以需要唯一值来区分。
- recyclerView:绑定对应的RecyclerView
- keyProvider:上面实现的 ItemKeyProvider
- detailsLookup:上面实现的 ItemDetailsLookup
- storage:选择状态的类型安全存储策略,它其实与我们 key 值类型所对应
createLongStorage()
:适合与 Long 类型 key 一起使用的存储策略createStringStorage()
:适合与 String 类型 key 一起使用的存储策略createParcelableStorage(type: Class<K!>)
:适合与 Parcelable 类型 key 一起使用的存储策略
现在我们就可以在 activity 创建我们的 tracker 实例啦! 在 Builder 中我们通过withSelectionPredicate
来可设置我们的选择模式,其中官方自带两种
createSelectAnything()
:多选createSelectSingleAnything()
:单选
kotlin
tracker = SelectionTracker.Builder(
"mySelection-1",
recyclerView,
TestAdapter.MyKeyProvider(adapter),
TestAdapter.MyItemDetailsLookup(recyclerView),
StorageStrategy.createParcelableStorage(Person::class.java)
)
// 设置选择模式
.withSelectionPredicate(
SelectionPredicates.createSelectAnything()
)
.build()
然后将 tracker 实例与 adapter 绑定:
kotlin
adapter.tracker = tracker
这里需要注意的是,recyclerview 绑定 adapter 要先于 adapter 绑定 tracker,不然会报以下错误:
css
java.lang.RuntimeException: Unable to start activity ComponentInfo{......}: java.lang.IllegalArgumentException
现在我们就可以对 tracker 进行监听,获取我们所选择的数据以便后续操作,例如批量删除操作
kotlin
tracker?.addObserver(
object : SelectionTracker.SelectionObserver<Person>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
// 打印所选择的 item 数据
for (item in tracker?.selection!!) {
println("item:$item")
}
}
}
)
也可以通过代码直接设置 item 进入选择状态
kotlin
tracker?.select(data)
最终效果
长按 item 就进入选择模式,当被选择 item 为 0 则自动退出选择模式
数据持久化
如果是自己实现选中 item 效果,我们还需要考虑如何实现数据的持久化。如果没有实现持久化,一旦屏幕配置改变,比如旋转屏幕时,我们已选中的 item 的选中状态就会消失。SelectionTracker 也考虑到这个,提供了非常便捷的API方便我们实现持久化,只要加入以下代码就可实现:
kotlin
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
tracker?.onRestoreInstanceState(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
tracker?.onSaveInstanceState(outState)
}
此时旋转屏幕选中效果依旧保持
拓展:自定义选择策略
虽然官方给了我们两种固定的选择模式,但可能在某些需求下还是不够的,所以我们这时候需要自定义选择策略。 我们需要继承接口SelectionPredicate<K>
,此时就需要重写三个方法
-
验证指定 key 下选择状态的更改
当我们进入选择模式,正常设置下对 item 点击就会对 item 的选择状态进行更改,所以此处的参数含义为:- key :当前点击 item 所绑定的数据。
- nextState:当前点击 item 后 item 所对应的选择状态。比如当前的 item 是选中的,点击后变为不选中,所以 nextState 的值为 false。 返回值为 true 意味着 item 的状态可以由 nextState 设置,所以正常情况下返回 true 即可。
kotlinpublic abstract boolean canSetStateForKey(@NonNull K key, boolean nextState);
-
验证指定 position 下选择状态的更改
但看官方注释canSetStateAtPosition
和canSetStateForKey
很像,只是从 key 变为了 position,所以参数说明如下- position :当前点击 item 所在的位置。
- nextState:当前点击 item 后 item 所对应的选择状态。比如当前的 item 是选中的,点击后变为不选中,所以 nextState 的值为 false。 返回值为 true 意味着 item 的状态可以由 nextState 设置,所以正常情况下返回 true 即可。
kotlinpublic abstract boolean canSetStateAtPosition(int position, boolean nextState);
但很奇怪的是
canSetStateAtPosition
在测试的时候一直没有调用,所以最终返回值为多少都不影响结果。注释说If necessary use {@link ItemKeyProvider} to identy associated key.
不知道是否与这个有关,如果有大佬清楚,希望能帮我解答一下。 -
是否可以选择多个 这个就比较容易理解,只要 item 可选择值大于一就要返回true,所以基本上返回 true 即可
kotlinpublic abstract boolean canSelectMultiple();
现在让我们尝试一下自己自定义只能选择两个 item 的选择策略
kotlin
private val customSelectPredicate = object : SelectionPredicate<Person>(){
// 当tracker选择的数量大于二并且下一个 item 的状态转为 true 时,我们要停止选择,即返回false
override fun canSetStateForKey(key: Person, nextState: Boolean): Boolean
= !(nextState && tracker?.selection?.size()!! >= 2)
override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean = true
// 选择两项 item,数量大于一所以返回 true
override fun canSelectMultiple(): Boolean = true
}
现在更改 builder 内的配置,发现功能能正确实现,我们自定义策略成功!
kotlin
tracker = SelectionTracker.Builder(
"mySelection-1",
recyclerView,
TestAdapter.MyKeyProvider(adapter),
TestAdapter.MyItemDetailsLookup(recyclerView),
StorageStrategy.createParcelableStorage(Person::class.java)
).withSelectionPredicate(
customSelectPredicate
).build()
完整代码
数据类 person:
kotlin
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Person(
val id: Long,
val name: String,
val telephone: String,
) : Parcelable
adapter:
kotlin
class TestAdapter(private val list: List<Person>) :
RecyclerView.Adapter<TestAdapter.MyViewHolder>() {
var tracker: SelectionTracker<Person>? = null
// 自定义ViewHolder
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tv_name)
private val tvTelephone: TextView = itemView.findViewById(R.id.tv_telephone)
// 绑定数据
fun bind(person: Person, isActivated: Boolean) {
tvName.text = person.name
tvTelephone.text = person.telephone
itemView.isActivated = isActivated
}
// 获取 ItemDetails
fun getItemDetails() = object : ItemDetailsLookup.ItemDetails<Person>() {
override fun getPosition(): Int = absoluteAdapterPosition
override fun getSelectionKey(): Person = list[absoluteAdapterPosition]
}
}
// 自定义 ItemKeyProvider
class MyKeyProvider(private val adapter: TestAdapter) : ItemKeyProvider<Person>(SCOPE_MAPPED) {
override fun getKey(position: Int): Person = adapter.getItem(position)
override fun getPosition(key: Person): Int = adapter.getPosition(key)
}
fun getItem(position: Int): Person = list[position]
fun getPosition(person: Person): Int = list.indexOf(person)
// 自定义 ItemDetailsLookup
class MyItemDetailsLookup(private val recyclerView: RecyclerView) :
ItemDetailsLookup<Person>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<Person>? {
val view = recyclerView.findChildViewUnder(e.x, e.y)
return if (view != null)
(recyclerView.getChildViewHolder(view) as MyViewHolder).getItemDetails()
else null
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_telephone, parent, false)
)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val data = list[position]
tracker?.let {
holder.bind(data, it.isSelected(data))
}
}
override fun getItemCount(): Int = list.size
}
activity:
kotlin
class MainActivity: AppCompatActivity() {
var tracker: SelectionTracker<Person>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 固定数据
val list: List<Person> = ArrayList(
listOf(
Person(1L, "Mike", "86100100"),
Person(2L, "Jane", "86100101"),
Person(3L, "John", "86100102"),
Person(4L, "Amy", "86100103"),
)
)
// 设置 RecyclerView
val recyclerView: RecyclerView = findViewById(R.id.rv_telephone)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
val adapter = TestAdapter(list)
recyclerView.adapter = adapter
// 实例化 tracker
tracker = SelectionTracker.Builder(
"test-selection",
recyclerView,
TestAdapter.MyKeyProvider(adapter),
TestAdapter.MyItemDetailsLookup(recyclerView),
StorageStrategy.createParcelableStorage(Person::class.java)
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
adapter.tracker = tracker
// 监听数据
tracker?.addObserver(
object : SelectionTracker.SelectionObserver<Person>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
for (item in tracker?.selection!!) {
println("item:$item")
}
}
})
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
tracker?.onRestoreInstanceState(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
tracker?.onSaveInstanceState(outState)
}
}
背景颜色选择器 selector
xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/darker_gray" android:state_activated="true" />
<item android:drawable="@android:color/white" />
</selector>
activity 布局
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:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_telephone"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
item 布局
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"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/bg_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_telephone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Name" />
<TextView
android:id="@+id/tv_telephone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
android:textSize="16sp"
android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name"
tools:text="Telephone" />
</androidx.constraintlayout.widget.ConstraintLayout>