Fork me on GitHub

View的事件分发机制

view的事件分发主要是指点击事件,也就是系统对MotionEvent的传递

准备or拓展

  1. 首先,拓展了解下Android界面的架构图



    UI界面架构

    Activity –>PhoneWindow –>DectorView –>TitleView/ContentView

    • PhoneWindow:每个activity都包含一个window对象,具体由PhoneWindow实现
    • DectorView:作为界面的根View,继承FrameLayout,是整个窗口界面的顶层视图。
    • TitleView:继承FrameLayout,actionbar就是设置在这里
    • ContentView:ID为content的FrameLayout,activity_main.xml就是设置在这里面


主界面详细结构
  • DecorView下层的ViewGroup装载了一个LinerLayout,而DecorView本身会在onResume()中装载,系统将其添加到PhoneWindow中。

    key point:requestWindowFeature(Window.FEATURE_NO_TITLE)一定要在setContentView()方法之前调用才会生效
    (至于原因,去看源码…)


View的事件分发(or传递)

1. 点击事件的产生

  这里所谓的点击事件所对应的对象就是MotionEvent, 典型有三种类型:

  1. ACTION_DOWN :手指接触屏幕
  2. ACTION_MOVE :手指在屏幕上移动
  3. ACTION_UP:手指从屏幕上松开的一瞬间

  正常情况下,一次手指触摸屏幕的行为会产生一系列点击事件。事件序列即:
DOWN -> UP 或者 DOWN -> MOVE-> … ->MOVE -> UP ;
一个事件序列由DOWN开始,中间含有n个MOVE事件,最终以UP结束。

2. 事件的传递规则

  • 2.1 事件的传递顺序:Activity -> Window -> Views
      事件总是先传递给Activity,Activity再传递给Window,最后Window再传给顶层View,如果顶层不处理,继续传给下一层View,重复下去。如果所有的View都不处理事件,那么事件最终会交给Activity.OnTouchEvent()处理

先上张图,下面都会围绕这张图展开


传递规则
  • 2.2 事件传递的3个核心方法:
    • public boolean dispatchTouchEvent(MotionEvent event)
       分发点击事件,将事件向下传递给目标View,如果当前View是目标View,则交给本View处理。返回值受当前View的OnTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
    • public boolean onInterceptTouchEvent(MotionEvent event)
       在dispatchTouchEvent内部调用,用来判断是否拦截事件,如果当前View拦截了某个事件,则调用onTouchEvent处理事件,并且在同一个事件序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。
    • public boolean onTouchEvent(MotionEvent event)
       在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View不再接收到事件。

注:在处理事件时,如果一个View设置了OnTouchListener,那么listener中的OnTouch方法的返回值将影响事件的下一步传递,如果为true,则OnTouchEvent不会被调用,如果为false,则调用当前View的OnTouchEvent。在OnTouchEvent中,如果设置了OnClickListener,则OnClick方法会被调用。

  • 2.3 默认机制:
     (1)默认情况下,一个事件序列只能被一个View处理。
    某个View一旦决定拦截某个事件,那么这个事件序列都会交给此View。反之,如果某View不消耗ACTION_DOWN事件(即它的onTouchEvent返回false),那么此事件序列不会再给它处理,会返回给此View的上一级去处理(即调用上一级的onTouchEvent)。
     (2)默认情况下,View的OnTouchEvent都会消耗事件,除非是不可点击状态(即clickable为false),而且view的enable属性不影响OnTouchEvent的默认返回值,只要其clickable为true,OnTouchEvent就会返回true。

3. 源码分析

从故事开始的地方说起,

1.当一个MotionEvent产生,事件最先传给activity,由activity进行分发,

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

activity会将事件交给window的dispatchTouchEvent去处理,如果下级View们都不处理,则调用自身的onTouchEvent处理
注:可以在activity中重写此方法,以便在事件发送到窗口之前拦截所有触摸屏事件

2.事件传给Window

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Window
public abstract boolean superDispatchTouchEvent(MotionEvent event);
//PhoneWindow(实现类)
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
```
抽象类window会将事件交由它的实现类 PhoneWindow去处理
PhoneWindow直接将事件传递给了DecorView
3.事件再到DecorView
``` java
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}

在上面曾提到,DecorView是继承自FrameLayout,所以直接调用父类的dispatchTouchEvent去处理事件,就这样将事件从activity传到了根View
4.事件进入正轨,从根ViewGroup开始,我们拆分来看
(一)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
...
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//mFirstTouchTarget:First touch target in the linked list of touch targets.
// 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);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}

先看其中的两个操作check for interception 、 handle an initial down;
  (1)check for interception:
  从字面意思来看:判断是否拦截事件。在当前事件为ACTION_DOWN时或者mFirstTouchTarget != null时,ViewGroup会判断是否拦截事件。这里的mFirstTouchTarget != null是指有子view要处理此事件。所以一旦ViewGroup决定拦截事件,那么mFirstTouchTarget就为null,而且当事件序列中后续的move和up事件到来时,判断条件不满足,导致ViewGroup中的OnInterceptTouchEvent不会被重复调用。
  满足条件后,ViewGroup会判断标记位FLAG_DISALLOW_INTERCEPT的值(一般在子View中设置),如果禁止拦截,Viewgroup将无法拦截除了ACTION_DOWN之外的事件(因为在处理ACTION_DOWN事件的时候,Viewgroup会在下面的”initial down”中resetTouchState重置标志位);反之则调用onInterceptTouchEvent去判断是否拦截事件。
  (2)handle an initial down:
  在ACTION_DOWN事件到来的时候,清除上一个触摸手势所保存的状态,包括FLAG_DISALLOW_INTERCEPT标志位的重置。所以在面对ACTION_DOWN事件时,ViewGroup总会调用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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
....
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
....
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
....
if (!canViewReceivePointerEvents(child)||
!isTransformedTouchPointInView(x, y, child, null))
{
ev.setTargetAccessibilityFocus(false);
continue;
}
....
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
....
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS;
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
.....
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
.....
}

  如果ViewGroup不拦截事件,则交由分发给子项。具体实现:
  先遍历ViewGroup所有子元素,判断子元素是否能收到点击事件,如果能,则事件会传递给它来处理。可以看到dispatchTransformedTouchEvent其实就是调用子view的dispatchTouchEvent。如果子View选择处理事件,则dispatchTouchEvent就会返回true,同时mFirstTouchTarget就会被赋值并结束循环。如果当前子元素不处理,则进入下一个子元素的循环。
  如果ViewGroup没有子元素,或者子元素的dispatchTouchEvent返回了false,导致mFirstTouchTarget未被赋值,这时ViewGroup就会自己处理事件。

5.事件交给了View
(1)View中的dispatchTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean dispatchTouchEvent(MotionEvent event) {
....
if (onFilterTouchEventForSecurity(event)) {
//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;
}

  从代码中可以看出,View会先判断有没有设置OnTouchListener,如果有且onTouch方法返回true,则不再调用下面的onTouchEvent,这样就方便在外部处理MotionEvent。
(2)View中的onTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
public boolean onTouchEvent(MotionEvent event) {
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
}

  这个地方就应证了上面所说的,view的enable属性不影响view消耗事件,只和CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE属性有关。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public boolean onTouchEvent(MotionEvent event) {
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
....
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
break;
case MotionEvent.ACTION_DOWN:
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
....
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
....
break;
case MotionEvent.ACTION_MOVE:
....
break;
}
return true;
}
}
private final class CheckForLongPress implements Runnable {
@Override
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
}

  可以看到,当ACTION_UP事件发生时,如果不是longPress会调用performClick,在这个方法里,如果view设置了OnClickListener,则会调用它的OnClick方法,而且performClick是通过Runnable发布调用的,方便在处理事件之前更新View状态。
  当ACTION_DOWN事件发生时,View会检查是否是LongClick事件,如果长按事件设置了回调,则执行回调方法。而且如果回调返回true的话,mHasPerformedLongPress就会设置为true,所以在长按时,当上面UP事件发生,performClick就无法调用,即短按事件就会被屏蔽。

总结

  View事件分发的流程大概就是这样,后面还会继续研究滑动冲突的问题。最后,说说源码分析的那点事儿,过程很蓝瘦,但是得学会取舍,一时看不懂先略过,注重大体逻辑,跟着翻译走,没准在后面就能明白前面变量或者方法的意义,水到渠成。