0%

View事件分发机制理解

引言

最近重新学习了一遍View的事件分发机制。其实之前已经把这一块差不多看过,不好理解,几遍下来,好像都了解了,但总觉得差点意思。
发现其实之前看《艺术探索》十分痛苦,主要是看得不够仔细,以及书中概念和知识点较多,没有一个大题的认识,很容易被绕进去。现在水平有些长进了,找了几本书和博客,终于有了更加深刻的理解。

事件分发的基本流程

首先要知道View的触摸事件,主要有ACTION_UP,ACTION_MOVE,ACTION_DOWN。以一个ACTION_DOWN事件开始到一个ACTION_UP事件的一系列事件作为一个事件序列。

View的事件分发,主要有三个流程,dispatchTouchEvent(分发),onInterceptTouchEvent(拦截),onTouchEvent(消费处理)。
Activity,View,ViewGroup处理事件的能力不全相同。
其中Activity和View只有dispatchTouchEvent()和onTouchEvent()方法,ViewGroup只有dispatchTouchEvent,onInterceptTouchEvent方法。

当一个点击事件产生后,事件传递顺序是:Activity -> Window -> View
一个Activity收到一个事件后,触发dispatchTouchEvent()。该方法伪代码如下

1
2
3
4
5
fun dispatchTouchEvent(MotionEvent ev) {
if(getWindows.superDispatchTouchEvent(ev))
reuturn true
return onTouchEvent(ev)
}

首先将事件交由Window进行分发。

  • 如果返回true,则事件循环结束,因为说明点击事件已被处理。
  • 如果返回false,则触发Activity的onTouchEvent()

当一个ViewGroup接收到一个事件,则触发dispatchTouchEvent()。

1
2
3
4
5
6
7
fun dispatchTouchEvent() {
if(onInterceptTouchEvent())
return onTouchEvent()
// 实际上是调用了 super.dispatchTouchEvent(),在其中调用view.onTouchEvent()
else
return child.dispatchTouchEvent()
}

  • 先根据当前的业务逻辑,调用onInterceptTouchEvent()判断是否拦截该事件
    • onIntercept()返回false,即不拦截,则调用子View的dispatchTouchEvent(),将当前事件传递到子View处理。
    • onIntercept()返回true,即为拦截,则调用onTouchEvent()消费该事件。
  • onTouchEvnet()或child.dispatchTouchEvent()返回false,说明该ViewGroup或子View没有消费该事件,则将该事件传回父容器,即调用父容器的onTouchEvent()方法
    如果所有的View都不消费该事件,则Activity的onTouchEvent()会被调用。

当一个View决定处理某个事件,其处理机制大抵如下。

1
2
3
4
5
6
7
8
9
fun dispatchTouchEvent() {
...
if(onTouchListener != null
&& onTouchListener.onTouch())
result = true
if(!result && onTouchEvent())
result = true
// onTouchEvent()中会调用OnClickListener.onClick()
}

先判断onTouchListener是否为空,不为空则调用onTouch()方法,onTouch()返回false才会调用onTouchEvent()方法。我们一般设置的OnClickListener.onClick()也是在其中被调用。也就是说onClick()方法一般是在事件传递的末端。

源码探究

Activity对事件的分发

PhoneWindow是Window的唯一实现类。其中的superDispatchTouchEvent()方法实现,直接将事件传递给了DecorView。

1
2
3
4
// 源码:PhoneWindows#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}

通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChlidAt(1)可以获取Activity所设置的View,这个mDecor显然就是getWindow().getDecorView()返回的View,而我们通过setContentView所设置的View则是他的一个子View。

DecorView继承自FrameLayout,且是一个父View,所以事件最终会传递给顶级View,即是setContentView设置的View。

ViewGroup对事件的分发

当一个ViewGroup决定对点击事件进行拦截时,如果onTouchListener被设置,则调用onTouch(),否则调用onTouchEvent()(此过程与上文介绍的View的处理机制相似)。也就是说,如果都提供的话,onTouch会屏蔽掉onTouchEvnet()
这个逻辑其实很好理解:onTouchListener是对触摸的监听,即触摸时候触发的回调;只有不对(普通)触摸做处理时,再进一步判断触摸事件,即点击,手势等。这里注意触摸和触摸事件的区别。

拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}

这是ViewGroup中关于判断是否拦截事件的部分。

当事件由ViewGroup的子View成功处理,mFirstTouchTarget会被赋值并指向子View

这句话总是似懂非懂,简单翻了翻后边的代码,看到后边dispatchTransformedTouchEvent()中,使用super.dispatchTouchEvnet(),我还以为当子View不处理事件时,会调用父View的dispatchTouchEvent(),此时就可以通过该标示得知该事件是否被处理。如果mFirstTouchTarget未被赋值,则直接intercept为true拦截该事件进行处理。
这段话写完我就产生了自我怀疑。我是在说个啥???
仔细又看一遍书后,算是明白了,这个操作其实实现的是:一个事件序列只能被一个View拦截且消耗。即一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接被它处理。当某个View拦截了down事件,则该事件序列的后续事件都由它处理。

一旦事件由当前ViewGroup拦截,mFirstTouchTarget != null 不成立。那么当UP和MOVE事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)为false,则onInterceptTouchEvent()不会再被调用,并且同一序列中的其他事件都默认交由它处理。

这里还有一种特殊情况,即是FLAG_DISALLOW_INTERCEPT。这个标志位是由requestDisallowIntercept()设置,一般用于禁止ViewGroup拦截除DOWN以外的任何事件。

  • 当分发DOWN事件时,会对该标志位进行重置,使得子View设置的标志位失效。所以面对DOWN事件,ViewGroup都会调用onInterceptTouchEvent()进行判断是否拦截

分发

如果当前事件未被拦截,则开始对子View进行分发,即通过一个for循环对所有子View进行遍历。
首先判断当前子View是否可以接收到点击事件

1
2
3
4
5
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

  • canViewReceivePointerEvents()

    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * Returns true if a child view can receive pointer events.
    * @hide
    */
    private static boolean canViewReceivePointerEvents(@NonNull View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
    || child.getAnimation() != null;
    }

    该方法通过判断child是否VISIBLE或者child是否处于播放动画状态。如果child不可见而且不处于动画状态(不是因为动画才在该位置不可见),那么它不是符合事件消费的条件。这样做的目的在于,即是一个child因为动画如位移而不在原来的位置可见,那也可以收到点击事件。这也解释了为什么位移动画(View动画)以后,View还可以在原来的位置接收到点击事件的原因。

  • isTransformedTouchPointInView()
    判断child是否在点击范围内

如果条件满足,则开始真正对child进行事件传递。主要调用dispatchTransformedTouchEvent()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;

// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
......
}

该方法中先判断了child是否为null

  • 如果为null,则调用super.dispatchTouchEvent(),即View.dispatchTouchEvent(),在其中实现对触摸事件进行处理消费。在ViewGroup中对子View进行分发后,如果事件没有被子View消耗,就会调用这个方法并传入null。
  • 如果不为null。则调用child.dispatchTouchEvent()将事件进行分发。如果该方法发返回false,则说明child未消耗,将继续进行事件的分发。如果返回true。则说明child已消耗该事件,mFirstTouchEvent被赋值,跳出对子View的遍历分发。
    如果没有任何子View对事件进行处理,则ViewGroup会如上所说,调用dispatchTransformtouchvnet()并传入null,从ViewGroup转到View.dispatchTouEvent(),自己对事件进行处理。

View对事件的处理

当事件被分发到某一个View中,此时View可能是一个底层View,也可能是一个ViewGroup,但都作为一个单独的元素,无法在向下传递事件,必须自己在View.dispatchTouchEvent()中,完成对View触摸事件的响应处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}

实现逻辑跟上文体现的差不多,先判断是否设置了OnTouchListener,如果OnTouchListener中的onTouch()返回true,则onTouchEvent()不会被调用。所以OnTouchListener.onTouch()比onTouchEvent()优先级更高,这样做的好处是方便在外界处理点击事件。因为onTouch()作为一个外部实现的回调方法,与使用者联系会更为紧密。

onTouchEvent()

可以查看一下onTouchEvent()内部实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}

该方法一开始是实现对不可用(disabled)View的处理,可以看到不可用的View也可以消耗点击事件,只是不会作出响应。
并且以上代码还可以看出,无论View是否可用,只要设置了CLICKABLE或LONG_CLICKABLE,onTouchEvent()都会消耗该事件并返回true。
1
2
3
4
5
6
7
8
9
10
11
12
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
......
if (!post(mPerformClick)) {
performClickInternal();
}
mIgnoreNextUpEvent = false;
break;
}
return true;
}

以上代码是对点击事件具体处理逻辑。没怎么看懂。。=_=(关于TOOLTIP,据说android8新添加的跟鼠标悬浮显示有关)

只知道核心代码部分即是触发了performClick()方法。在perfromClick()中,如果View设置了OnClickListener,则会调用我们一般设置的onClick()(终于,出现了点击事件中与我们接触最为频繁的)!由此也可见,onClick()方法其实是在事件分发机制中处于最最末端的。

关于滑动冲突解决

未完待续…


未完 待续 (╯‵□′)╯︵┻━┻