Android 笔记:RecyclerView
RecyclerView 很适合用于数据集合的展示,包括以列表形式、网格形式等。它的特点是:只会维持当前界面上能够显示的数量的视图实例。例如一个屏幕只显示得下 10 条数据,那么它就只会在内存中保留稍多于 10 个的视图实例。当界面滚动时,部分数据的界面被隐藏,相应的视图实例会被回收重用于新数据的显示,这也就是它名字的由来:Recycler。这可以极大地节省系统资源。
引入 RecyclerView
# lib.versions.toml
[versions]
recyclerview = "1.3.2"
[libraries]
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
// build.gradle.kts
dependencies {
implementation(libs.androidx.recyclerview)
}
使用 RecyclerView
创建好 Fragment 或 Activity,在布局文件中使用 androidx.recyclerview.widget.RecyclerView
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/crime_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
RecyclerView 本身是个容器,只负责管理其中的子项,通过回收和重用子项来提高性能,避免频繁创建和销毁视图。
要使用 RecyclerView 时,还涉及到几个概念:
LayoutManager
LayoutManager 决定 RecyclerView 的子项如何布局和排列。常见的有:
- LinearLayoutManager:用于线性排列子项(水平或垂直方向)。
- GridLayoutManager:用于网格排列子项,可以设置列或行的数量。
- StaggeredGridLayoutManager:用于不规则的网格布局,例如瀑布流效果。
还可以自定义 LayoutManager,根据需求编写自己的布局逻辑。
简单地使用 LinearLayoutManager:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// ...
binding.crimeRecyclerView.layoutManager = LinearLayoutManager(context)
return binding.root
}
子项视图
子项是 RecyclerView 中的每一项(例如 CrimeList 中的单个 Crime),对应的视图需要单独定义:
<!-- list_item_crime.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/crime_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Crime Title"/>
<TextView
android:id="@+id/crime_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Date"/>
</LinearLayout>
ViewHolder
ViewHolder 持有子项视图的引用,RecyclerView
在内部通过 ViewHolder
的实例间接操作子项视图,实现与子项视图的交互和管理。
class CrimeHolder(val binding: ListItemCrimeBinding) : RecyclerView.ViewHolder(binding.root) {
}
自定义 ViewHolder 类继承自 RecyclerView.ViewHolder
。构造方法这里使用了 view binding 的实例作为参数,同时将根视图传入父类的构造方法。
Adapter
RecyclerView 并不是直接创建 ViewHolder 的,而是通过 Adapter 来创建。Adapter 通常维护着整个数据集合。自定义的 Adapter 需要继承 RecyclerView.Adapter
,需要实现的有 3 个方法:
- onCreateViewHolder:创建并返回一个新的 ViewHolder 实例。
- onBindViewHolder:将对应的数据绑定到指定 ViewHolder。
- getItemCount:返回数据集合的数量。
class CrimeListAdapter(private val crimes: List<Crime>) : RecyclerView.Adapter<CrimeHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CrimeHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ListItemCrimeBinding.inflate(inflater, parent, false)
return CrimeHolder(binding)
}
override fun getItemCount() = crimes.size
override fun onBindViewHolder(holder: CrimeHolder, position: Int) {
val crime = crimes[position]
holder.binding.crimeTitle.text = crime.title
holder.binding.crimeDate.text = crime.date.toString()
}
}
最后,在使用 RecyclerView 的 Fragment 或者 Activity 上给对应的 RecyclerView 的 adapter 赋值:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCrimeListBinding.inflate(inflater, container, false)
binding.crimeRecyclerView.layoutManager = LinearLayoutManager(context)
binding.crimeRecyclerView.adapter = CrimeListAdapter(crimeListViewModel.crimes)
return binding.root
}
因为 RecyclerView 回收重用的特性,Adapter 的 onCreateViewHolder()
被调用的次数会明显少于 onBindViewHolder()
。
viewType
如果列表的子项视图会根据对应的数据不同而有所不同,可以利用 onCreateViewHolder(parent: ViewGroup, viewType: Int)
的 viewType
参数来创建不同类型的 ViewHolder 来达到渲染不同视图的目的。而这里的 viewType
来源于 getItemViewType(position)
的返回值,getItemViewType(position)
需要被重写。
override fun getItemViewType(position: Int): Int {
return when (dataList[position]) {
is Banner -> VIEW_TYPE_BANNER
is Product -> VIEW_TYPE_PRODUCT
is Ad -> VIEW_TYPE_AD
else -> throw IllegalArgumentException("Invalid type")
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_BANNER -> BannerViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_banner, parent, false)
)
VIEW_TYPE_PRODUCT -> ProductViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_product, parent, false)
)
VIEW_TYPE_AD -> AdViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_ad, parent, false)
)
else -> throw IllegalArgumentException("Invalid view type")
}
}
事件绑定
因为 ViewHolder 管理着子项视图,因此对于子项视图的事件处理逻辑,可以在 ViewHolder 中编写。首先,把数据绑定的代码从 Adapter 的 onBindViewHolder()
中移到 ViewHolder 里:
class CrimeHolder(private val binding: ListItemCrimeBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(crime: Crime) {
binding.crimeTitle.text = crime.title
binding.crimeDate.text = crime.date.toString()
}
}
class CrimeListAdapter(private val crimes: List<Crime>) : RecyclerView.Adapter<CrimeHolder>() {
override fun onBindViewHolder(holder: CrimeHolder, position: Int) {
// val crime = crimes[position]
// holder.binding.crimeTitle.text = crime.title
// holder.binding.crimeDate.text = crime.date.toString()
holder.bind(crimes[position])
}
}
然后再在 CrimeHolder.bind()
方法里加上事件绑定的代码:
class CrimeHolder(private val binding: ListItemCrimeBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(crime: Crime) {
// ...
binding.root.setOnClickListener { _ ->
Toast.makeText(binding.root.context, "${crime.title} clicked!", Toast.LENGTH_SHORT).show()
}
}
}
用 ListAdapter 提升性能
Adapter 提供了一些方法用于将数据发生了怎样的变化告诉视图,例如 notifyItemMoved()
,notifyItemInserted()
,notifyItemChanged()
等,让界面作出相应的变化。但使用这些方法的前提是知道数据发生了怎样的变化。但在许多情况下,数据如何改变是无法获知的。
如果获取到的是一个修改后的数据副本,要么得通过计算和比较,得出哪些数据发生了变化,然后使用对应的 notify...
方法;要么干脆不计算,直接使用 notifyDataSetChanged()
重新渲染整个列表。当然,后者相对来说,性能和效率要差得多,它无法做到像 notifyItem...
系列方法那样精准地只修改变化了的数据所对应的视图。
这时,如果有方法可以方便地计算出具体是哪些数据发生了变化,就可以使用 notifyItem...
系列方法提高渲染效率了。而 ListAdapter
就提供了这些方法。
ListAdapter
继承自 RecyclerView.Adapter
,它内部使用了一个 DiffUtil
类,来检测哪些数据发生了变化。要用它的话,得创建一个类,继承 DiffUtil.ItemCallback
,并实现 areItemsTheSame()
和 areContentsTheSame()
方法,再将这个新创建的类的实例传给 ListAdapter
的构造方法。最后通过 ListAdapter.submitList(...)
来通知视图作出变化。
data class MyItem(val id: Int, val name: String, val description: String)
// 定义如何比较两个 MyItem 对象
class MyItemDiffCallback : DiffUtil.ItemCallback<MyItem>() {
override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
// 比较唯一标识符(通常是 ID)
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
// 比较内容是否完全相同
return oldItem == newItem
}
}
// 使用 ListAdapter
class MyAdapter : ListAdapter<MyItem, MyViewHolder>(MyItemDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
// 创建 ViewHolder
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
// 绑定数据
holder.bind(getItem(position))
}
}