跳到主要内容

事件分发机制

问题

Android 的事件分发机制是怎样的?dispatchTouchEventonInterceptTouchEventonTouchEvent 三者的关系是什么?

答案

1. 事件分发总体流程

事件从 Activity → Window → DecorView → ViewGroup → View 自顶向下传递,如果没有被消费,则自底向上回传

2. 三个核心方法

方法所在类返回值含义
dispatchTouchEvent()Activity / ViewGroup / Viewtrue = 事件被消费
onInterceptTouchEvent()仅 ViewGrouptrue = 拦截,不传给子 View
onTouchEvent()Activity / ViewGroup / Viewtrue = 消费事件

伪代码描述 ViewGroup 分发逻辑:

// ViewGroup.dispatchTouchEvent 伪代码
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
var handled = false

if (onInterceptTouchEvent(event)) {
// 拦截 → 自己处理
handled = onTouchEvent(event)
} else {
// 不拦截 → 传给子 View
handled = child.dispatchTouchEvent(event)
if (!handled) {
// 子 View 不消费 → 自己处理
handled = onTouchEvent(event)
}
}

return handled
}

3. ACTION_DOWN 的特殊性

关键知识点

ACTION_DOWN 是事件序列的起点。如果一个 View 在 ACTION_DOWN 时返回 false(不消费),后续的 ACTION_MOVEACTION_UP不会再传递给它。

一次完整的事件序列:ACTION_DOWN → ACTION_MOVE → ... → ACTION_MOVE → ACTION_UP

4. 冲突处理

外部拦截法(推荐)

父 ViewGrouponInterceptTouchEvent 中判断是否拦截:

class ParentLayout : FrameLayout {

private var lastX = 0f
private var lastY = 0f

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
lastX = ev.x
lastY = ev.y
return false // DOWN 不拦截,否则子 View 收不到事件
}
MotionEvent.ACTION_MOVE -> {
val dx = ev.x - lastX
val dy = ev.y - lastY
// 水平滑动距离 > 垂直滑动距离 → 拦截(父 View 处理水平滚动)
return abs(dx) > abs(dy)
}
}
return false
}
}

内部拦截法

子 View 中通过 requestDisallowInterceptTouchEvent 控制父 View 的拦截行为:

class ChildView : View {

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
// 禁止父 View 拦截
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
if (needParentScroll()) {
// 让父 View 拦截处理
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
return super.dispatchTouchEvent(ev)
}
}

5. 常见冲突场景

场景解决方案
ScrollView 嵌套 RecyclerViewRecyclerView.isNestedScrollingEnabled = false 或用 NestedScrollView
ViewPager2 嵌套 RecyclerView方向不同时自动处理;方向相同需外部拦截法
下拉刷新 + 列表滚动SwipeRefreshLayout 已内置处理
侧滑删除 + 列表滚动外部拦截法,根据滑动方向判断

常见面试问题

Q1: onTouchListeneronTouchEventonClickListener 的执行顺序?

答案

View.dispatchTouchEvent() 中的优先级:

  1. OnTouchListener.onTouch():最先调用。如果返回 true,不再执行后续
  2. onTouchEvent():onTouch 返回 false 时调用
  3. OnClickListener.onClick():在 onTouchEventACTION_UP 中触发
onTouch(DOWN) → onTouchEvent(DOWN) → onTouch(UP) → onTouchEvent(UP) → onClick

Q2: 如果一个 View 设置了 clickable = true 但没有设置 OnClickListener,事件会被消费吗?

答案

onTouchEvent 中只要 clickable || longClickable || contextClickable 为 true,就返回 true(消费事件),无论是否设置了监听器。这也是为什么 Button 默认消费事件(clickable = true),而 TextView 默认不消费。

Q3: requestDisallowInterceptTouchEvent 的原理是什么?

答案

调用此方法会设置 ViewGroup 的 FLAG_DISALLOW_INTERCEPT 标志位。当此标志为 true 时,dispatchTouchEvent 会跳过 onInterceptTouchEvent 的调用,直接传递给子 View。

注意:ACTION_DOWN 时会重置这个标志,所以正确的做法是在子 View 的 ACTION_DOWN 事件时调用 requestDisallowInterceptTouchEvent(true)

Q4: 多指触控事件是如何分发的?

答案

  • ACTION_POINTER_DOWN:第二个及以后手指按下
  • ACTION_POINTER_UP:非最后一个手指抬起
  • 通过 event.getPointerId(pointerIndex) 获取手指 ID
  • 通过 event.findPointerIndex(pointerId) 获取手指索引

每个手指有唯一的 pointerId,但 pointerIndex 可能变化。追踪手指应该用 pointerId

Q5: NestedScrolling 机制和传统事件分发有什么区别?

答案

传统事件分发是二择一的:要么父 View 拦截处理,要么子 View 处理。

NestedScrolling 允许父子协同滚动

  1. 子 View 滚动前先问父 View 要不要消费一部分(onNestedPreScroll
  2. 子 View 滚动自己的部分
  3. 子 View 滚动后剩余的再给父 View(onNestedScroll

RecyclerView 默认实现了 NestedScrollingChild,CoordinatorLayout 实现了 NestedScrollingParent

相关链接