需要:RecyclerView列表是分类的,有好多section,每个section下有几个item,要在头部固定一个sticky header来显示section信息,列表滑动要有推动section header 的成果。有些库不反对高度可变的sticky header,看了下大部分都是应用ItemDecoration实现的,于是革新了一个简略的,间接应用即可。能够反对不同高度的header: 比方空字符串header用较小的高度;多行字符串,单行字符串。

间接看下成果:

提供假数据:
数据模型

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())    }}