Android事件分发机制详解

本文主要记录了Android中的事件分发机制。通过对源码进行分析和实例测试,对Android事件分发机制有了更深的了解。主要为学习Android时的笔记。

更多文章欢迎访问我的个人博客–>幻境云图

1. 触发过程

1.1 点击控件

1.2 dispatchTouchEven

一定会执行dispatchTouchEvent方法,若当前类没有该方法,则向上往父类查找。

1
2
3
4
5
6
7
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
  • 条件1 mOnTouchListener != null
1
2
3
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}

给控件设置监听就会给mOnTouchListener赋值,则条件1成立。

  • 条件2 (mViewFlags & ENABLED_MASK) == ENABLED 控件是否是可点击的

  • 条件3 mOnTouchListener.onTouch(this, event) 回调onTouch方法,返回true 则成立

小结:dispatchTouchEvent 方法中一定会执行onTouch方法,如果onTouch方法返回true 则dispatchTouchEvent方法直接返回true 不会执行if外的 return onTouchEvent(event)。

1
2
3
4
5
6
7
    title_bar.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.v("Az","onTouch");
return false;
}
});

在setOnTouchListener时,onTouch方法默认返回false,所以才会执行后面的onTouchEvent方法;

1.3 onTouchEvent

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
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();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}

首先在第14行我们可以看出,如果该控件是可以点击的就会进入到第16行的switch判断中去,而如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。在经过种种判断之后,会执行到第38行的performClick()方法,那我们进入到这个方法里瞧一瞧:

若当前事件为抬手,则进入performClick方法

1
2
3
4
5
6
7
8
9
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}

如果 mOnClickListener != null 则会执行onClick方法,

1
2
3
4
5
6
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}

所以只要给控件设置了点击监听,setOnClickListener就会给mOnClickListener赋值,上面条件就成立,然后回调onClick方法。

到这儿差不多就清楚了分发流程。

这样View的整个事件分发的流程就让我们搞清楚了!不过别高兴的太早,现在还没结束,还有一个很重要的知识点需要说明,就是touch事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean onTouchEvent(MotionEvent event) { 
//省略...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
}

可以看出在dispatchTouchEvent方法中,onTouch方法返回false,然后执行onTouchEvent方法,在进入if判断后,不管进入那个case,最后都会return true,所以才会执行后续的action.

**1. onTouch和onTouchEvent有什么区别,又该如何使用?**

都是dispatchTouchEvent中的方法,onTouch优先级高,若onTouch返回true,就会消费掉当前事件,onTouchEvent就不会执行。

要执行onTouch也需要两个条件,1 给控件设置了触摸监听OnTouchListener ,2该控件是可以点击的。

若控件是非enable的,则不会执行onTouch方法,会执行onTouchEvent,所以想要监听ouTouch事件只能重写onTouchEvent方法来实现。

小结:

控件被点击或触摸后一定会执行dispatchTouchEvent方法(当前类没有则去父类找),如果设置了触摸监听且控件是enable的,就执行onTouch方法 。OnTouch方法返回true则消耗掉本次事件,不执行后面的方法,返回false则执行onTouchEvent方法,如果设置了点击监听且控件是enable的,就在抬手的时候执行onClick方法。

给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

2. ViewGroup

Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View的

上边说只要你触摸了任何控件,就一定会调用该控件的dispatchTouchEvent方法。这个说法没错,只不过还不完整而已。实际情况是,当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。

ViewGroup的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
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}

第二个if语句 if (disallowIntercept || !onInterceptTouchEvent(ev)

第一个条件disallowIntercept 是否禁用掉事件拦截的功能,默认是false 所以是否进入if内部就由第二个条件决定了。

ViewGroup中有一个onInterceptTouchEvent方法 是否拦截触摸事件 默认返回false 即不拦截

1
2
3
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}

第二个条件 !onInterceptTouchEvent(ev) 对返回值取反 即返回false不拦截触摸事件时进入if内部,返回true拦截时不进入if内部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//省略。。。
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
for (int i = count - 1; i >= 0; i--) {//遍历当前ViewGroup下的所有子View
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
if (frame.contains(scrolledXInt, scrolledYInt)) {//判断当前遍历的View是不是正在点击的View
if (child.dispatchTouchEvent(ev)) {//是则调用子View的dispatchTouchEvent
mMotionTarget = child;
return true;
}
}
}
}
}

if内部对子View进行了遍历,最终调用子View的dispatchTouchEvent,然后控件可点击那么dispatchTouchEvent一定会返回true,所以后面的代码就执行不了。

即 ViewGroup 的onInterceptTouchEvent返回false,不拦截触摸事件时,最终会执行子View的dispatchTouchEvent。

ViewGroup 的onInterceptTouchEvent返回true,拦截触摸事件,就不会进入if内部,则会执行到后面的程序

1
2
3
4
5
6
7
8
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}

可以看到,最后会执行super.dispatchTouchEvent(ev),执行父类即View的dispatchTouchEvent。

View的dispatchTouchEvent如下:

1
2
3
4
5
6
7
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}

然后又和前面的一样了。执行onTouch或者onTouchEvent。。

3. 总结

传递顺序

Activity -> PhoneWindow -> DecorView -> ViewGroup -> … -> View

通俗语言总结一下,事件来的时候,

Activity会询问Window,Window这个事件你能不能消耗,

Window一看,你先等等,我去问问DecorView他能不能消耗,

DecorView一看,onInterceptTouchEvent返回false啊,不让我拦截啊,

(DecorView继承自FrameLayout,FrameLayout是ViewGroup的子类,所以DecorView也是ViewGroup的子类,事件从Activity传到了ViewGroup)

遍历一下子View吧,问问他们能不能消耗,那个谁,事件按在你的身上了,你看看你能不能消耗,

假如子View为RelativeLayout

RelativeLayout一看,也没有让我拦截啊,我也得遍历看看这个事件发生在那个子View上面,

到这儿事件从ViewGroup传到View上了

那个TextView,事件在你身上,你能不能消耗了他。TextView一看,消耗不了啊,

RelativeLayout一看TextView消耗不了啊,mFirstTouchTarget==null啊,得,我自己消耗吧,嗯!一看自己的onTouchEvent也消耗不了啊!那个DecorView事件我消耗不了,

DecorView一看自己,我也消耗不了,继续往上传,那个Window啊。事件我消耗不了啊,

Window再告诉Activity事件消耗不了啊。

Activity还得我自己来啊。调用自己的onTouchEvent,还是消耗不了,算了,不要了。

最后Activity的onTouchEvent无论返回什么,事件分发都结束。(如果事件在边界范围外默认会返回false)

参考

https://blog.csdn.net/guolin_blog/article/details/9097463

https://blog.csdn.net/guolin_blog/article/details/9153747

------------------本文到此结束感谢您的阅读------------------
0%