Android 的 View 事件分发机制,主要指 触摸事件 MotionEvent 从系统传递到 Activity、ViewGroup、View 的过程。
核心方法有三个:
dispatchTouchEvent(MotionEvent ev)
onInterceptTouchEvent(MotionEvent ev)
onTouchEvent(MotionEvent ev)
其中:
| 方法 | 所属对象 | 作用 |
|---|---|---|
dispatchTouchEvent() |
Activity / ViewGroup / View | 分发事件 |
onInterceptTouchEvent() |
ViewGroup | 判断是否拦截事件 |
onTouchEvent() |
View | 处理事件 |
一、事件分发整体流程
触摸事件大致流程如下:
Activity
↓
Window
↓
DecorView
↓
ViewGroup
↓
子 View
通常可以简化理解为:
Activity.dispatchTouchEvent()
↓
ViewGroup.dispatchTouchEvent()
↓
ViewGroup.onInterceptTouchEvent()
↓
View.dispatchTouchEvent()
↓
View.onTouchEvent()
二、Activity 的事件分发
当用户触摸屏幕时,事件首先到达 Activity:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
含义是:
- 先交给 Window / DecorView 向下分发;
- 如果下面的 View 消费了事件,返回
true; - 如果没人处理,Activity 自己的
onTouchEvent()处理。
三、ViewGroup 的事件分发
ViewGroup 比普通 View 多了一个方法:
onInterceptTouchEvent()
它可以决定是否拦截事件。
简化逻辑如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean intercepted = onInterceptTouchEvent(ev);
if (!intercepted) {
// 不拦截,分发给子 View
child.dispatchTouchEvent(ev);
} else {
// 拦截,自己处理
onTouchEvent(ev);
}
}
四、View 的事件分发
普通 View 没有子 View,所以它的分发比较简单:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onTouchListener != null && onTouchListener.onTouch(this, ev)) {
return true;
}
return onTouchEvent(ev);
}
也就是说:
- 先执行
OnTouchListener.onTouch(); - 如果
onTouch()返回true,事件被消费; - 如果返回
false,继续执行onTouchEvent(); onTouchEvent()中可能触发onClick()。
五、三个返回值的含义
1. dispatchTouchEvent()
return true;
表示事件被当前对象消费,不再继续传递。
return false;
表示当前对象不处理,交回上层处理。
return super.dispatchTouchEvent(ev);
表示按照系统默认逻辑继续分发。
2. onInterceptTouchEvent()
只存在于 ViewGroup。
return true;
表示拦截事件,事件不再传给子 View,由当前 ViewGroup 的 onTouchEvent() 处理。
return false;
表示不拦截,事件继续传给子 View。
return super.onInterceptTouchEvent(ev);
默认一般是不拦截。
3. onTouchEvent()
return true;
表示当前 View 消费该事件。
return false;
表示当前 View 不消费该事件,交给父 View 处理。
六、事件序列
一次完整点击事件通常包含:
ACTION_DOWN
ACTION_MOVE
ACTION_UP
例如:
手指按下 → ACTION_DOWN
手指移动 → ACTION_MOVE
手指抬起 → ACTION_UP
Android 的事件分发是以 事件序列 为单位的。
重要规则
如果一个 View 在 ACTION_DOWN 中返回了 false,那么后续的 ACTION_MOVE 和 ACTION_UP 通常不会再分发给它。
例如:
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
这种情况下,该 View 后续收不到 MOVE 和 UP。
七、点击事件 onClick 的触发条件
onClick() 是在 onTouchEvent() 中触发的。
大致条件是:
- View 是可点击的;
- 收到了
ACTION_DOWN; - 收到了
ACTION_UP; - 手指没有明显移出 View 范围;
- 事件没有被父 View 拦截。
执行顺序通常是:
onTouch()
↓
onTouchEvent()
↓
onClick()
如果 onTouch() 返回 true,则 onClick() 不会执行。
例如:
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
这里返回 true,表示事件被 onTouch() 消费,所以不会触发 onClick()。
如果返回 false:
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
事件继续交给 onTouchEvent(),如果满足条件,会触发 onClick()。
八、ViewGroup 拦截示例
例如自定义 ViewGroup:
public class MyLayout extends ViewGroup {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
}
含义:
ACTION_DOWN 不拦截,子 View 可以收到
ACTION_MOVE 拦截,后续事件交给 MyLayout 处理
如果父 ViewGroup 在事件中途拦截,子 View 会收到一个:
ACTION_CANCEL
表示事件被取消。
九、父子 View 冲突处理
常见场景:
ScrollView中嵌套RecyclerView- 横向滑动 ViewPager 嵌套纵向列表
- 自定义滑动控件嵌套点击控件
解决方式通常有两种。
方式一:外部拦截法
父 ViewGroup 根据情况决定是否拦截:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false;
case MotionEvent.ACTION_MOVE:
if (需要父容器处理) {
return true;
}
return false;
case MotionEvent.ACTION_UP:
return false;
}
return false;
}
特点:
判断逻辑在父 ViewGroup
方式二:内部拦截法
子 View 请求父 View 不要拦截:
getParent().requestDisallowInterceptTouchEvent(true);
示例:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (需要父容器处理) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(ev);
}
注意:
requestDisallowInterceptTouchEvent(true) 对 ACTION_DOWN 通常无效,因为 ACTION_DOWN 一定会经过父 ViewGroup。
十、常见结论
1. 事件从外向内分发
Activity → ViewGroup → View
2. 事件从内向外消费
View → ViewGroup → Activity
3. ViewGroup 可以拦截事件
onInterceptTouchEvent()
4. View 没有拦截方法
普通 View 没有:
onInterceptTouchEvent()
5. onTouch() 优先于 onClick()
顺序是:
dispatchTouchEvent()
↓
onTouch()
↓
onTouchEvent()
↓
onClick()
6. ACTION_DOWN 很关键
如果某个 View 不消费 ACTION_DOWN,通常后续事件不会再给它。
十一、简单记忆口诀
分发 dispatchTouchEvent
拦截 onInterceptTouchEvent
消费 onTouchEvent
Activity 先分发
ViewGroup 可拦截
View 只消费
DOWN 决定归属
MOVE 可能拦截
UP 触发点击
总结一句话:
Android 触摸事件从 Activity 开始向下分发,经过 ViewGroup 判断是否拦截,最终交给目标 View 处理;如果子 View 不处理,事件会向父容器回传,由父 ViewGroup 或 Activity 处理。