事件分发机制
问题
Android 的事件分发机制是怎样的?dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 三者的关系是什么?
答案
1. 事件分发总体流程
事件从 Activity → Window → DecorView → ViewGroup → View 自顶向下传递,如果没有被消费,则自底向上回传。
2. 三个核心方法
| 方法 | 所在类 | 返回值含义 |
|---|---|---|
dispatchTouchEvent() | Activity / ViewGroup / View | true = 事件被消费 |
onInterceptTouchEvent() | 仅 ViewGroup | true = 拦截,不传给子 View |
onTouchEvent() | Activity / ViewGroup / View | true = 消费事件 |
伪代码描述 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_MOVE、ACTION_UP 都不会再传递给它。
一次完整的事件序列:ACTION_DOWN → ACTION_MOVE → ... → ACTION_MOVE → ACTION_UP
4. 冲突处理
外部拦截法(推荐)
在父 ViewGroup 的 onInterceptTouchEvent 中判断是否拦截:
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 嵌套 RecyclerView | RecyclerView.isNestedScrollingEnabled = false 或用 NestedScrollView |
| ViewPager2 嵌套 RecyclerView | 方向不同时自动处理;方向相同需外部拦截法 |
| 下拉刷新 + 列表滚动 | SwipeRefreshLayout 已内置处理 |
| 侧滑删除 + 列表滚动 | 外部拦截法,根据滑动方向判断 |
常见面试问题
Q1: onTouchListener、onTouchEvent、onClickListener 的执行顺序?
答案:
在 View.dispatchTouchEvent() 中的优先级:
OnTouchListener.onTouch():最先调用。如果返回 true,不再执行后续onTouchEvent():onTouch 返回 false 时调用OnClickListener.onClick():在onTouchEvent的ACTION_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 允许父子协同滚动:
- 子 View 滚动前先问父 View 要不要消费一部分(
onNestedPreScroll) - 子 View 滚动自己的部分
- 子 View 滚动后剩余的再给父 View(
onNestedScroll)
RecyclerView 默认实现了 NestedScrollingChild,CoordinatorLayout 实现了 NestedScrollingParent。