如何用100行代码构建一个多样式RecyclerView适配器

  • RecyclerView多样式布局的框架有很多,大家熟悉已久的BaseRecyclerViewAdapterHelpervlayout等等,包括google在新版本RecyclerView中推出的MergeAdapterConcatAdapter等。既然有这么多现成的框架,为什么还要去自己编写一个呢?很多时候这些框架考虑的都是常见通用性场景,在某些奇葩的产品设计需求中也许并不适用,这也是这篇文章出现的原因。

  • 目前多样式适配器框架总体的设计方案有两种,一种是BaseRecyclerViewAdapterHelper这种极度简化开发者编写的代码量,用最简洁的方式去实现多样式。另外一种就是vlayout这种,将每种样式设计为单独模块(子适配器),视图创建,数据绑定都在该模块内部处理,再用一个主适配器将这些子适配器进行包装关联。本文采用的是第二种设计方式。

由于代码量极少,下面就不多说了,直接贴代码:

  1. 视图构建器,保持原生api命名方式
<ViewTypeCreator.kt>
abstract class ViewTypeCreator<T, VH : ViewHolder> {

    abstract fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): VH

    abstract fun onBindViewHolder(holder: VH, data: T)

    abstract fun match(data: T): Boolean

    open fun getItemId(position: Int) = RecyclerView.NO_ID

}
  1. 数据适配器
<MultiTypeAdapter.kt>
abstract class MultiTypeAdapter : RecyclerView.Adapter<ViewHolder>() {
    private val dataCache: ArrayList<Class<*>> = ArrayList()
    private val creatorCache: SparseArray<SparseArray<ViewTypeCreator<Any, *>>> = SparseArray()
    private val viewTypeCache: SparseArray<ViewTypeCreator<Any, *>> = SparseArray()

    abstract fun getData(position: Int): Any

    inline fun <reified T : Any> registerCreator(creator: ViewTypeCreator<T, *>) {
        registerCreatorInner(T::class.java, creator)
    }

    fun registerCreatorInner(clazz: Class<*>, creator: ViewTypeCreator<*, *>) {
        var index = dataCache.indexOf(clazz)
        if (index == -1) {
            dataCache.add(clazz)
            index = dataCache.size - 1
        }
        var cache = creatorCache[index]
        if (cache == null) {
            cache = SparseArray()
        }
        val id = System.identityHashCode(creator)
        @Suppress("UNCHECKED_CAST")
        cache.put(id, creator as ViewTypeCreator<Any, *>)
        creatorCache.put(index, cache)
    }

    override fun getItemViewType(position: Int): Int {
        val data = getData(position)
        val viewType = getCreatorViewType(data)
        return if (viewType != -1) {
            viewType
        } else
            super.getItemViewType(position)
    }

    override fun getItemId(position: Int): Long {
        val itemViewType = getItemViewType(position)
        val viewCreator = getViewCreatorByViewType(itemViewType)
        return viewCreator.getItemId(position)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val viewCreator: ViewTypeCreator<*, *> = getViewCreatorByViewType(viewType)
        return viewCreator.onCreateViewHolder(LayoutInflater.from(parent.context), parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val data = getData(position)
        @Suppress("UNCHECKED_CAST")
        val viewCreator: ViewTypeCreator<Any, ViewHolder> =
            getViewCreatorByViewType(getItemViewType(position)) as ViewTypeCreator<Any, ViewHolder>
        viewCreator.onBindViewHolder(holder, data)
    }

    private fun getCreatorViewType(data: Any): Int {
        val clazz: Class<*> = data::class.java
        var viewType: Int
        val index = dataCache.indexOf(clazz)
        if (dataCache.size > 0 && index != -1) {
            val creators: SparseArray<ViewTypeCreator<Any, *>> = creatorCache[index]
            // The Data bind more than one viewTypeCreator.
            if (creators.size() > 1) {
                creators.forEach { id, viewCreator ->
                    if (viewCreator.match(data)) {
                        viewType = id
                        if (viewTypeCache.indexOfKey(viewType) < 0) {
                            viewTypeCache.put(viewType, viewCreator)
                        }
                        return viewType
                    }
                }
            }
            // The Data only bind one viewTypeCreator.
            else if (creators.size() == 1) {
                viewType = creators.keyAt(0)
                if (viewTypeCache.indexOfKey(viewType) < 0) {
                    viewTypeCache.put(viewType, creators.valueAt(0))
                }
                return viewType
            }
        }
        throw RuntimeException("Current dataType [$clazz] is not found in DataTypeCache:\n$dataCache \nPlease check the Type of data for your custom creator.")
    }

    private fun getViewCreatorByViewType(viewType: Int): ViewTypeCreator<Any, *> {
        return viewTypeCache[viewType]
    }
}

好了,代码就这么多,下面简单介绍下原理:

  1. 视图构建器不过多介绍,主要就是抽象出视图构建的方法,这里只重点说下match这个方法:
fun match(data: T): Boolean
  • 这里面接收一个数据类型参数,需要返回一个Boolean值,通常的产品设计常见一种数据类型应该是对应一种视图类型,但是就是存在这么奇葩的设计,比如返回一个Person数据类型,如果sex为男需要展示一种样式(左右布局:头像在左边,右边显示简介),如果sex为女则需要展示另一种样式(上下布局:头像在中间,下面显示简介),这样的场景就可以这样定义两种视图:
<ManCreator.kt>
class ManCreator : ViewTypeCreator<Person, ManCreator.Holder>() {
    ...
    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_man, parent, false))
    }
    // 当person为男性的时候会通过该Creator创建视图
    override fun match(data: Person) = data.sex == Sex.MAN
}

<WomanCreator.kt>
class WomanCreator : ViewTypeCreator<Person, WomanCreator.Holder>() {
    ...
    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_female, parent, false))
    }
    // 当person为女性的时候会通过该Creator创建视图
    override fun match(data: Person) = data.sex == Sex.WOMAN
}
  1. 适配器,当然是继承RecyclerView.Adapter
  • 先介绍一下3个缓存:
<MultiTypeAdapter.kt>
abstract class MultiTypeAdapter : RecyclerView.Adapter<ViewHolder>() {
    // 按数据顺序,存储数据类型
    private val dataCache: ArrayList<Class<*>> = ArrayList()// [DataType]
    // 存储ViewTypeCreator,索引为该数据类型在缓存的下标,由于一个数据类型可对应多个Creator,
    // 因此使用集合存储
    private val creatorCache: SparseArray<SparseArray<ViewTypeCreator<Any, *>>> = SparseArray()// DataTypeIndex - [ViewTypeCreators]
    // 存储ViewTypeCreator,索引为对应的viewType,一对一的关系,该缓存是为了快速查找
    private val viewTypeCache: SparseArray<ViewTypeCreator<Any, *>> = SparseArray()// ViewType - ViewTypeCreator
}
  • 接下来看下注册视图构建器的方法:
<MultiTypeAdapter.kt>
    // 自动读取泛型Data数据的类型进行存储
    inline fun <reified T : Any> registerCreator(creator: ViewTypeCreator<T, *>) {
        registerCreatorInner(T::class.java, creator)
    }

    fun registerCreatorInner(clazz: Class<*>, creator: ViewTypeCreator<*, *>) {
        var index = dataCache.indexOf(clazz)
        if (index == -1) {
            // 如果没有存储过进行缓存
            dataCache.add(clazz)
            index = dataCache.size - 1
        }
        // 初始化该Data数据类型对应的ViewTypeCreator集合
        var cache = creatorCache[index]
        if (cache == null) {
            cache = SparseArray()
        }
        // 构造唯一标识作为索引
        val id = System.identityHashCode(creator)
        @Suppress("UNCHECKED_CAST")
        cache.put(id, creator as ViewTypeCreator<Any, *>)
        creatorCache.put(index, cache)
    }
  • 最后按照RecyclerView.Adapter调用流程分析下原理:
<MultiTypeAdapter.kt>
    // 获取当前索引的数据
    abstract fun getData(position: Int): Any

    override fun getItemViewType(position: Int): Int {
        val data = getData(position)// 1.获取当前索引对应的数据
        val viewType = getCreatorViewType(data)// 2.根据当前数据获取ViewType
        return if (viewType != -1) {
            viewType
        } else
            super.getItemViewType(position)
    }

    private fun getCreatorViewType(data: Any): Int {
        val clazz: Class<*> = data::class.java// 获取data数据的class类型
        var viewType: Int
        val index = dataCache.indexOf(clazz)// 查找出该data在缓存中的索引
        if (dataCache.size > 0 && index != -1) {// 判断是否注册过该data对应的ViewTypeCreator
            val creators: SparseArray<ViewTypeCreator<Any, *>> = creatorCache[index]// 获取该data对应的ViewTypeCreator集合
            // The Data bind more than one viewTypeCreator.
            if (creators.size() > 1) {// 一个data数据对应多种viewType
                creators.forEach { id, viewCreator ->
                    if (viewCreator.match(data)) {// 遍历ViewTypeCreator,并且判断是否符合条件,也就是上面说的match匹配方法
                        viewType = id// viewType就是上面根据ViewTypeCreator实例获取到的唯一id:System.identityHashCode
                        if (viewTypeCache.indexOfKey(viewType) < 0) {// 如果viewTypeCache没有缓存过,则添加入缓存
                            viewTypeCache.put(viewType, viewCreator)// 这一级缓存是为了能够快速根据viewType查找对应的ViewTypeCreator
                        }
                        return viewType
                    }
                }
            }
            // The Data only bind one viewTypeCreator.
            else if (creators.size() == 1) {// 一个data数据对应一种viewType
                viewType = creators.keyAt(0)
                if (viewTypeCache.indexOfKey(viewType) < 0) {
                    viewTypeCache.put(viewType, creators.valueAt(0))
                }
                return viewType
            }
        }
        throw RuntimeException("Current dataType [$clazz] is not found in DataTypeCache:\n$dataCache \nPlease check the Type of data for your custom creator.")
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // 根据viewType查找到对应的ViewTypeCreator,并且通过onCreateViewHolder构建Holer视图
        val viewCreator: ViewTypeCreator<*, *> = getViewCreatorByViewType(viewType)
        return viewCreator.onCreateViewHolder(LayoutInflater.from(parent.context), parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // 根据viewType查找到对应的ViewTypeCreator,并且通过onBindViewHolder绑定数据
        val data = getData(position)
        @Suppress("UNCHECKED_CAST")
        val viewCreator: ViewTypeCreator<Any, ViewHolder> =
            getViewCreatorByViewType(getItemViewType(position)) as ViewTypeCreator<Any, ViewHolder>
        viewCreator.onBindViewHolder(holder, data)
    }

    // 根据viewType查找到对应的ViewTypeCreator
    private fun getViewCreatorByViewType(viewType: Int): ViewTypeCreator<Any, *> {
        return viewTypeCache[viewType]
    }

以上就是MultiTypeAdapter的所有代码

  1. 下面举个使用案例:
  • 先定义一个适配器,继承MultiTypeAdapter
class SampleAdapter : MultiTypeAdapter() {

    val data = mutableListOf<Any>()

    override fun getData(position: Int) = data[position]

    override fun getItemCount() = data.size
}
  • 定义两种展示文字和一种展示图片的viewType
// 文字数据类型定义:包含主标题和副标题
data class Title(val mainTitle: String = "", val subTitle: String = "")
// 文字样式1
class MainTitleCreator : ViewTypeCreator<Title, MainTitleCreator.Holder>() {
    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val title: TextView = itemView.findViewById(R.id.main_title)
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_main_title, parent, false))
    }

    override fun onBindViewHolder(holder: Holder, data: Title) {
        holder.title.text = data.mainTitle
    }

    override fun match(data: Title): Boolean {
        return !TextUtils.isEmpty(data.mainTitle) && TextUtils.isEmpty(data.subTitle)
    }
}
// 文字样式2
class SubTitleCreator : ViewTypeCreator<Title, SubTitleCreator.Holder>() {
    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val title: TextView = itemView.findViewById(R.id.sub_title)
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_sub_title, parent, false))
    }

    override fun onBindViewHolder(holder: Holder, data: Title) {
        holder.title.text = data.subTitle
    }

    override fun match(data: Title): Boolean {
        return !TextUtils.isEmpty(data.subTitle) && TextUtils.isEmpty(data.mainTitle)
    }
}

// 图片样式
class ImageCreator : ViewTypeCreator<Int, ImageCreator.Holder>() {
    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val image: ImageView = itemView.findViewById(R.id.image)
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_image, parent, false))
    }

    override fun onBindViewHolder(holder: Holder, data: Int) {
        holder.image.setImageResource(data)
    }

    override fun match(data: Int): Boolean {
        return false
    }
}
  • 注册viewTypeCreator
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        recycler_view.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
        val adapter = SampleAdapter()
        // image type
        adapter.registerCreator(ImageCreator())
        // the same bean but different view type
        adapter.registerCreator(MainTitleCreator())
        adapter.registerCreator(SubTitleCreator())
        for (i in 0..10) {
            adapter.data.add(R.drawable.test)
            adapter.data.add("I am string")
            adapter.data.add(Title("I am MainTitle"))
            adapter.data.add(Title("", "I am SubTitle"))
        }
        recycler_view.adapter = adapter
    }

最终展示效果如下:

device-2020-07-24-103721.png

如果后续产品设计新增了样式,只需要定义新的ViewTypeCreator,再注册到MultiTypeAdapter中,然后在数据集中添加对应类型的数据即可,适配器和数据,视图构建完全解耦。

可以直接远程依赖引入,项目地址:https://github.com/seagazer/multitype

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341