View 事件分发的那些问题

答案参考自:


View事件分发机制

答案参考自:

以下内容原文链接:言简意赅的View分发机制_view事件分发机制_兜哥的博客-CSDN博客

当一个 Touch 事件来到时,首先 ActivitydispatchTouchEvent 方法会被触发,如果在 Activity 中没有重写该方法,那么这个方法最终会走到 ViewGroup#dispatchTouchEvent 中进行分发处理:

  1. 首先会判断是否已经有子 View 拦截过该方法。

  2. 如果已有子 View 拦截了该事件,则后续的 moveup 事件都由这个 ViewGroup 处理,他的 onTouchEvent 被调用。

  3. 如果该事件没有被拦截过,他就会遍历他下面的子 View,然后调用子 ViewdispatchTouchEvent 方法,若返回了 true,代表该 View 处理了事件,并将 mFirstTouchTarget 赋值为空,若为 false 则继续遍历分发(若还有子 View 的话)。

  4. 若遍历了所有的子 View,都没有被合适处理,则根 View 会自己来处理,ActivityonTouchEvent 会被触发。

事件是先到 DecorView 还是先到 Window

由上述流程图中可以得知,事件的分发顺序为

Activity -> Window -> DecorView -> ViewGroup -> View

ViewonTouchEventOnClickListernerOnTouchListeneronTouch方法的三者优先级

答案参考自:

点击事件的执行顺序为

OnTouchListener.DOWN -> OnTouchEvent.DOWN -> OnTouchListener.MOVE -> OnTouchEvent.MOVE -> OnTouchListener.UP -> OnTouchEvent.UP -> OnClickListener

所以三者的优先级为

OnTouchListener > onTouchEvent > onClick

onTouchonTouchEvent 的区别

答案参考自:

  1. onTouchListeneronTouch方法优先级比onTouchEvent高,会先触发。

  2. 假如onTouch方法返回false会接着触发onTouchEvent,反之onTouchEvent方法不会被调用。

  3. 内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发。

ActivityViewGroupView都不消费ACTION_DOWN,那么ACTION_UP事件是怎么传递的

首先,如果大家都不消费 ACTION_DOWN,那么 ACTION_DOWN 的事件传递流程是这样的:

1
2
3
4
5
6
7
-> Activity.dispatchTouchEvent() 
-> ViewGroup1.dispatchTouchEvent()
-> ViewGroup1.onInterceptTouchEvent()
-> view1.dispatchTouchEvent()
-> view1.onTouchEvent()
-> ViewGroup1.onTouchEvent()
-> Activity.onTouchEvent();

接着,由于大家都不消费 ACTION_DOWN,对于 ACTION_MOVEACTION_UP 的事件传递是这样的:

1
2
3
-> Activity.dispatchTouchEvent()
-> Activity.onTouchEvent();
-> 消费

点击事件被拦截,但是想传到下面的View,如何操作

1
getParent().requestDisallowInterceptTouchEvent(true);

可将点击事件传到下面的View, 剥夺了父View 对除了ACTION_DOWN以外的事件的处理权。

如何解决View的事件冲突

三种出现滑动冲突的情况

  1. 内部View外部View的滑动方向相反。
  2. 内部View外部View的滑动方向相同。
  3. 前两种情况的嵌套。

三种情况的处理思路:

  1. 通过手势滑动的角度判断滑动的方向。
  2. 通过当前处于的不同的页面状态来判断应该滑动的View
  3. 通过前两种的综合使用。

滑动冲突的解决方式

外部拦截法

通过重写父容器的onInterceptTouchEvent方法,所有的事件都先经过父容器的筛选,对其中父容器需要的事件进行拦截。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN : {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE : {
if (父容器需要当前的点击事件)
intercepted = true;
else
intercepted = false;
break;
}
case MotionEvent.ACTION_UP : {
intercepted = false;
break;
}
default :
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}

内部拦截法

父元素拦截除ACTION_DOWN以外的其他事件,当事件到达子元素后,由子元素判断是否需要这些事件,不需要的事件将重新交由父容器来处理。这种方法和Android的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常的工作。

相关阅读链接:Android TouchEvent之requestDisallowInterceptTouchEvent - 简书 (jianshu.com)

子元素的dispatchTouchEvent方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN : {
parent.requestDisallowInterceptTouchEvent(true); // 此子View的所有父ViewGroup会跳过onInterceptTouchEvent回调
break;
}
case MotionEvent.ACTION_MOVE : {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要当前的点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP : {
break;
}
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}

父容器的onInterceptTouchEvent方法:

1
2
3
4
5
6
7
8
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}

requestDisallowInterceptTouchEvent的调用时机

parent.requestDisallowInterceptTouchEvent的调用需要写在onTouchEvent方法中

我们一个手势的操作,会经历ACTION_DOWNACTION_MOVEACTION_UP等操作。

view调用requestDisallowInterceptTouchEvent(true)的时间,是必须在能拿到点击事件的时候。

比如我们在ACTION_DOWN的时候调用了方法,接下来的ACTION_MOVEACTION_UP都会直接传递到子view上了;如果是在子viewACTION_MOVE方法中调用的话,那么要确认父viewACTION_MOVE的过程中,能否将事件传递给子view就好了。

同时对父 View 和子 View 设置点击方法,优先响应哪个

优先响应子 view。

如果先响应父 view,那么子 view 将永远无法响应。父 view 要优先响应事件,必须先调用 onInterceptTouchEvent 对事件进行拦截,那么事件不会再往下传递,直接交给父 view 的 onTouchEvent 处理。

Android系统中ViewGroup的拦截事件默认不拦截。

ACTION_CANCEL什么时候触发

  1. 如果在父View中拦截ACTION_UPACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件。一般是系统自己处理

  2. 如果触摸某个控件,但是又不是在这个控件的区域上抬起(移动到别的地方了),就会出现ACTION_CANCEL

为什么子 View 不消费 ACTION_DOWN,之后的所有事件都不会向下传递了

答案是:mFirstTouchTarget

当子 view 对事件进行处理的时,那么 mFirstTouchTarget 就会被赋值,若是子 view 不对事件进行处理,那么 mFirstTouchTarget 就为 null,之后 VIewGroup 就会默认拦截所有的事件。

我们可以从 dispatchTouchEvent 中找到如下代码,可以看出来,若是子 View 不处理 ACTION_DOWN,那么之后的事件也不会给到它了。

1
2
3
4
5
6
7
8
// 检查是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 省略和问题无关代码
} else {
// 默认拦截
intercepted = true;
}

ViewGroup 中的 onTouchEvent 中消费 ACTION_DOWN 事件(onInterceptTouch 默认设置),那么 ACTION_MOVEACTION_UP 事件是怎么传递的?

首先,我们先分析一下 ACTION_DOWN 的事件走向,由于 ViewGroup 中的 onInterceptTouch 是默认设置的,那么 ACTION_DOWN 的事件最终在 ViewGroup 中的 onTouchEvent 方法中停止了,事件走向是这样的:

1
2
3
4
5
6
-> Activity.dispatchTouchEvent() 
-> ViewGroup1.dispatchTouchEvent()
-> ViewGroup1.onInterceptTouchEvent()
-> view1.dispatchTouchEvent()
-> view1.onTouchEvent()
-> ViewGroup1.onTouchEvent()

接着 ACTION_MOVE 和 ACTION_UP 的事件分发流程,之后 onInterceptTouch 和 View 中的方法都不会被调用了,事件分发如下:

1
2
3
-> Activity.dispatchTouchEvent() 
-> ViewGroup1.dispatchTouchEvent()
-> ViewGroup1.onTouchEvent()

View 事件分发的那些问题
https://luoyuy.top/posts/0403f1031cdf/
作者
LuoYu-Ying
发布于
2022年5月16日
许可协议