RecyclerView用ItemDecoration实现StickyHeader

proheart

需求:RecyclerView列表是分类的,有好多section,每个section下有几个item,要在头部固定一个sticky header来显示section信息,列表滑动要有推动section header 的效果。有些库不支持高度可变的sticky header,看了下大部分都是使用ItemDecoration实现的,于是改造了一个简单的,直接使用即可。可以支持不同高度的header: 比如空字符串header用较小的高度;多行字符串,单行字符串。

直接看下效果:

2.gif

提供假数据:
数据模型

sealed class ItemModel {
    class SectionHeader(val label: String): ItemModel()
    class Product(val name: String, val count: Int, val price: Double): ItemModel()
}

假数据

object FakeData {
    fun buildData() = mutableListOf<ItemModel>().apply {
        // 增加一个长字符串label的header
        add(ItemModel.SectionHeader("This section label is very long, and it contains link. Http://www.should_support_link_clicking.com Please click if you need find more."))
        repeat(5) {
            add(ItemModel.Product("Banana $it", it + 10, 12.99 + it))
        }
        add(ItemModel.SectionHeader(" ")) // 增加一个header label是空字符串的case
        repeat(6) {
            add(ItemModel.Product("Apple $it", it + 20, 5.99 + it))
        }
        add(ItemModel.SectionHeader("Section 3"))
        repeat(3) {
            add(ItemModel.Product("Orange $it", it + 1, 4.99 + it))
        }
        add(ItemModel.SectionHeader("Section 4"))
        repeat(5) {
            add(ItemModel.Product("Orange $it", it + 1, 4.99 + it))
        }
        add(ItemModel.SectionHeader("Section 5"))
        repeat(7) {
            add(ItemModel.Product("Orange $it", it + 1, 4.99 + it))
        }
        add(ItemModel.SectionHeader("Section 6"))
        repeat(3) {
            add(ItemModel.Product("Orange $it", it + 1, 4.99 + it))
        }
    }
}

用ItemDecoration实现StickyHeader

class SectionStickyHeaderItemDecoration(
    private val sectionStickyHeaderListener: SectionStickyHeaderListener
) : RecyclerView.ItemDecoration() {

    private var headerHeight = 0

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
        val topChild = parent.getChildAt(0) ?: return

        val topChildPosition = parent.getChildAdapterPosition(topChild)
        if (topChildPosition == RecyclerView.NO_POSITION) return

        val headerPosition = sectionStickyHeaderListener.getHeaderPositionForItem(topChildPosition)
        val currentHeader = getHeaderViewForItem(headerPosition, parent)
        fixLayoutSize(parent, currentHeader)

        val contactPoint = currentHeader.bottom
        val childInContact = getChildInContact(parent, contactPoint, headerPosition)

        if (childInContact != null && sectionStickyHeaderListener.isHeader(
                parent.getChildAdapterPosition(
                    childInContact
                )
            )
        ) {
            moveHeader(c, currentHeader, childInContact)
            return
        }

        drawHeader(c, currentHeader)
    }

    private fun drawHeader(c: Canvas, header: View) {
        c.save()
        c.translate(0f, 0f)
        header.draw(c)
        c.restore()
    }

    private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View) {
        c.save()
        c.translate(0f, (nextHeader.top - currentHeader.height).toFloat())
        currentHeader.draw(c)
        c.restore()
    }

    private fun getChildInContact(
        parent: RecyclerView,
        contactPoint: Int,
        currentHeaderPos: Int
    ): View? {
        var childInContact: View? = null
        for (i in 0 until parent.childCount) {
            var heightTolerance = 0
            val child = parent.getChildAt(i)
            //measure height tolerance with child if child is another header
            if (currentHeaderPos != i) {
                val isChildHeader =
                    sectionStickyHeaderListener.isHeader(parent.getChildAdapterPosition(child))
                if (isChildHeader) {
                    heightTolerance = headerHeight - child.height
                }
            }

            //add heightTolerance if child top be in display area
            val childBottomPosition = if (child.top > 0) {
                child.bottom + heightTolerance
            } else {
                child.bottom
            }
            if (childBottomPosition > contactPoint) {
                if (child.top <= contactPoint) {
                    // This child overlaps the contactPoint
                    childInContact = child
                    break
                }
            }
        }
        return childInContact
    }

    // Measures and layouts the top sticky header
    private fun fixLayoutSize(parent: RecyclerView, view: View) {
        // Specs for parent (RecyclerView)
        val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
        val heightSpec =
            View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)

        // Specs for children (headers)
        val childWidthSpec = ViewGroup.getChildMeasureSpec(
            widthSpec,
            parent.paddingLeft + parent.paddingRight,
            view.layoutParams.width
        )
        val childHeightSpec = ViewGroup.getChildMeasureSpec(
            heightSpec,
            parent.paddingTop + parent.paddingBottom,
            view.layoutParams.height
        )
        view.measure(childWidthSpec, childHeightSpec)
        headerHeight = view.measuredHeight
        view.layout(0, 0, view.measuredWidth, view.measuredHeight)
    }

    private fun getHeaderViewForItem(headerPosition: Int, parent: RecyclerView): View {
        val layoutId = sectionStickyHeaderListener.getHeaderLayout(headerPosition)
        val header = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
        sectionStickyHeaderListener.bindHeaderData(header, headerPosition)
        return header
    }

    interface SectionStickyHeaderListener {
        fun getHeaderPositionForItem(itemPosition: Int): Int
        fun getHeaderLayout(headerPosition: Int): Int
        fun bindHeaderData(header: View, headerPosition: Int)
        fun isHeader(itemPosition: Int): Boolean
    }
}

让Adapter实现必要的接口

class DataAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>(),
    SectionStickyHeaderItemDecoration.SectionStickyHeaderListener {

    private val data = mutableListOf<ItemModel>()

    fun setData(list: List<ItemModel>) {
        data.clear()
        data.addAll(list)
    }

    override fun getItemViewType(position: Int) =
        if (data[position] is ItemModel.SectionHeader) HEADER else ITEM

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflate = LayoutInflater.from(parent.context)
        return if (viewType == HEADER) {
            val view = inflate.inflate(R.layout.header, parent, false)
            HeaderViewHolder(view)
        } else {
            val view = inflate.inflate(R.layout.item, parent, false)
            ProductViewHolder(view)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is HeaderViewHolder)
            holder.bind(data[position] as ItemModel.SectionHeader)
        else if (holder is ProductViewHolder)
            holder.bind(data[position] as ItemModel.Product)
    }

    override fun getItemCount() = data.size

    inner class ProductViewHolder(view: View) : RecyclerView.ViewHolder(view) {

        private var title: TextView = view.findViewById(R.id.name)
        private var count: TextView = view.findViewById(R.id.count)
        private var price: TextView = view.findViewById(R.id.price)

        fun bind(product: ItemModel.Product) {
            val context = title.context
            title.text = product.name
            count.text = String.format(context.getString(R.string.count), product.count.toString())
            price.text = String.format(context.getString(R.string.price), product.price.toString())
        }
    }

    inner class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        var header: TextView = view.findViewById(R.id.list_item_section_text)

        fun bind(sectionHeader: ItemModel.SectionHeader) {
            header.text = sectionHeader.label
            header.layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, if (sectionHeader.label.trim().isEmpty()) {
                    convertPixelsToDp(header.context, EMPTY_HEADER_HEIGHT).toInt()
                } else {
                    ViewGroup.LayoutParams.WRAP_CONTENT
                }
            )
        }
    }

    private fun convertPixelsToDp(context: Context, px: Float): Float {
        return px / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
    }

    override fun getHeaderPositionForItem(itemPosition: Int): Int {
        for (i in itemPosition downTo 1) {
            if (isHeader(i)) return i
        }
        return 0
    }

    override fun getHeaderLayout(headerPosition: Int) = R.layout.header

    override fun bindHeaderData(header: View, headerPosition: Int) {
        val label = (data[headerPosition] as ItemModel.SectionHeader).label
        header.findViewById<TextView>(R.id.list_item_section_text).apply {
            text = label
            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                if (label.trim().isEmpty()) {
                    convertPixelsToDp(header.context, EMPTY_HEADER_HEIGHT).toInt()
                } else {
                    ViewGroup.LayoutParams.WRAP_CONTENT
                }
            )
        }
    }

    override fun isHeader(itemPosition: Int) = data[itemPosition] is ItemModel.SectionHeader

    companion object {
        const val HEADER = 0
        const val ITEM = 1
        const val EMPTY_HEADER_HEIGHT = 75F
    }
}

在Activity/Fragment中使用之。


class MainActivity : AppCompatActivity() {

    lateinit var recyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recyclerView = findViewById(R.id.recycler_view)

        val dataAdapter = DataAdapter()
        val stickyHeaderItemDecoration = SectionStickyHeaderItemDecoration(dataAdapter)
        val dividerItemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
        val linearLayoutManager = LinearLayoutManager(this)

        recyclerView.apply {
            adapter = dataAdapter
            layoutManager = linearLayoutManager
            recyclerView.addItemDecoration(stickyHeaderItemDecoration) // sticky header
            recyclerView.addItemDecoration(dividerItemDecoration) // 分割线
        }

        dataAdapter.setData(FakeData.buildData())
    }
}
阅读 266

Developer, Java & Android

38 声望
20 粉丝
0 条评论

Developer, Java & Android

38 声望
20 粉丝
文章目录
宣传栏