RecyclerView双层嵌套解决滑动问题

4,356

使用场景及产生问题

当一个列表中还需一个与外层列表滑动方向一致的列表时,当想滑动里层列表时,手势会默认被外层RecyclerView拦截处理,导致里层列表无法滑动。 ![image](D:/Program Files/LICEcap/1.gif)

解决方案

思路一

既然外层会优先拦截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滑动到顶部或者底部时,手指不松开继续滑动时,外层列表会触发同向滑动,符合我们预期效果