如何为RecyclerView封装一个通用的Adapter

如何为RecyclerView封装一个通用的Adapter前言 在 Android 5 0 之前 如果你需要展示一个可以滚动的列表 我们用的是 ListView 控件 Android 5 0 后 官方在 support v7 包推出了一个新的控件 RecyclerView 用来替代 ListView 解决 ListView 的一些问题和缺陷

大家好,我是讯享网,很高兴认识大家。

前言

在 Android 5.0 之前,如果你需要展示一个可以滚动的列表,我们用的是 ListView 控件。Android 5.0 后,官方在 support-v7 包推出了一个新的控件:RecyclerView,用来替代 ListView,解决 ListView 的一些问题和缺陷。可以说,RecyclerView 是一个先进的、灵活的加强版 ListView

本文将使用 kotlin 作为开发语言展示示例代码,实现一个完整的通用的 RecyclerView.Adapter,并用它来实现一个类似苹果 AppStore 的典型布局。

appstore.gif
讯享网

RecyclerView 的基础使用

如果你已熟悉使用 RecyclerView,可以跳过此节

要使用 RecyclerView,我们需要导入 support-v7 库:

  1. 打开 app module 的 build.gradle
  2. 添加库的依赖
    dependencies { implementation 'com.android.support:recyclerview-v7:28.0.0' } 

    讯享网
  3. 在 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"/> 
  4. 新建一个 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 } 
  5. LayoutManagerAdapterRecyclerView 关联起来
    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 也就是 种类型的视图,我们的万能 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.OnClickListenerView.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 典型布局以及更多具体详情示例,可以在我的 github 项目 recyclerkit 中查看

小讯
上一篇 2025-03-23 19:14
下一篇 2025-03-07 14:04

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/34043.html