ConcatAdapter-RecyclerView最佳伴侣

背景

  • 日常逛技术帖时候,无意中发现Android又新推出了一个类ConcatAdapter,该类是用来辅助RecyclerView去添加ViewHolder的。那有人有疑惑了,普通的Adapter也可以添加ViewHolder,这玩意有啥用呢?不要急,慢慢听我下面和你讲。可否还记得当初ListView对比RecyclerView唯一的优势是什么??ListView唯一的优势就是有系统api可以设置HeadView,而ReclcerView在ConcatAdapter问世前都是没有系统api支持的。说到这里相信大家都能猜到它的作用了吧!对,ConcatAdapter就是可以合并多个Adapter,可以做到在RecyclerView的头、尾插入不同的Adapter,实现一列表多样式视图, 而且还可以指定Index来插入指定Adapter。(工程师🐂🍺)
  • ConcatAdapter

    ConcatAdapter我打算分两部分来讲:一、如何入手并且使用;二、内部原因如何实现。为了减轻文章篇幅长度,我会拆成两篇文章来说。
    注意 :要想使用ConcatAdapter,必须引入依赖:implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06",比该版本高都是支持的。

    如何插入头部视图
  • 声明需要插入到头部的视图样式,即正常声明RecyclerView.Adapter和RecyclerView.ViewHolder,我们暂时命名为HeadAdapter和HeadViewHolder(只包含你想要插入到头部的视图)
  • class HeadAdapter: RecyclerView.Adapter<HeaderViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.item_head_view,parent,false)
            return HeaderViewHolder(view)
        override fun getItemCount(): Int {
            return 1
        override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
            holder.bind(position.toString() )
    //ViewHolder也没什么特殊的,你按照正常的ViewHolder去继承就行,不要在乎我的Base,我就是封装下少写点代码
    class HeaderViewHolder(val view: View) : BaseViewHolder<String>(view) {
        private val flowerNumberTextView: TextView = itemView.findViewById(R.id.item_number)
        override fun bind(number: String) {
            flowerNumberTextView.text = number
    

    看完上面的代码,你已经成功创建了一个即将被插入到列表头部的视图了。观众就懵逼了,这不是日常的创建ViewHolder和adapter而已嘛?是不是想骗我们啊??不要着急,重头戏通常都是摆最后的嘛。跟着我继续往下看。

    如何创建正常列表视图
  • 为了照顾下不同阶层的大老爷们,虽然都是简单的代码,我这里都是会贴出来的。下面创建正常列表视图,我会造100个假数据,主要展示形式就是文字列表。
  • class ContentAdapter<T : UniteBean>(val content: List<T>) :
        RecyclerView.Adapter<BaseViewHolder<T>>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T> {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.iteam_content_view, parent, false)
            return TextContentViewHolder(view)
        override fun getItemCount(): Int {
            return content.size
        override fun getItemViewType(position: Int): Int {
            return content[position].getType()
        override fun onBindViewHolder(holder: BaseViewHolder<T>, position: Int) {
            holder.bind(content[position])
    

    看到这里观众老爷们估计要喊:”rnm退钱”。一直都是正常创建adapter和viewHolder而已,有什么不同。别急别急嘛,记得你心里这句话,就是正常创建、使用而已。是的,工程师也想要尽量减少对用户习惯的侵入性,所以他们开发了ConcatAdapter。

    如何在列表中插入头部视图
  • 第一步:把头部视图的Adapter和列表内容视图的Adapter实例化。
  • 第二步:给recyclerView设置一个layoutManager
  • 第三部:创建ConcatAdapter,把所以的adapter依照视图展示样式进行添加。
  • override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_new_recycler)
            val adapter = initAdapter()
            // adapter.addAdapter(0,mHeadAdapter) (可以动态插入列表所在位置去展示)
            new_recycler.adapter = adapter
            new_recycler.layoutManager = LinearLayoutManager(this)
        private fun initAdapter(): ConcatAdapter {
            mHeadAdapter = HeadAdapter()
            mContentAdapter = ContentAdapter(mSource)
            //按照添加顺序,在列表中也是按照此顺序展示
            return ConcatAdapter(mHeadAdapter, mContentAdapter)
    

    优势:比起以往自己做头部和尾部视图方法,position = 0 或者 position == size - 1 或者根据type 去做渲染头、尾视图,用ConcatAdapter能实现单一职责,每个adapter负责自身任务,头、中、尾视图都有自己的adapter去负责管理对应的ViewHolder,扩展的同时也进行了解耦,解决以前一个RecyclerView只能对应一个Adapter的尴尬场景。

    最符合的实际应用

  • 上面给大家讲完ConcatAdapter如何使用,要结合实际场景进行开发才行,要不然我这篇文章就太垃圾了,什么都没讲。我们日常遇到的肯定是各种各样的设计师,要求自定义我们列表的头部刷新控件,就是实现那种列表头部下拉出现一个炫酷的加载动画,在没有ConcatAdapter时候,我们往RecyclerView插入一个刷新动画视图是比较困难的,但是用了ConcatAdapter就变得简单了。因为ConcatAdapter可以动态添加、删除Adapter。
  • //在recyclerView到达底部的时候,判断是否拦截点击事件的分发,我这里手指放开就把头部的视图给移除了,实际上可结合请求业务接口结束后再remove。简单实现下,给大家看效果,实际效果很不错的。
    fun dynamicCalculate(
            recyclerView: RecyclerView,
            headAdapter: RecyclerView.Adapter<*>,
            contact: ConcatAdapter
            var lastY = 0f
            recyclerView.setOnTouchListener { v: View, event: MotionEvent ->
                when (event.action) {
                    MotionEvent.ACTION_DOWN -> {
                        lastY = event.y
                        recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE &&
                                !recyclerView.canScrollVertically(-1)
                    MotionEvent.ACTION_MOVE -> {
                        if (recyclerView.scrollState != RecyclerView.SCROLL_STATE_SETTLING &&
                            !recyclerView.canScrollVertically(-1)
                            if (event.y < lastY) {
                                //手指往上滑动,给recyclerView处理滑动事件
                                lastY = event.y
                                false
                            } else {
                                lastY = event.y
                                //往下拉,把头部View展示出来
                                if (!contact.adapters.contains(headAdapter)) {
                                    contact.addAdapter(0, headAdapter)
                        } else {
                            false
                    MotionEvent.ACTION_UP -> {
                        if (contact.adapters.contains(headAdapter)) {
                            contact.removeAdapter(headAdapter)
                        } else {
                            false
                    else -> {
                        false
    

    注意:在书写头部炫酷的刷新动画时候,也要注意onViewDetachedFromWindow时候要把动画给停掉。因为再怎么说都还是在recyclerView内进行操作的,所以视图不可见后不应该再做动画的。

    最终实现效果图(用系统自带动画都能足以和ios的系统下拉刷后回弹回去的效果平起平坐了)

  • 新增的ConcatAdapter我相信往后肯定会成为主流,因为他可以把负责的列表拆分多个模块,这样无论对后期维护还是开发都是更为方便的。
  • 参考文献:在 RecyclerView 中使用 header 快人一步