Android滑动冲突:事件分发与嵌套滑动的深度解析

376 阅读2分钟

一、滑动冲突的根源与核心场景

滑动冲突的根本原因,是父容器子 View都想要处理同一个触摸事件。

1. 父子滑动方向不同

  • 场景ViewPager(横向)嵌套 RecyclerView(纵向)。
  • 表现:当用户进行斜向滑动时,系统无法确定是应该由 ViewPager 还是 RecyclerView 来处理事件。

2. 父子滑动方向相同

  • 场景RecyclerView 嵌套另一个 RecyclerView(均垂直滑动)。
  • 表现:父容器和子 View 争抢事件,导致滑动不流畅。

二、解决方案:外部拦截法与内部拦截法

解决事件冲突的本质,是建立一个清晰的规则,明确在什么条件下由谁来处理事件

1. 外部拦截法(父容器主导)

  • 核心思想:由父容器通过重写 onInterceptTouchEvent() 方法,来决定是否拦截事件。

  • 工作原理

    • ACTION_DOWN 时,父容器不拦截,让子 View 能够正常接收事件。
    • ACTION_MOVE 时,父容器根据滑动方向、滑动距离等条件,判断是否应该由自己处理事件。
    • 如果条件满足,onInterceptTouchEvent() 返回 true,父容器拦截事件,并向子 View 发送一个 ACTION_CANCEL 事件,从而接管控制权。
  • 代码示例

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        // 在 ACTION_MOVE 中判断滑动方向,决定是否拦截
        if (ev.action == MotionEvent.ACTION_MOVE) {
            val dx = abs(ev.x - startX)
            val dy = abs(ev.y - startY)
            if (dx > dy && dx > scaledTouchSlop) { // 横向滑动距离更大,且超过最小滑动阈值
                return true // 拦截,自己处理
            }
        }
        return false // 默认不拦截
    }
    

2. 内部拦截法(子View协商)

  • 核心思想:子 View 通过 requestDisallowInterceptTouchEvent() 方法,来请求父容器不要拦截事件。

  • 工作原理

    • 子 View 在 ACTION_DOWN 时,调用 parent.requestDisallowInterceptTouchEvent(true),告诉父容器“我现在要处理事件了,请不要拦截”。
    • 当子 View 不再需要事件时(如滚动到顶部/底部),再次调用 parent.requestDisallowInterceptTouchEvent(false),将控制权交还给父容器。
  • 代码示例

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        if (ev.action == MotionEvent.ACTION_DOWN) {
            parent.requestDisallowInterceptTouchEvent(true) // 请求父容器不要拦截
        }
        // ...
        // 当滚动到底部时,将控制权交还给父容器
        if (!canScrollVertically(1)) {
            parent.requestDisallowInterceptTouchEvent(false)
        }
        return super.dispatchTouchEvent(ev)
    }
    

三、系统级解决方案:NestedScrolling机制

NestedScrolling是Android 5.0引入的官方机制,用于实现父子View之间的协同滑动,解决了传统事件分发机制的局限性。

  • 核心接口

    • NestedScrollingParent3:父容器需要实现此接口。
    • NestedScrollingChild3:子 View 需要实现此接口(如RecyclerView)。
  • 工作原理

    1. 预滑动:子 View 在滑动前,会通过 dispatchNestedPreScroll() 方法将滑动距离传递给父容器。父容器可以在此阶段“预消费”一部分滑动。
    2. 子View消费:子 View 处理剩余的滑动距离。
    3. 后滑动:子 View 滑动完成后,会通过 dispatchNestedScroll() 方法将未消费的滑动距离传递给父容器。