前言
在 Android 5.0 之前,如果你需要展示一个可以滚动的列表,我们用的是 ListView
控件。Android 5.0 后,官方在 support-v7
包推出了一个新的控件:RecyclerView
,用来替代 ListView
,解决 ListView
的一些问题和缺陷。可以说,RecyclerView
是一个先进的、灵活的加强版 ListView
。
本文将使用
kotlin
作为开发语言展示示例代码,实现一个完整的通用的RecyclerView.Adapter
,并用它来实现一个类似苹果 AppStore 的典型布局(后续再加入一个类似淘宝主页的布局)。
RecyclerView 的基础使用
如果你已熟悉使用
RecyclerView
,可以跳过此节
要使用 RecyclerView
,我们需要导入 support-v7
库:
- 打开 app module 的
build.gradle
- 添加库的依赖
dependencies { implementation 'com.android.support:recyclerview-v7:28.0.0' }
- 在 layout 文件中引入(可以作为整个布局的父容器)
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent"/>
- 新建一个 Adapter 类继承
RecyclerView.Adapter
并重写其以下方法
这里我们把类命名为fun onCreateViewHolder(parent: ViewGroup, type: Int): VH fun getItemCount(): Int fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int)
RecyclerAdapter
class RecyclerAdapter(private val dataSet: Array<String>) : RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() { class ViewHolder(val textView: TextView) : RecyclerView.ViewHolder(textView) // 供LayoutManager调用,创建新的视图 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerAdapter.ViewHolder { val textView = LayoutInflater.from(parent.context) .inflate(R.layout.recycler_text_view, parent, false) as TextView return ViewHolder(textView) } // 供LayoutManager调用,绑定视图数据 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { holder.textView.text = dataSet[position] } // 供LayoutManager调用,返回视图数量大小 override fun getItemCount() = dataSet.size }
- 将
LayoutManager
、Adapter
和RecyclerView
关联起来class RecyclerActivity: Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.recycler_activity) val dataSet = arrayListOf("one", "two", "three") val layoutManager = LinearLayoutManager(this) val adapter = RecyclerAdapter(dataSet) recyclerView = findViewById<RecyclerView>(R.id.recyclerView) recyclerView.layoutManager = linearLayoutManager recyclerView.adapter = adapter } }
通过以上5个步骤,我们用 RecyclerView
完成了最基本的布局和数据绑定
现在我们思考一个问题:在开发过程中,对于每个类似滚动列表或者网格甚至瀑布流的页面,我们是否都需要对每个页面都创建一个属于该页面的
Adapter
,然后重写第4步中的每一个方法?
答案很明显是否定的
接下来我们来看看如何编写一个通用的 Adapter
,以便让它胜任各类复杂布局,以避免我们每开发一个页面都创建一个属于该页面的 Adapter
的情况
一步步实现 "万能的" Adapter
要胜任各种布局,就要从 Adapter
中有关视图的方法中寻找解决方法:
// onCreateViewHolder 方法通过 viewType 创建视图
fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerAdapter.ViewHolder
onCreateViewHolder
第二个参数是 viewType
,也就是视图的类型。可以看出,通过这个参数,我们可以只在一个 Adapter
中便创建出 Int.MAX_VALUE
也就是 2147483647
种类型的视图,我们的万能 Adapter
就从这里开始突破
RecyclerView.Adapter
中还有一个方法:
// 根据 position 返回 viewType
fun getItemViewType(position: Int): Int
所有关键点就在 viewType
这里!因此我们的 ReclclerAdapter
必须重写 getItemViewType
方法,用于返回我们想要的 viewType
给视图创建者 LayoutManager
在这里我们定义一个通用的 ViewModel
类,用这个类的 layout
属性来保存视图模型中每一条数据的视图类型 viewType
,我们还需要为每一个 ViewModel
标识它的 spanSize
,以便我们使用 GridLayoutManager
(或者其它 LayoutManager
) 时决定这个视图所占的行数或者列数,最后用类型为 Any
的属性 value
来保存任意的实体数据:
/*
* ViewModel
* RecyclerAdapter子类
* @param layout: 就是我们的viewType
* @param spanSize: 当使用GridLayoutManager时View占据的列数(水平布局时为行数)
* @param value: 保存各类实体数据
*/
data class ViewModel(
val layout: Int,
val spanSize: Int,
val value: Any)
于是我们的 RecyclerAdapter
有如下代码:
// RecyclerAdapter
...
private val models = ArrayList<ViewModel>()
override fun getItemViewType(position: Int): Int {
return models[position].layout
}
override fun onCreateViewHolder(parent: ViewGroup, type: Int)
: RecyclerView.ViewHolder {
// type is layout
// see fun getItemViewType
val view = LayoutInflater.from(parent.context).inflate(type, parent, false)
return ViewHolder(view)
}
/*
* 当使用GridLayoutManager时,我们可以这样:
* layoutManager.spanSizeLookup = adapter.getSpanSizeLookup()
*/
fun getSpanSizeLookup(): GridLayoutManager.SpanSizeLookup {
return object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
// empty spanCount must equal to GridLayoutManager's spanCount
return models[position].spanSize
}
}
}
class ViewHolder(val view: View): RecyclerView.ViewHolder(view)
...
也就是说,我们平时编写的 layout
文件夹中的 xml
文件就是 viewType
!
于是,我们就可以用下面示例代码实现混合布局(3种布局),用 spanSize
来决定行数或者列数:
// Some Activity or Fragment
...
val models = arrayListOf(
RecyclerAdapter.ViewModel(
R.layout.layout_1,
2,
"I'm Layout 1"
)
RecyclerAdapter.ViewModel(
R.layout.layout_2,
1,
"I'm Layout 2"
)
RecyclerAdapter.ViewModel(
R.layout.layout_3,
1,
"I'm Layout 3"
)
)
val adapter = RecyclerAdapter(models)
...
我们甚至还可以在 RecycleView
中嵌套 RecycleView
,用 RecyclerAdapter
实现纵横交错的布局。
上面我们用 onCreateViewHolder
方法通过核心参数 viewType
在同一个 Adapter
中创建出我们想要的各种layout视图,接下来我们需要对视图的数据进行绑定。视图的引用保存在 RecyclerAdapter.ViewHolder
中,这里我们优化一下这个类:
class ViewHolder(val context: Context, val view: View) : RecyclerView.ViewHolder(view) {
private val views: SparseArray<View> = SparseArray()
fun <T: View> findView(key: Int): T {
var v = views[key]
if (v == null) {
v = view.findViewById<T>(key)
views.put(key, v)
}
@Suppress("UNCHECKED_CAST")
return v as T
}
}
各类View是以树的形式组织的,而且 RecyclerView
会对 viewType
相同的视图进行回收重用
,也就是 Recycle
,为了避免大量视图绑定时频繁调用 findViewById
方法(递归),我们使用 SparseArray
来缓存视图中的子视图(其实优化效果并不明显 _)。
接下来我们定义一个函数别名用作数据绑定的接口:
typealias OnModelViewBind = (
index: Int,
viewModel: RecyclerAdapter.ViewModel,
viewHolder: RecyclerAdapter.ViewHolder
) -> Unit
index
为视图模型的下标,等同于 position
。
为 RecyclerAdapter
定义一个函数引用:
// RecyclerAdapter
...
var onModelViewBind: OnModelViewBind? = null
...
然后 onBindViewHolder
函数实现如下:
// RecyclerAdapter
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
val model = models[position]
onModelViewBind?.invoke(position, model, viewHolder)
}
于是在外部代码中,视图与数据的绑定代码就这样写:
// Some Activity or Fragment
adapter.onModelViewBind = { index, viewModel, viewHolder ->
when (viewModel.layout) {
R.layout.layout_1 -> {
// get view from viewHolder.findView function
// get model form viewModel.value
}
R.layout.layout_2 -> {
// get view from viewHolder.findView function
// get model form viewModel.value
}
R.layout.layout_3 -> {
// get view from viewHolder.findView function
// get model form viewModel.value
}
}
}
我们还需要两个函数来为 RecyclerAdapter
初始化和添加 ViewModel
,请仔细看它们的区别:
// RecyclerAdapter
...
/**
* initial the items of recycler adapter
* @param items items to display
*/
fun setItems(items: ArrayList<ViewModel>) {
models.clear()
models.addAll(items)
notifyDataSetChanged()
}
/**
* add the items of recycler adapter
* @param items items to add
*/
fun addItems(items: ArrayList<ViewModel>) {
models.addAll(items)
notifyDataSetChanged()
}
...
接下来我们给 RecyclerView
的每一个视图项添加单击事件和长按事件,先定义两个函数别名作为单击事件和长按事件的接口:
...
typealias OnModelViewClick = (
index: Int,
viewModel: RecyclerAdapter.ViewModel
) -> Unit
typealias OnModelViewLongClick = (
index: Int,
viewModel: RecyclerAdapter.ViewModel
) -> Unit
...
然后为 RecyclerAdapter
定义两个函数引用:
// RecyclerAdapter
...
var onModelViewClick: OnModelViewClick? = null
var onModelViewLongClick: OnModelViewLongClick? = null
...
继承 View.OnClickListener
和 View.OnLongClickListener
接口并实现其方法:
class RecyclerAdapter(private val context: Context, private val spanCount: Int = 1)
: RecyclerView.Adapter<RecyclerView.ViewHolder>(), View.OnClickListener, View.OnLongClickListener {
override fun onClick(view: View) {
val position = recyclerView.getChildAdapterPosition(view)
if (!models.isEmpty() && position >= 0) {
val model = models[position]
onModelViewClick?.invoke(position, model)
}
}
override fun onLongClick(view: View): Boolean {
val position = recyclerView.getChildAdapterPosition(view)
if (!models.isEmpty() && position >= 0) {
val model = models[position]
onModelViewLongClick?.invoke(position, model)
}
return true
}
}
修改 onCreateViewHolder
方法:
override fun onCreateViewHolder(parent: ViewGroup, type: Int): RecyclerView.ViewHolder {
// type is layout
// see fun getItemViewType
val view = LayoutInflater.from(parent.context).inflate(type, parent, false)
view.setOnClickListener(this)
view.setOnLongClickListener(this)
return ViewHolder(view)
}
这样便实现了 RecyclerView
的视图项的单击和长按事件。
我们还可以给 RecyclerAdapter
添加更多的功能,用来支持更多的场景:
- 视图模型数量为零时显示一个
空视图
- 视图模型的多选
- 为
RecyclerView
添加下拉刷新和上拉加载更多 - ...
文章开头提到的 App Store 典型布局(分别用kotlin
与java
实现)以及更多具体详情示例,可以在我的 github 项目 recyclerkit 中查看