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

前言

在 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 也就是 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.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 中查看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值