简单说ItemDecoration就是Item的装饰,在Item的四周,我们可以给它添加上自定义的装饰。
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 stickyHeaderDecorator by lazy { StickyHeaderDecorator(requireContext()) }赋值:
with(rv_view) { layoutManager = linearLayoutManager adapter = testRecyclerAdapter addItemDecoration(stickyHeaderDecorator) }同步更新数据:
val textData = TextDataUtils().getTestData() textData.sortBy { it.title }//排序 val list = textData.map { bean -> bean.title }//记录每个item分组标题 stickyHeaderDecorator.setCategoryList(list)//同步分组标题数据Decorator testRecyclerAdapter.addAllItems(textData)//同步数据至Adapter
2. 接下来就是实现StickyHeaderDecorator 直接上代码 : )
class StickyHeaderDecorator(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地址:StickyHeaderDecoratorDemo 资源:源码下载
有好想法,我们一起探讨~