oynix

于无声处听惊雷,于无色处见繁花

Android之事件传递

触摸事件,主要有三种类型,ACTION_DOWN、ACTION_MOVE和ACTION_UP,分别指点击、移动和抬起。这篇文章主要来说一说一个事件产生之后,在控件间的传递。

与其说是事件传递,不如说是事件序列的传递。一个事件序列,是由一个ACTION_DOWN和一个ACTION_UP组成,中间可能包含0个至多个ACTION_MOVE事件。手指按下时,会产生一个ACTION_DOWN事件,手指抬起时,会产生一个ACTION_UP事件,如果按下和抬起的位置偏移超过系统设定的阈值,便会产生一个或多个ACTION_MOVE事件,这便是一个事件序列。

事件序列,是按照视图树由根到枝叶的顺序传递的,意思就是说,最外层的View或ViewGroup最先接收到事件,然后向内传递。其中事件流经的控件主要分为两种:ViewGroup和View。

ViewGroup

ViewGroup是View的子类,其中有3个和事件相关的方法:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,分别是分发事件、拦截事件和处理事件。

简单来说就是,系统通过dispatchTouchEvent方法将事件传入,在dispatchTouchEvent内根据onInterceptTouchEvent来判断是否拦截该事件,如果拦截了,则不再将其沿着视图树向内传递;如果拦截,则根据onTouchEvent方法来判读是否处理,如果处理了,则表示消费了该事件。

View

View没有onInterceptTouchEvent,因为View无子View,是在视图树的最内层了,不需要拦截。当事件通过dispatchTouchEvent传入,根据onTouchEvent方法来判断是否消费。

事件流转

因为事件都是以事件序列的形式传递的,单独看某个事件意义不大,这里分析事件序列,先看ACTION_DOWN事件,这里的向内和向外都是针对视图树的结构而言的。

1
2
3
4
5
6
7
// 视图树
DecorView
|-FrameLayout
|-TextView
|-LinearLayout
|-ImageView
|-FrameLayout
  1. 向内分发ACTION_DOWN
  • ViewGroup接收到事件后,会根据onInterceptTouchEvent方法来判断是否拦截,如果拦截,则根据onTouchEvent方法来判断是否消费事件。
  • View接收到事件后,根据onTouchEvent方法判断是否消费事件。
  • 如果ViewGroup没有拦截事件,则根据坐标,依次遍历该事件命中的子View/ViewGroup,重复上面这个过程
  • 当事件被拦截,或者传递到视图树最内层时,便不会继续向内传递,转而开始回传。
  1. 向外回传ACTION_DOWN
  • 事件回传,传递的是dispatchTouchEvent方法的返回值,true/false
  • 如果回传的false,父ViewGroup的onTouchEvent方法会被调用,可理解为本着事件不浪费的原则,既然子视图都没消费,那么就看看自己是否可以消费掉
  • 如果回传到视图根时,依然是false,表示没有子视图消费ACTION_DOWN事件,则该序列的后续事件不再向内传递,都由根视图处理
  1. 事件序列的后续事件
  • 系统本着一个事件序列只由一个视图处理的原则
  • 后续事件都将传递至消费掉ACTION_DOWN事件的ViewGroup/View,且在dispatchTouchEvent内直接调用onTouchEvent,不经过onInterceptTouchEvent方法,如果有的话
  • 当然,后续事件在传递过程中也可以被拦截和消费,但不建议这么做,比如一个按钮只接收了一个按下事件,则在逻辑上可能会不完整
  • 后续事件在回传时,即便是false,父ViewGroup的onTouchEvent也不会被调用,因这与第一点相悖

根视图

根据上一篇文章的介绍可以知道,DecorView是我们看到的视图的根,它继承自FrameLayout,是一个ViewGroup,所以它有3个和事件相关的方法。此外,Activity对应一个PhoneWindow,PhoneWindow对应一个DecorView,所以在事件序列产生的时候,最新接收到的是DecorView。

根据源码,DecorView将事件转给了Window.Callback,这个Callback是Window在Activity中初始化时传入的,就是Activity本身,Activity实现了Callback接口,即,在DecorView中调用了Activity的dispatchTouchEvent。

Activity的dispatchTouchEvent方法中,先将事件传给了Window的superDispatchTouchEvent方法,若没消费,再调用自己的onTouchEvent。而Window的superDispatchTouchEvent很简单,就是直接调用了DecorView的superDispatchTouchEvent,最终调用了ViewGroup的dispatchTouchEvent方法。

总结下来就是,DecorView将事件转给了Activity,在Activity的dispatchTouchEvent方法中判断,若DecorView没有消费,则交给Activity的onTouchEvent。

所以,从处理事件顺序来看,真实的事件流转顺序应该是:Activity -> DecorView -> … -> DecorView -> Activity。

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
28
29
30
31
32
33
34
35
36
// DecorView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

// Activity.java
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
}

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 在Activity中可以重写该方法,来监听用户的操作
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

// PhoneWindow.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}

总结

  1. 将事件传递分为向内分发和向外回传两个过程更便于理解,这里的向内和向外都是针对视图树的结构而言的。
  2. 向内分发过程中,事件一旦被拦截,或者传到了最内层,便会开始回传事件的消费结果,也就是入口dispatchTouchEvent方法的结果
  3. 当回传结果是false,且事件是序列事件的头事件ACTION_DOWN时,会调用父ViewGroup的onTouchEvent来尝试消费事件
  4. 系统本着一个事件序列只交给一个View/ViewGroup来处理的原则

扩展之滑动冲突

当在列表内部内嵌可滑动视图时,内部的可滑动视图常常监听不到滑动事件,这是因为列表作为内嵌视图的父ViewGroup会拦截滑动事件,所以内部自然感知不到。比较好的体验是,内部可滑动视图向上滑动到顶时,后续的滑动事件交给父列表处理,让父列表滑动来响应用户的滑动操作;当内部视图向下滑动到底时,后续的滑动事件也是交给父列表处理,让父列表滑动个来响应用户怼滑动操作。

为了解决这个问题,就必须要让事件序列传到最内层,根据上面内容可以知道,要想让事件序列传到内部,那么ACTION_DOWN事件就要交给内部视图来处理。

再看后续事件,作为内部视图的父视图,后续的每一个事件在传到内部视图之前,都会流经外部的列表,因此,就有两种方式来处理后续事件

  • 由父视图来判断当前事件是由自己消费,还是交给内部视图消费
  • 由内部试图来判断当前事件是由自己消费,还是让父视图消费

第一种比较简单,在父视图内对ACTION_DOWN之外的后续事件判断,需要则拦截,不需要则通过;

通过源码可以看到,在子视图中,是可以控制父视图是否可以拦截事件的,所以上述的第二种方式也是可行的。内部视图在收到ACTION_DOWN事件后,通过parent.requestDisallowInterceptTouchEvent(true)来禁止父视图拦截事件,当自己不再需要后续事件时,通过parent.requestDisallowInterceptTouchEvent(false)来让父视图拦截处理后面的滑动事件,来响应用户的操作即可。

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
// ViewGroup.java
// Check for interception.
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
....
// Check for interception. 判断是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 当设置了FLAG_DISALLOW_INTERCEPT时,则直接不拦截事件
// 没有设置FLAG_DISALLOW_INTERCEPT时,才会去调用onInterceptTouchEvent来判断
// FLAG_DISALLOW_INTERCEPT通过requestDisallowInterceptTouchEvent来设置
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// 当不是ACTION_DOWN事件,且ACTION_DOWN没有被消费,则直接拦截,不再向内分发
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
}
------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2021/10/c5a24f040ab4/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道