有需求要实现吸顶效果, 掘金里翻到一篇大佬的文章, 借鉴了一下.
基本思路:
- 使用
RecycleView.ItemDecoration
重写onDrawOver()
方法 - 为了通用性, 不使用
Paint
等graphics接口绘制. 直接获取RecycleView的itemView
绘制在onDrawOver给定的Canvas上. - 在无法获取itemView时, 通过
反射
创建itemView - 反射创建出来的itemView, 需要走一遍
measure()
和layout()
否则无法绘制
遗留问题:
- 吸顶Item根布局中的
<android:background="">
属性无法绘制, 可以添加子View, 并设置background替代根布局的背景属性
完整代码有点长, 贴一些关键代码吧:
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val position = (parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
//获取标题的View
val gid = callback.getGroupId(position)
val titleView = mTitleViewSet[gid]?: mTitleViewSet.run{
val gp = callback.getCurrTopGroupPosition(position)
val holder = parent.findViewHolderForAdapterPosition(gp)
if(holder != null){
//已经绘制过
mTitleViewSet.put(gid, holder.itemView)
}else {
//没有绘制过, 创建一个
setupUnDrawView(parent, gp, gid, state)
}
mTitleViewSet[gid]
}
//判断& 计算 bottom高度 和大佬文章一致... 代码省略,
//将获取到的View 绘制在canvas上.
titleView.draw(c)
}
/**
* 利用反射, 设置RecycleView还没有绘制过的吸顶View
*/
private fun setupUnDrawView(parent: RecyclerView, gp: Int, gid: Long, state: RecyclerView.State) {
if (mDecoration == null) {
mDecoration = ArrayList()
var i = 0
var d: ItemDecoration?
while (parent.getItemDecorationAt(i).also { d = it } != null) {
if (d != null && d != this@TopSeekDecoration) {
//收集 其他边距Decoration, 用来绘制边距
mDecoration?.add(d!!)
}
++i
}
}
val recycler = with(parent.javaClass
.getDeclaredField("mRecycler")) {
isAccessible = true
get(parent) as RecyclerView.Recycler
}
val v = with(recycler.javaClass
.getDeclaredMethod("getViewForPosition",
Int::class.javaPrimitiveType,
Boolean::class.javaPrimitiveType)) {
isAccessible = true
//生成view
invoke(recycler, gp, true) as View
}
//计算view的宽高
parent.layoutManager.measureChild(v, 0, 0)
//缓存下来
mTitleViewSet.put(gid, v)
val margins = Rect()
//计算边距
mDecoration?.let {
for (dec in it) {
dec.getItemOffsets(margins, v, parent, state)
}
}
mTitleHeight = v.measuredHeight
//没有绘制过的View, 需要执行一次layout, 确认各个子布局的坐标
v.layout(margins.left, margins.top, margins.right, margins.bottom)
}
/**
* 回调
*/
interface TitleDecorationCallback {
/**
* 获取position对应的分组(标题)id
* @param position
* @return
*/
fun getGroupId(position: Int): Long
/**
* 获取标题的描述, debug用
* @param position
* @return
*/
fun getItemDes(position: Int): String
/**
* 获取标题所在的position
* @param position
* @return
*/
fun getCurrTopGroupPosition(position: Int): Int
}
使用方法:
val topSeek = TopSeekDecoration(this, object : TopSeekDecoration.TitleDecorationCallback {
override fun getCurrTopGroupPosition(position: Int): Int {
mAdapter?.data?.run {
for(i in position downTo 0){
val bean = get(i)
if(bean.isTitle){
return i
}
}
}
return 0
}
override fun getGroupId(position: Int): Long {
return mAdapter?.data?.get(position).groupId
}
override fun getItemDes(position: Int): String {
mAdapter?.data?.run {
return get(position).toString()
}
return "未知 Item"
}
})
mRecycleView?.addItemDecoration(topSeek)