写在前面
最近在写一个项目,里边有一个常见的需求便是:多项选中 RecyclerView 的 item 进行批量处理(例如批量删去)。尽管能够自行完成,但感觉这么常见的功用或许有什么比较常用的库能够便利运用,四处查查,终究被我发现了这个库 RecyclerView-Selection !官方是这么阐明的:
凭借
recyclerview-selection
库,用户能够经过触摸或鼠标输入来挑选RecyclerView列表中的项。您仍然能够操控所选项的视觉出现作用。您也仍然能够操控用于束缚挑选行为的政策,例如契合入选条件的项以及能够挑选的项数。
翻了下相关文档,完成起来相对于自己完成仍是便利简略多,但相关教程比较少和零散,所以这篇文章就此诞生。
参阅文章
官方文档:
自定义 RecyclerView | Android 开发者 | Android Developers (google.cn)
其他优异文章:
recyclerview-selection简化RecyclerView复选及状况耐久化 – ()
Android-RecyclerView 的 SelectionTracker 挑选器运用 | JiaoPeng`s Blogs (hijiaopeng.github.io)
A guide to recyclerview-selection | by Marcos Holgado | ProAndroidDev
预备
布局
这是一个简略的通讯录demo,只需一个RecyclerView用于展现数据
<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别离用于展现联系人名字和联系人电话
<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
实体类用于存储展现数据
data class Person(
val id: Long, // 仅有id
val name: String, // 通讯录名字
val telephone: String, // 通讯录电话
)
数据的来源不是本文的重点,所以事先写死几个数据用于展现测试用
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
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
,所以在项目中添加依赖如下
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 序列化。
plugins {
......
id 'kotlin-parcelize'
}
现在就能够经过注解快速完成数据类的序列化,这儿要注意包的导入要正确
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 的结构函数:
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 所对应的位置
// 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 实例的函数。
abstract fun getItemDetails(e: MotionEvent): ItemDetailsLookup.ItemDetails<K!>?
获取 ItemDetails
官方文档:ItemDetailsLookup.ItemDetails | Android Developers (google.cn)
首先在 ViewHolder 中完成getItemDetails()
函数回来 ItemDetailsLookup 所需的 ItemDetails 值。
在这儿咱们需求重写两个办法:
-
getPosition()
:回来 item 在适配器中的位置 -
getSelectionKey()
:回来被挑选 item 的 key 值 根据函数的名称咱们也能很快知道其意义并完成:
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 部分的完成。
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 内完成并传入。
var tracker: SelectionTracker<Person>? = null
tracker 能够告诉咱们指定 key 值是否被挑选,咱们能够借此来设置咱们 ViewHolder 挑选与否的 UI 款式。本事例是经过给布局设置 selector
,将布局的activated
状况与挑选器直接相关,当然也能够经过简略的 if else 进行款式修正。
<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
<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
状况与挑选器直接相关
// 添加 isActivated 参数
fun bind(person: Person, isActivated: Boolean) {
tvName.text = person.name
tvTelephone.text = person.telephone
itemView.isActivated = isActivated
}
与此同时需求修正 onBindViewHolder()
内的代码
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()
:单选
tracker = SelectionTracker.Builder(
"mySelection-1",
recyclerView,
TestAdapter.MyKeyProvider(adapter),
TestAdapter.MyItemDetailsLookup(recyclerView),
StorageStrategy.createParcelableStorage(Person::class.java)
)
// 设置挑选形式
.withSelectionPredicate(
SelectionPredicates.createSelectAnything()
)
.build()
然后将 tracker 实例与 adapter 绑定:
adapter.tracker = tracker
这儿需求注意的是,recyclerview 绑定 adapter 要先于 adapter 绑定 tracker,不然会报以下错误:
java.lang.RuntimeException: Unable to start activity ComponentInfo{……}: java.lang.IllegalArgumentException
现在咱们就能够对 tracker 进行监听,获取咱们所挑选的数据以便后续操作,例如批量删去操作
tracker?.addObserver(
object : SelectionTracker.SelectionObserver<Person>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
// 打印所挑选的 item 数据
for (item in tracker?.selection!!) {
println("item:$item")
}
}
}
)
也能够经过代码直接设置 item 进入挑选状况
tracker?.select(data)
终究作用
长按 item 就进入挑选形式,当被挑选 item 为 0 则自动退出挑选形式
数据耐久化
假如是自己完成选中 item 作用,咱们还需求考虑怎么完成数据的耐久化。假如没有完成耐久化,一旦屏幕配置改动,比方旋转屏幕时,咱们已选中的 item 的选中状况就会消失。SelectionTracker 也考虑到这个,供给了非常快捷的API便利咱们完成耐久化,只需参加以下代码就可完成:
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 即可。
public 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 即可。
但很奇怪的是public abstract boolean canSetStateAtPosition(int position, boolean nextState);
canSetStateAtPosition
在测试的时分一直没有调用,所以终究回来值为多少都不影响结果。注释说If necessary use {@link ItemKeyProvider} to identy associated key.
不知道是否与这个有关,假如有大佬清楚,希望能帮我解答一下。 - 是否能够挑选多个
这个就比较简单理解,只需 item 可挑选值大于一就要回来true,所以基本上回来 true 即可
public abstract boolean canSelectMultiple();
现在让咱们尝试一下自己自定义只能挑选两个 item 的挑选战略
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 内的配置,发现功用能正确完成,咱们自定义战略成功!
tracker = SelectionTracker.Builder(
"mySelection-1",
recyclerView,
TestAdapter.MyKeyProvider(adapter),
TestAdapter.MyItemDetailsLookup(recyclerView),
StorageStrategy.createParcelableStorage(Person::class.java)
).withSelectionPredicate(
customSelectPredicate
).build()
完整代码
数据类 person:
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Person(
val id: Long,
val name: String,
val telephone: String,
) : Parcelable
adapter:
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:
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
<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 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 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>