目录
- 是什么?为什么要学习呢?
- 了解一下,事件分发是如何从点击到View这边,会经历那些类
- 事件分发,有那些事件?
- 我们写一个自定义ViewGroup和自定义View,真实了解一下事件的流程是怎么样的。
- dispatchTouchEvent方法的源码介绍:了解这个以后,有助于我们后面解决事件冲突
- 如何解决事件冲突问题?
一、是什么?为什么要学习呢?
事件分发机制指的是: Android 系统中处理用户触摸、点击、滑动等输入事件的核心流程,定义了事件如何从系统层传递到应用中的各个组件(如 Activity、ViewGroup、View),以及如何处理事件的消费、拦截和传递。
了解这个以后,当我们使用嵌套控件出现事件冲突的时候,就可以运用这个知识去解决;还可以实现一些自定义复杂的交互,并且事件分发机制也是常见面试题。
二、了解一下,事件分发是如何从点击到View这边,会经历哪些类?
事件的传递起点是硬件层(触摸屏驱动),经过系统服务处理后到达应用进程。大概介绍一下会经过哪些类和方法,方便我们后面知道如何对事件进行干预。
简单总结,就是会经过这些类,最终到达我们的view:
- ViewRootImpl → DecorView → Activity → PhoneWindow → ViewGroup → View
接下来我们会重点介绍ViewGroup 和 View。
ViewGroup可以决定是否拦截事件,分发给子view,View最终处理事件的逻辑,因为view下面就不会再有子view了。如果他不处理,就会返回给ViewGroup来处理,如果他也不处理,那么就都不处理。
三、事件分发,有那些事件?
| 事件类型 | 触发场景 | 关键作用 |
|---|---|---|
ACTION_DOWN | 手指首次触摸屏幕时触发 | 事件的起点,若某个 View 未消费此事件,后续事件(如 MOVE、UP)不会传递给它。 |
ACTION_MOVE | 手指在屏幕上滑动时触发(可能连续多次) | 处理滑动、拖拽等连续交互行为。 |
ACTION_UP | 手指离开屏幕时触发 | 事件的终点,通常与 ACTION_DOWN 配对使用,完成点击或滑动逻辑。 |
ACTION_CANCEL | 事件被上层拦截时触发,如父容器(如 ViewGroup)通过 onInterceptTouchEvent 拦截事件时,子 View 会收到此事件。 | 通知子 View 停止处理当前事件,恢复初始状态(如取消高亮)。 |
一次完整的事件,ACTION_DOWN和ACTION_UP,只会发生一次,不管是单指,还是多指。而ACTION_MOVE可能会有0次,或者多次。后面,我们会写一个例子看看。
四、接下来,我们写一个自定义ViewGroup和自定义View,真实了解一下事件的流程是怎么样的。
4.1 案例
(1)MyFrameLayout是 ViewGroup,MyButton是子view
<?xml version="1.0" encoding="utf-8"?>
<com.example.rxjavademo2.MyFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:context=".MainActivity2">
<com.example.rxjavademo2.MyButton
android:layout_width="200dp"
android:background="#D01F1F"
android:layout_height="200dp"
android:text="hello Button"/>
</com.example.rxjavademo2.MyFrameLayout>
public class MyFrameLayout extends FrameLayout {
private static final String TAG = "MyFrameLayout";
public MyFrameLayout(Context context) {
super(context);
}
public MyFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent: 父容器");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: 父容器");
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent: 父容器");
return super.dispatchTouchEvent(ev);
}
}
@SuppressLint("AppCompatCustomView")
public class MyButton extends Button {
private static final String TAG = "MyButton";
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "dispatchTouchEvent: 子view");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent: ACTION_DOWN 子view");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onTouchEvent: ACTION_MOVE 子view");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouchEvent: ACTION_UP 子view");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "onTouchEvent: ACTION_CANCEL 子view");
break;
}
return super.onTouchEvent(event);
}
}
public class MainActivity2 extends AppCompatActivity {
private static final String TAG = "MyMainActivity2";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main2);
MyButton myButton = findViewById(R.id.mybutton);
myButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "onClick: ");
}
});
myButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "onTouch: "+event.getAction());
return false;
}
});
}
}
(2)接下来,我们点击一下控件,看会经历哪些。
当我们点击的时候,就是按下和抬起,每一次事件,父容器都会经过dispatchTouchEvent、onInterceptTouchEvent。子view是处理的,所以他会经过dispatchTouchEvent、onTouchEvent,然后具体的事件。我们会发现父容器的onTouchEvent不会执行,因为他交给子view来处理事件了,一个事件只能有一个view来消费。
每一个事件都要经过父容器到子view,都是从上往下,上指的是父容器,下指的是子view。
了解了这个以后,我们后面就知道,如果我不想子view处理事件,就可以拦截的。后续我们会讲到。
(3)我们在看看ACTION_MOVE,我们点击的时候滑动一下。
之前我们就说过ACTION_MOVE会经历多次。
4.2 三个方法
1. dispatchTouchEvent(MotionEvent ev)
- 事件分发的入口方法,所有触摸事件都会首先进入该方法。
- 决定事件是否继续传递,或由当前 View/ViewGroup 消费。
2. onInterceptTouchEvent(MotionEvent ev)
- 仅存在于
ViewGroup中,用于判断是否拦截事件。 - 默认不拦截(返回
false),子 View 优先处理事件。
3. onTouchEvent(MotionEvent event)
- 最终处理事件的方法,用于响应点击、触摸等操作。
- 在
View和ViewGroup中均可重写。
五、dispatchTouchEvent方法的源码介绍:了解这个以后,有助于我们后面解决事件冲突
5.1 ViewGroup的dispatchTouchEvent
ViewGroup的dispatchTouchEvent我们可以简单分为三个部分
(1)拦截检查
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
// 🌟1. 检查是否需要拦截事件(调用 onInterceptTouchEvent)
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 🌟允许子 View 通过 requestDisallowInterceptTouchEvent 禁止父容器拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev); // 关键拦截方法
ev.setAction(action); // 恢复事件 Action(防止被修改)
} else {
intercepted = false;
}
} else {
intercepted = true; // 非 DOWN 事件且无目标 View,直接拦截
}
...
}
这里主要是检查,父容器是否拦截事件,如果拦截,就不会再子view中寻找可接收事件的,直接就执行的onTouchEvent方法
(2)子view的分发逻辑
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
// 1. 检查是否需要拦截事件(调用 onInterceptTouchEvent)
// 🌟2. 若未拦截,寻找可接收事件的子 View
if (!canceled && !intercepted) {
// 🌟遍历子 View,找到能处理事件的子 View(mFirstTouchTarget)
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.dispatchTouchEvent(ev)) { // 递归调用子 View 的分发
// 找到目标子 View,记录到 mFirstTouchTarget 链表
break;
}
}
}
.....
}
这里的逻辑就是寻找可接收事件的子 View,然后记录起来
(3)分发事件给目标子 View 或自身处理
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
// 1. 检查是否需要拦截事件(调用 onInterceptTouchEvent)
// 2. 若未拦截,寻找可接收事件的子 View
// 🌟3. 分发事件给目标子 View 或自身处理
if (mFirstTouchTarget == null) {
// 🌟没有子 View 消费事件,调用自身 onTouchEvent
handled = super.dispatchTouchEvent(ev);
} else {
// 🌟将事件分发给已找到的子 View(通过链表传递)
// 处理多点触控和后续事件(如 MOVE、UP)
}
}
return handled;
}
若子 View 消费了 ACTION_DOWN 事件,会被记录到 mFirstTouchTarget,后续事件(如 MOVE、UP)直接分发给它。
5.2 View的dispatchTouchEvent
View 的 dispatchTouchEvent 主要处理自身的事件分发,没有子 View 参与。
(1) 优先处理 OnTouchListener
public boolean dispatchTouchEvent(MotionEvent event) {
// 1. 检查是否设置了 OnTouchListener,并优先调用 onTouch()
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true; // OnTouchListener 消费事件
}
// 2. 若未消费,调用 onTouchEvent()
if (onTouchEvent(event)) {
return true; // 自身消费事件
}
return false; // 未消费,事件回传给父容器
}
(1) onTouchEvent
在 View.onTouchEvent 中处理具体的事件,如点击、长按等逻辑:
public boolean onTouchEvent(MotionEvent event) {
// 检查是否可点击(如 Button 默认可点击)
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
performClick(); // 触发点击事件
break;
case MotionEvent.ACTION_DOWN:
// 处理按下状态(如背景变化)
break;
// 其他事件处理...
}
return true; // 可点击 View 默认消费事件
}
return false;
}
5.4 接下来,我们看看三个面试题:事件冲突的
比如ViewPage+ListView结合使用,那么肯定会出现事件冲突,ViewPage是左右滑动,ListView是上下滑动,如下三种情况,属于哪种滑动?
(1)父容器onInterceptTouchEvent 返回 true 时 ,只能如何滑动?
结论,只能左右滑动,父容器消费事件。
因为onInterceptTouchEvent 返回 true,则 intercepted = true,后续流程直接跳过分发子 View。- 不再遍历子 View,直接调用 super.dispatchTouchEvent(ev)(即 View.dispatchTouchEvent),事件由父容器的 onTouchEvent 处理。
(2)父容器onInterceptTouchEvent 返回 false 时,只能如何滑动?
结论,只能上下滑动,子view消费事件。
不拦截以后,会遍历子 View,找到消费 ACTION_DOWN 的 View,并记录到 mFirstTouchTarget。后续事件直接分发给 mFirstTouchTarget 对应的子 View。
(3)父容器 onInterceptTouchEvent 返回 false,子 View dispatchTouchEvent 返回 false,只能如何滑动?
结论,左右滑动,父容器消费事件【因为我们这里用的是viewpager,他是消费了,如果是其他的,可能不消费,直接返回给activity,也就是不消费事件,因为找不到消费事件的view】
- 若子 View 的
dispatchTouchEvent返回false,父容器调用自身onTouchEvent。父容器可能在onTouchEvent中根据滑动方向处理事件。
六、如何解决事件冲突问题?
比如ViewPage+ListView结合使用,解决冲突就是:当用户上下滑动的时候,把时间给Listview处理,左右滑动,给ViewPager处理。
两个问题:
- 如何判断是上下滑动,还是左右滑动,
- 如何将事件给到其他view呢?
两种方法:
- 外部拦截法:
- 内部拦截法:
6.1 外部拦截法
由 父容器(ViewPager) 决定是否拦截事件,重写 onInterceptTouchEvent。
public class CustomViewPager extends ViewPager {
private float mStartX, mStartY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartX = ev.getX();
mStartY = ev.getY();
// 不拦截 DOWN 事件,保证子 View 能接收后续事件
return false;
case MotionEvent.ACTION_MOVE:
float dx = ev.getX() - mStartX;
float dy = ev.getY() - mStartY;
if (Math.abs(dx) > Math.abs(dy)) {
// 水平滑动,拦截事件,由 ViewPager 处理
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
}
我们知道move事件是会有多次的,所以当我们在move检测到是左右滑动时,拦截事件不让子view处理。那么就会调用自身的onTouchEvent.
外部拦截法的优点就是,逻辑简单,直接在父容器处理拦截,子 View(ListView)无需修改。
6.2 内部拦截法(子 View 请求父容器不要拦截)
由 子 View(ListView) 通过 requestDisallowInterceptTouchEvent 动态请求父容器不拦截。
-
父容器(ViewPager)默认不拦截事件:
public class CustomViewPager extends ViewPager { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // 默认不拦截,除非子 View 允许 return ev.getAction() == MotionEvent.ACTION_MOVE; } }
那么为什么不直接返回false,因为如果直接返回false,那么父容器就会一直给子view发,子view又允许父容器,但父容器就得不到执行。
-
子 View(ListView)动态控制拦截:
public class CustomListView extends ListView { private float mLastX, mLastY; @Override public boolean dispatchTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 禁止父容器拦截 DOWN 事件 getParent().requestDisallowInterceptTouchEvent(true); mLastX = x; mLastY = y; break; case MotionEvent.ACTION_MOVE: float dx = x - mLastX; float dy = y - mLastY; if (Math.abs(dx) > Math.abs(dy)) { // 水平滑动时,允许父容器拦截 getParent().requestDisallowInterceptTouchEvent(false); } mLastX = x; mLastY = y; break; } return super.dispatchTouchEvent(event); } }需要 在
MOVE时根据滑动方向动态调整父容器的拦截权限,当子 View 未禁止父容器拦截时(如水平滑动),父容器仍能正常处理滑动切换,并且不给子view分发事件。