需求
- 实现分组头部悬停效果。
- 悬停部分支持点击事件。
前提
- 数据集应该是header/items类似的情形。
- 数据集应该已经分组排好序。
- 每个item都有对应的一个头部
- 第一项应该是header
代码实现
class HeaderItemDecoration(
parent: RecyclerView,
private val shouldFadeOutHeader: Boolean = false,
private val isHeader: (itemPosition: Int) -> Boolean
) : RecyclerView.ItemDecoration() {
private var currentHeader: Pair<Int, RecyclerView.ViewHolder>? = null
init {
parent.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
// clear saved header as it can be outdated now
currentHeader = null
}
})
parent.doOnEachNextLayout {
// clear saved layout as it may need layout update
currentHeader = null
}
// handle click on sticky header
parent.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(
recyclerView: RecyclerView,
motionEvent: MotionEvent
): Boolean {
return if (motionEvent.action == MotionEvent.ACTION_DOWN) {
val b = motionEvent.y <= currentHeader?.second?.itemView?.bottom ?: 0
val second = currentHeader?.second
if (b && second is ChannelHotHeadViewHolder && !second.switchSort.isChecked && (motionEvent.x >= second.switchSort.left && motionEvent.x <= second.switchSort.right)) {
//点击事件传递
EventBus.getDefault().post(ClassifyItemHeadClickEvent())
}
b
} else false
}
})
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
//val topChild = parent.getChildAt(0) ?: return
val topChild = parent.findChildViewUnder(
parent.paddingLeft.toFloat(),
parent.paddingTop.toFloat() /*+ (currentHeader?.second?.itemView?.height ?: 0 )*/
) ?: return
val topChildPosition = parent.getChildAdapterPosition(topChild)
if (topChildPosition == RecyclerView.NO_POSITION) {
return
}
val headerView = getHeaderViewForItem(topChildPosition, parent) ?: return
val contactPoint = headerView.bottom + parent.paddingTop
val childInContact = getChildInContact(parent, contactPoint) ?: return
if (isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, headerView, childInContact, parent.paddingTop)
return
}
drawHeader(c, headerView, parent.paddingTop)
}
private fun getHeaderViewForItem(itemPosition: Int, parent: RecyclerView): View? {
if (parent.adapter == null) {
return null
}
val adapter = parent.adapter
val headerPosition = getHeaderPositionForItem(itemPosition)
if (headerPosition == RecyclerView.NO_POSITION) return null
val headerType = adapter?.getItemViewType(headerPosition) ?: return null
// if match reuse viewHolder
if (currentHeader?.first == headerPosition && currentHeader?.second?.itemViewType == headerType) {
if (currentHeader?.second is ChannelHotHeadViewHolder && adapter is ChannelListAdapter) {
(currentHeader?.second as ChannelHotHeadViewHolder).switchSort.isChecked = adapter.isDefaultSort
}
return currentHeader?.second?.itemView
}
val headerHolder = adapter.createViewHolder(parent, headerType)
if (headerHolder is ChannelClassifyHeadViewHolder) {
headerHolder.vLine.visibility = View.GONE
}
if (adapter is ChannelListAdapter) {
if (headerType != ChannelListAdapter.HOT_HEAD_TYPE) {
parent.adapter?.onBindViewHolder(headerHolder, headerPosition)
} else if (headerHolder is ChannelHotHeadViewHolder) {
headerHolder.switchSort.isChecked = adapter.isDefaultSort
}
fixLayoutSize(parent, headerHolder.itemView)
// save for next draw
currentHeader = headerPosition to headerHolder
}
return headerHolder.itemView
}
private fun drawHeader(c: Canvas, header: View, paddingTop: Int) {
c.save()
c.translate(0f, paddingTop.toFloat())
header.draw(c)
c.restore()
}
private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View, paddingTop: Int) {
c.save()
if (!shouldFadeOutHeader) {
c.clipRect(0, paddingTop, c.width, paddingTop + currentHeader.height)
} else {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
c.saveLayerAlpha(
RectF(0f, 0f, c.width.toFloat(), c.height.toFloat()),
(((nextHeader.top - paddingTop) / nextHeader.height.toFloat()) * 255).toInt()
)
} else {
c.saveLayerAlpha(
0f, 0f, c.width.toFloat(), c.height.toFloat(),
(((nextHeader.top - paddingTop) / nextHeader.height.toFloat()) * 255).toInt(),
Canvas.ALL_SAVE_FLAG
)
}
}
c.translate(0f, (nextHeader.top - currentHeader.height).toFloat() /*+ paddingTop*/)
currentHeader.draw(c)
if (shouldFadeOutHeader) {
c.restore()
}
c.restore()
}
private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? {
var childInContact: View? = null
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val mBounds = Rect()
parent.getDecoratedBoundsWithMargins(child, mBounds)
if (mBounds.bottom > contactPoint) {
if (mBounds.top <= contactPoint) {
// This child overlaps the contactPoint
childInContact = child
break
}
}
}
return childInContact
}
/**
* Properly measures and layouts the top sticky header.
*
* @param parent ViewGroup: RecyclerView in this case.
*/
private fun fixLayoutSize(parent: ViewGroup, 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)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
private fun getHeaderPositionForItem(itemPosition: Int): Int {
var headerPosition = RecyclerView.NO_POSITION
var currentPosition = itemPosition
do {
if (isHeader(currentPosition)) {
headerPosition = currentPosition
break
}
currentPosition -= 1
} while (currentPosition >= 0)
return headerPosition
}
}
inline fun View.doOnEachNextLayout(crossinline action: (view: View) -> Unit) {
addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
action(
view
)
}
}
复制代码
实现逻辑
- ItemDecoration的常用用法,通过复写它的3个方法:
- getItemOffsets 撑开 ItemView 上、下、左、右四个方向的空间。
- onDraw 在 ItemView 内容之下绘制图形。
- onDrawOver 在 ItemView 内容之上绘制图形。
- 复写onDrawOver我们可以通过绘制ItemView来实现悬停的效果。
-
在onDrawOver方法内部获取屏幕内可见到第一个View。
View topChild = parent.getChildAt(0); 复制代码
-
确定该View对应的ViewHeader。
int topChildPosition = parent.getChildAdapterPosition(topChild); View currentHeader = getHeaderViewForItem(topChildPosition, parent); 复制代码
-
定义drawHeader()方法来绘制RecyclerView中悬停的HeaderView。
-
- 实现一个动画效果:当一个新的HeaderView接近头部时,它应该可以可以push掉顶部的HeaderView,并最终占据头部的位置。
- 确定头部的是否HeaderView是否遇到即将到的新的HeaderView。
View childInContact = getChildInContact(parent, contactPoint); 复制代码
- 获取接触点(contact point): 我们绘制的HeaderView的底部和即将成为HeaderView的头部。
int contactPoint = currentHeader.getBottom(); 复制代码
- 如果列表中的Item即将接近contact point,重绘HeaderView,它的底部和即将到的项的顶部重合。 实现translate() 方法:HeaderView的头部将会慢慢消失,“就好像它被慢慢push到屏幕外直至不可见”。当它完全不可见时,绘制新的HeaderView。
if (childInContact != null) { if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) { moveHeader(c, currentHeader, childInContact); } else { drawHeader(c, currentHeader); } 复制代码
- 确定头部的是否HeaderView是否遇到即将到的新的HeaderView。
- 点击事件
- HeaderView内部有一个SwitchButton的点击事件。HeaderView是Canvas绘制的,内部并没有实现OntouchEvent方法,因此无法响应内部事件。这里可以通过拦截事件,然后发通知的方式实现它的点击事件,具体见上面的代码注释,比较容易理解。
参考资料
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END