RecyclerView ItemDecoration-实现分组/悬浮(粘性)头部

简单说ItemDecoration就是Item的装饰,在Item的四周,我们可以给它添加上自定义的装饰;
(是用Kotlin实现的,如果有需要java版,可留言回复)

 

ItemDecoration主要就三个方法 : ) 

getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State){}

onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State){}

onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State){}

直接上代码(代码带注释)
1. Activity/Fragment中 : ) 
创建:

private val testRecyclerAdapter by lazy {
    TestRecyclerAdapter()
}
private val linearLayoutManager by lazy {
    LinearLayoutManager(context)
}
private val headerDecoration by lazy {
    StickyHeaderDecoration(requireContext())
}

赋值:

with(rv_view) {
            layoutManager = linearLayoutManager
            adapter = testRecyclerAdapter
            addItemDecoration(headerDecoration)
        }

同步更新数据:


val textData = TextDataUtils().getTestData()
textData.sortBy { it.title }//排序
val list = textData.map { bean -> bean.title }//记录每个item分组标题
headerDecoration.setCategoryList(list)//同步分组标题数据Decoration
testRecyclerAdapter.addAllItems(textData)//同步数据至Adapter

 

2. 接下来就是实现StickyHeaderDecoration
直接上代码 : ) 

class StickyHeaderDecoration(context: Context) : RecyclerView.ItemDecoration() {

    var hideCategoryHeader: ((isHide: Boolean) -> Unit)? = null

    var updateCategoryHeader: ((categoryName: String) -> Unit)? = null

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val colorBg = context.resources.getColor(R.color.primary_purple)
    private val colorText = context.resources.getColor(R.color.primary_white)

    private val categoryList = mutableListOf<String>()
    private val categorySet = mutableSetOf<String>()//记录有多少组子标题
    val categoryHeaderMap = mutableMapOf<String, Int>()//记录每组子标题开始的位置
    private var categoryName = ""

    fun setCategoryList(value: List<String>) {
        categoryList.clear()
        categoryList.addAll(value)
        categorySet.clear()
        categorySet.addAll(value)

        //如果分组只有一个的情况,即隐藏粘性标题
        if (categorySet.size > 1) {
            hideCategoryHeader?.invoke(false)
        } else {
            hideCategoryHeader?.invoke(true)
        }
    }

    //设置文字属性
    private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
        color = colorText
        textSize = 18.toSp()
    }

    private val headerMarginStart = 36.toDp() //子标题内容与左侧的距离
    private val headerSpaceHeight = 60.toDp() //为每个子标题对应最后一个item添加空隙高度
    private val headerBackgroundHeight = 40.toDp()//子标题背景高度
    private val headerBackgroundRadius = 10.toDp()//为子标题背景设置圆角

    //简单的理解
    // 设置item布局间隙(留空间给draw方法绘制)
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        if (isHideInventoryHeader()) return
        val adapterPosition = parent.getChildAdapterPosition(view)
        if (adapterPosition == RecyclerView.NO_POSITION) {
            return
        }
        //Top 头部
        if (isFirstOfGroup(adapterPosition)) {
            outRect.top = headerBackgroundHeight.toInt()
            categoryHeaderMap[categoryList[adapterPosition]] = adapterPosition
        }
        //Bottom 底部
        if (isEndOfGroup(adapterPosition)) {
            outRect.bottom = headerSpaceHeight.toInt()
        }
    }

    //可在此方法中绘制背景
    override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        if (isHideInventoryHeader()) return
        val count = parent.childCount
        if (count == 0) {
            return
        }
        for (i in 0 until parent.childCount) {
            val child = parent.getChildAt(i)
            val adapterPosition = parent.getChildAdapterPosition(child)
            if (isFirstOfGroup(adapterPosition)) {
                val left = child.left.toFloat()
                val right = child.right.toFloat()
                val top = child.top.toFloat() - headerBackgroundHeight
                val bottom = child.top.toFloat()
                val radius = headerBackgroundRadius
                paint.color = colorBg
                //绘制背景
                canvas.drawRoundRect(
                    left, top, right, bottom, radius,
                    radius, paint
                )
            }
        }
    }

    //留的空间给draw方法绘制内容/粘性标题也在此设置
    override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        if (isHideInventoryHeader()) return
        val count = parent.childCount
        if (count == 0) {
            return
        }
        //在每个背景上绘制文字
        drawHeaderTextIndex(canvas, parent)

        //绘制粘性标题
        drawStickyTimestampIndex(canvas, parent)
    }

    private fun drawHeaderTextIndex(canvas: Canvas, parent: RecyclerView) {
        for (i in 0 until parent.childCount) {
            val child = parent.getChildAt(i)
            val adapterPosition = parent.getChildAdapterPosition(child)
            if (adapterPosition == RecyclerView.NO_POSITION) {
                return
            }
            if (isFirstOfGroup(adapterPosition)) {
                val categoryName = categoryList[adapterPosition]
                val start = child.left + headerMarginStart
                val fontMetrics = textPaint.fontMetrics
                //计算文字自身高度
                val fontHeight = fontMetrics.bottom - fontMetrics.top
                val baseline =
                    child.top.toFloat() - (headerBackgroundHeight - fontHeight) / 2 - fontMetrics.bottom
                canvas.drawText(categoryName.toUpperCase(), start, baseline, textPaint)
            }
        }
    }

    private fun drawStickyTimestampIndex(canvas: Canvas, parent: RecyclerView) {
        val layoutManager = parent.layoutManager as LinearLayoutManager
        val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
        if (firstVisiblePosition != RecyclerView.NO_POSITION) {
            val firstVisibleChildView =
                parent.findViewHolderForAdapterPosition(firstVisiblePosition)?.itemView
            firstVisibleChildView?.let { child ->
                val firstChild = parent.getChildAt(0)
                val left = firstChild.left.toFloat()
                val right = firstChild.right.toFloat()
                val top = 0.toFloat()
                val bottom = headerBackgroundHeight
                val radius = headerBackgroundRadius
                paint.color = colorBg

                val name = categoryList[firstVisiblePosition]
                if (categoryName != name) {
                    categoryName = name
                    // 监听当前滚动到的标题
                    categoryName?.let { name ->
                        updateCategoryHeader?.invoke(name)
                    }
                }
                val start = child.left + headerMarginStart
                //计算文字高度
                val fontMetrics = textPaint.fontMetrics
                val fontHeight = fontMetrics.bottom - fontMetrics.top
                val baseline =
                    headerBackgroundHeight - (headerBackgroundHeight - fontHeight) / 2 - fontMetrics.bottom

                var upwardBottom = bottom
                var upwardBaseline = baseline
                // 下一个组马上到达顶部
                if (isFirstOfGroup(firstVisiblePosition + 1)) {
                    upwardBottom = min(child.bottom.toFloat() + headerSpaceHeight, bottom)
                    if (child.bottom.toFloat() + headerSpaceHeight < headerBackgroundHeight) {
                        upwardBaseline = baseline * (child.bottom.toFloat() + headerSpaceHeight)/headerBackgroundHeight
                    }
                }
                //绘制粘性标题背景
                canvas.drawRoundRect(left, top, right, upwardBottom, radius, radius, paint)
                //绘制粘性标题
                canvas.drawText(categoryName.toUpperCase(), start, upwardBaseline, textPaint)
            }
        }
    }

    //判断是不是每组的第一个item
    private fun isFirstOfGroup(adapterPosition: Int): Boolean {
        return adapterPosition == 0 || categoryList[adapterPosition] != categoryList[adapterPosition - 1]
    }

    //判断是不是每组的最后一个item
    private fun isEndOfGroup(adapterPosition: Int): Boolean {
        if (adapterPosition + 1 == categoryList.size) return true
        return categoryList[adapterPosition] != categoryList[adapterPosition + 1]
    }

    //如果分组只有一个的情况,即隐藏粘性标题
    private fun isHideInventoryHeader(): Boolean {
        return categorySet.size <= 1 || categoryList.isNullOrEmpty()
    }
}

 

3.  RecyclerAdapter 我还是贴一下代码,就正常写:)

class TestRecyclerAdapter : RecyclerView.Adapter<TextViewHolder>() {

    private val textBeans: MutableList<TextBean> = mutableListOf()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextViewHolder {
        return TextViewHolder(parent.inflate(R.layout.rv_test_item))
    }

    override fun getItemCount()= textBeans.size

    override fun onBindViewHolder(holder: TextViewHolder, position: Int) {
        holder.bind(textBeans[position])
    }

    fun addAllItems(items: List<TextBean>) {
        textBeans.clear()
        textBeans.addAll(items)
        notifyDataSetChanged()
    }
}

 ViewHolder:)

class TextViewHolder(view: View): RecyclerView.ViewHolder(view){

    open fun bind(testText: TextBean) {
        with(itemView) {
            item_text.text = testText.desc
        }

        itemView.setOnClickListener {
            //TODO
        }
    }
}

cc: 因为是用 Kotlin实现,里面带有Kotlin的扩展方法,我再补上:)

fun ViewGroup.inflate(@LayoutRes id: Int): View {
    return LayoutInflater.from(this.context).inflate(id, this, false)
}

fun Int.toDp(): Float = (this * Resources.getSystem().displayMetrics.density)

fun Int.toSp(): Float = (this * Resources.getSystem().displayMetrics.scaledDensity)


Git地址:(稍后贴上)
感谢你的时间!有什么好的建议一起探讨~


 

 

热门文章

暂无图片
编程学习 ·

连续子数组的最大和

一、问题描述 给定一个数组, 找出数组的一个连续子数组, 这个子数组的和最大; 遍历数组,将数组的值加入到sum中, 如果sum大于0, 继续遍历下一个数据, 如果sum小于等于0,说明前面的子数组是无用的,丢弃前面的数组,从下一个数组开始继续遍历; 二、连续子数组的最大和代…
暂无图片
编程学习 ·

Unity2D教程:菜单界面、文字设置、常用界面功能

菜单界面创建一个UI-Image,会自动生成Canvas。 设置Canvas的设置模式为随屏幕改变 设置Image的伸展模式,在这个界面按下Alt键会变成这样,选择右下角那个就是填充整个Canvas了在当前Image下创建Button,将Button下面的Text和Button自身都弄成预置物Text可以加一个Outline组件…
暂无图片
编程学习 ·

WEB安全的总结学习与心得(十一)——命令注入

WEB安全的总结学习与心得(十一) 01 命令注入之简介 类型:服务器端漏洞 02 命令注入的三个条件 1.调用可执行系统命令的函数 2.函数或函数的参数可控 3.拼接注入命令 03 命令注入的攻击过程 1.构造命令 2.拼接命令,执行注入的命令 3.结果回显
暂无图片
编程学习 ·

MIT 计算机操作环境导论Missing Semester Lesson 10 Q&A

最后一节课,我们回答学生提出的问题:学习操作系统相关内容的推荐,比如进程,虚拟内存,中断,内存管理等你会优先学习的工具有那些?使用 Python VS Bash脚本 VS 其他语言?source script.sh 和 ./script.sh 有什么区别?各种软件包和工具存储在哪里?引用过程是怎样的? /bi…
暂无图片
编程学习 ·

Linux上远程命令

远程Linux: ssh -l root 192.168.0.1 -l 用户 远程成功了在输密码 远程Windows: rdesktop - u tedu -p tedu 192.168.0.10:3389 -u 用户 -p 密码
暂无图片
编程学习 ·

转 C++常用设计模式

适配器模式可以将一个类的接口转换成客户端希望的另一个接口,使得原来由于接口不兼容而不能在一起工作的那些类可以在一起工作。通俗的讲就是当我们已经有了一些类,而这些类不能满足新的需求,此时就可以考虑是否能将现有的类适配成可以满足新需求的类。适配器类需要继承或依…
暂无图片
编程学习 ·

数据表的规范

数据库的设计范式 六种范式 1. 第一范式 2. 第二范式 3. 第三范式 4. BCNF 巴斯-科德范式 5. 第四范式 6. 第五范式 完美范式 * 范式设计越高阶,冗余度越低。数据表中的键 1. 超键: 能唯一标识元组的属性集叫超键 2. 候选键:如果超键不包括多余的属性,这个超键就是候选键 …
暂无图片
编程学习 ·

本地项目提交到Github上

1.在个人github主页创建一个空仓库2.填写完相关资料后再项目文件中打开本地git客户端3.进入到刚刚的新建仓库中,如图操作3.依次在git客户端内输入以下命令,这部会用到上面复制到的地址 git initgit add .origin后面的地址是你刚刚自己复制的地址 git remote add origin https:/…
暂无图片
编程学习 ·

詹姆斯高斯林的传奇人生

詹姆斯高斯林 (James Gosling)是一名软件专家。 高斯林1955年生于加拿大,从小就显露出不同寻常的智慧,12岁便能自己设计电子游戏。 1984年,高斯林进入太阳计算机系统公司,并在这家公司度过了26年时光。 1955年5月19日出生于加拿大,Java编程语言的共同创始人之一,一般公…
暂无图片
编程学习 ·

ssm专题学习-spring的使用与相关项目的实现(1)

ssm专题学习-spring的使用与相关项目的实现(1)前言配置maven更改镜像创建项目更新并下载所需jar包web.xml、applicationContext.xml和spring-mvc.xml配置建立架构UserUserControllerUserDaoUserServiceIUserServiceJSP部分效果图 前言 暑期线上实训的第四节课。从今天开始课程…
暂无图片
编程学习 ·

HADOOP YARN原理及资源调度

文章目录1、ResourceManager2、NodeManager3、Applicacation Master4、yarn调度方式5、Yarn架构及各角色职责6、Yarn作业执行流程7、特点8、MapReduce在Yarn上的运转 1、ResourceManager ReaouceManager(资源管理器RM)常驻守护进程: 管理集群资源,负责全局资源的监控、分配和…
暂无图片
编程学习 ·

C++的默认构造函数与构造函数

构造函数:C++用于构建类的新对象时需要调用的函数。 默认构造函数:未提供显式初始值时,用来创建对象的构造函数。 class testClass { public:testClass(); /* 默认构造函数 */testClass(int a, char b); /* 构造函数 */testClass(int a=10,char b…
暂无图片
编程学习 ·

ElasticSearch 索引设置总结

在使用ES时,我们常见的就是需要生成一个template来定义索引的设置,分词器,Mapping.本文将基于项目经验来总结一些常用的配置。Index设置index.refresh_interval 配置一个刷新时间,将index buffer刷新到os cache的时间间隔,刷新到os cache的数据才可以被索引到,默认是1s.如…
暂无图片
编程学习 ·

ubuntu20.04微信无法输入中文解决

打开输入法首选项, 勾选show suggestion去微信聊天框输入你好,会提醒下一个字,将提醒的字按空格输入到聊天框,就可以输入中文了 缺点就是每次都要这么操作
暂无图片
编程学习 ·

苹果CMSV10黑色自适应简约炫酷影视网站模板

苹果CMSV10黑色自适应简约炫酷影视网站模板苹果CMSv10最新原创酷黑渐变特色自适应视频站模板,模板非常简单,安装方法都一样,直接覆盖附件模板,后台设置模板即可,没有其他设置。酷黑渐变特色功能触动封面直接播放视频内容,自适应影视模板原文摘自: 苹果CMSV10黑色自适应简…
暂无图片
编程学习 ·

国际机器人赛事RoboCup@Home Education2020线上比赛成功举办

2020年6月27-28日,国际权威机器人赛事RoboCup@Home Education于线上举行,此赛事是RoboCup@Home的一项教育计划,旨在鼓励学校参与到以人工智能为中心的服务机器人开发,该比赛每年与RoboCup@Home同时进行,参考同一套比赛规则和评分标准,同时加入大量的教学培训和技术交流,…
暂无图片
编程学习 ·

自学python第三天

if 、while、for的使用 1、if条件 1.1、简单if if 条件:条件成⽴立执⾏行行的代码1条件成⽴立执⾏行行的代码2 ......例如 if True:print(条件成⽴立执⾏行行的代码1)print(条件成⽴立执⾏行行的代码2) # 下⽅方的代码没有缩进到if语句句块,所以和if条件⽆无关 print(我是⽆无…