使用场景及产生问题
当一个列表中还需一个与外层列表滑动方向一致的列表时,当想滑动里层列表时,手势会默认被外层RecyclerView拦截处理,导致里层列表无法滑动。 
解决方案
思路一
既然外层会优先拦截Touch事件并直接处理,要让里层能获取到Touch事件,则让外层RecyclerView获取Touch时,判断位置是否在里层RecyclerView上,如果在,则外层不处理,交给里层处理。
实现步骤:
- 自定义外层RecyclerView,重写onInterceptTouchEvent方法,在里面处理touch事件
- 当touch事件位置在里层RecyclerView上,则不拦截touch事件,直接分发 关键代码如下
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
val manager = layoutManager as LinearLayoutManager
//获取第一个可见的item位置
val firstVisiblePosition = manager.findFirstVisibleItemPosition()
if (firstVisiblePosition == 0){
//获取view相对父布局的底边,及可见高度
val height = getChildAt(firstVisiblePosition).bottom
//如果touch事件位置在需要处理的item中(即在里层RecyclerView上),则需要处理touch事件,不进行拦截,直接分发
if (e.y <= height){
return false
}
}
return super.onInterceptTouchEvent(e)
}
该方案需要在外层RecyclerView处理获取里层RecyclerView的位置信息,此处由于里层RecyclerView位置在第一个,处理相对简单,如果在任意位置,在onInterceptTouchEvent处理时获取里层RecyclerView位置时会比较麻烦,不建议采取此方案
思路二
直接处理里层RecyclerView,如果touch在自身位置,则让父容器不进行拦截,这样无需判断位置,如果自身能接受touch事件,则必定触摸区域在里层RecyclerView所在位置上
实现步骤:
自定义里层RecyclerView,重写onInterceptTouchEvent方法,通知不容器不进行拦截 关键代码如下
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
//让父布局不拦截
parent.requestDisallowInterceptTouchEvent(true)
return super.onInterceptTouchEvent(e)
}
可以看出,效果和思路一基本一致,只是处理事件分发更简单,无需计算相关位置信息,而且无论里层RecyclerView在哪个位置,都能进行正常滑动。
上面两种方案都能基本解决同向滑动双RecyclerView嵌套问题,但是事件处理则分开了,在里层RecyclerView位置仅能滑动里层,外层区域仅能滑动外层,而我们期望的是,在里层RecyclerView滑动到底部后,外层RecyclerView能继续滑动,无需松开手指二次滑动外层(里层滑动到顶部时也是如此,继续滑动时外层RecyclerView下滑)
思路三
我们在思路二知道,touch处理时直接让父容器不进行任何拦截,简单粗暴。那我们可以处理为,如果是向下滑时,已经在顶部了,那无需让父容器进行分发了(向上滑也是如此)。那我们在思路二基础上继续进行优化
实现步骤:
实现步骤:
- 自定义里层RecyclerView,重写onInterceptTouchEvent方法,在里面处理touch事件
- 在DOWN事件处理让父容器不进行拦截(这样才能接收到后续事件,如MOVE)
- 判断滑动方向,并且是否自身已滑动到边界(底部或者顶部),如果已经滑动到了边界,则无需自身处理手势,让父容器继续拦截
class CustomRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
//记录上次手指位置
private var mLastY = 0f
//是否已经滑到了底部
private var isToBottom = false
//是否已经滑到了顶部
private var isToTop = true
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//记录按下位置
mLastY = event.y
//如果手指按下触摸区域在自身,先不允许父View拦截事件
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
checkPosition(event.y)
if (isToBottom || isToTop) {
//已经滑动到顶部或者底部时,不需要自己处理手势,无需下发
parent.requestDisallowInterceptTouchEvent(false)
return false
} else {
parent.requestDisallowInterceptTouchEvent(true)
}
mLastY = event.y
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent(
false
)
}
return super.dispatchTouchEvent(event)
}
/**
* 判断item的位置情况,确认是否需要滑动
*/
private fun checkPosition(nowY: Float) {
//暂时仅处理LinearLayoutManager情况
val manager = layoutManager as LinearLayoutManager
isToTop = false
isToBottom = false
//获取可见的item位置
val firstVisiblePosition = manager.findFirstCompletelyVisibleItemPosition()
val lastVisiblePosition = manager.findLastCompletelyVisibleItemPosition()
//如果当前有item显示
if (layoutManager!!.childCount > 0) {
if (lastVisiblePosition == manager.itemCount - 1) {
//检查是否能向上滑,且滑动方向是向上
if (canScrollVertically(-1) && nowY < mLastY) {
//标记已经滑动到了底部,不能再向上滑动了
isToBottom = true
}
} else if (firstVisiblePosition == 0) {
//检查是否能向下滑,且滑动方向是向下
if (canScrollVertically(1) && nowY > mLastY) {
//标记已经滑动到了顶部,不能再向下滑动了
isToTop = true
}
}
}
}
}
可看出,当里层RecyclerView滑动到顶部或者底部时,手指不松开继续滑动时,外层列表会触发同向滑动,符合我们预期效果