实现思路

粘性头部效果的核心是监听RecyclerView滚动事件,动态计算当前屏幕顶部应显示的月份标题位置。当某个月份的标题即将被顶出屏幕时,将其固定在顶部并跟随滚动。

布局设计

使用CoordinatorLayout嵌套RecyclerView和HeaderView。HeaderView作为粘性头部容器,初始设置为GONE:

<androidx.coordinatorlayout.widget.CoordinatorLayout>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"/>
        
    <TextView
        android:id="@+id/stickyHeader"
        android:layout_gravity="top"
        android:visibility="gone"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

数据处理

确保数据源按月份分组,并添加HeaderItem类型:

data class BillItem(
    val month: String,
    val isHeader: Boolean = false,
    // other fields
)

实现步骤

监听滚动事件 在RecyclerView的OnScrollListener中计算当前应显示的头部:

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        updateStickyHeader()
    }
})

查找当前头部位置 通过LayoutManager查找第一个可见项的位置:

fun updateStickyHeader() {
    val layoutManager = recyclerView.layoutManager as LinearLayoutManager
    val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
    
    if (firstVisiblePos == RecyclerView.NO_POSITION) return
    
    val headerItem = findHeaderForPosition(firstVisiblePos)
    stickyHeader.text = headerItem.month
    stickyHeader.visibility = View.VISIBLE
    
    // 计算偏移量
    val nextHeaderPos = findNextHeaderPosition(firstVisiblePos)
    if (nextHeaderPos != -1) {
        val nextHeaderView = layoutManager.findViewByPosition(nextHeaderPos)
        val offset = nextHeaderView?.top?.minus(stickyHeader.height) ?: 0
        stickyHeader.translationY = offset.toFloat()
    }
}

Header定位逻辑 实现查找当前月份标题的方法:

fun findHeaderForPosition(pos: Int): BillItem {
    for (i in pos downTo 0) {
        if (adapter.getItem(i).isHeader) {
            return adapter.getItem(i)
        }
    }
    throw IllegalStateException("No header found")
}

性能优化

  1. 使用ViewHolder缓存已计算的Header位置
  2. 批量更新时暂停粘性头部计算
  3. 对二分查找算法优化Header定位

动画效果

添加平滑过渡动画增强用户体验:

stickyHeader.animate()
    .translationY(offset.toFloat())
    .setDuration(100)
    .start()

边界处理

处理列表顶部和底部的特殊情况:

when {
    firstVisiblePos == 0 -> stickyHeader.visibility = View.GONE
    nextHeaderPos == -1 -> stickyHeader.translationY = 0f
}

完整实现要点

  1. 自定义ItemDecoration处理普通Header绘制
  2. 粘性头部使用独立View实现悬浮效果
  3. 通过RecyclerView的ScrollListener驱动状态更新
  4. 处理好RecyclerView的测量和布局阶段

这种实现方式相比第三方库具有更好的可控性,能完美模拟微信账单的流畅滚动效果,且内存占用更低。实际项目中可根据需求添加点击事件或更复杂的交互效果。

更多推荐