簡單說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
CSDN資源:源碼下載
有好想法,我們一起探討~