需要: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()) }}