这两天研究了一下Android中的事件分发机制,在这里和大家分享下学习成果。首先我们先看一个简单的例子:
1.button.setOnClickListener(new OnClickListener() {
2. @Override
3. public void onClick(View v) {
4. Log.d("TAG", "onClick execute");
5. }
6.});
很显然这是一个button被设置了点击事件,只要一点击这个button就会触发onClick方法,就会打印这log。可是如果我们同时给button加上一个touch事件的监听,重写一下ontouch方法,那么结果又会如何呢。
1.button.setOnTouchListener(new OnTouchListener() {
2. @Override
3. public boolean onTouch(View v, MotionEvent event) {
4. Log.d("TAG", "onTouch execute, action " + event.getAction());
5. return false;
6. }
7.});
大家想想一下,是onclick先执行还是ontouch先执行?下面让我们看下结果。
可以看到是ontouch先执行,而且是执行两次的。
那么我们先得到一个简单的结论:ontouch是先于onclick执行的。
下面让我们改下代码,把ontouch中的返回值改成true,那么结果会怎样呢?
可以看到,onclick方法没有执行,这是怎么回事呢,让我们简单拔一下源码,便知分晓。
首先我们知道,一个当我们触摸到了一个控件一定会先调用他的dispatchTouchEvent方法,那么我们就从这个方法看起。我们进入button的源码发现没有找到dispatchTouchEvent这个方法,找寻他的父类textview依然没有,最终在他的爷爷view中找到了。
1.public boolean dispatchTouchEvent(MotionEvent event) {
2. if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
3. mOnTouchListener.onTouch(this, event)) {
4. return true;
5. }
6. return onTouchEvent(event);
}
我们看这个方法很简单,首先看if中的条件,在if中有三个条件,第一个条件在view这个类中可以找到
setOnTouchListener这个方法对mOnTouchListener进行了赋值,所以第一个条件一定是成立的;那么我们看第二个条件,这是判断当前控件是不是可用的,这个很显然也成立;那么就看第三个条件了,第三个条件是判断的ontouch方法的返回值,第一次我们没有修改他的返回值,返回的是false,所以导致if不成立。调用了onTouchEvent方法,然后onclick调用了,由此可以看出是onTouchEvent中调用了onclick方法。那么我们来看下第二次我们改变了ontouch方法中的返回值为true,这时if条件全部成立,返回了true,所以onTouchEvent方法没有执行,所以onclick方法也就没有执行。
如果我们把当前的button改成textview,再去为textview设置onclick方法和ontouch方法,这时结果又会怎样呢?
1.textView.setOnClickListener(new OnClickListener() {
2. @Override
3. public void onClick(View v) {
4. Log.d("TAG", "onClick execute");
5. }
6.});
1.textView.setOnTouchListener(new OnTouchListener() {
2. @Override
3. public boolean onTouch(View v, MotionEvent event) {
4. Log.d("TAG", "onTouch execute, action " + event.getAction());
5. return false;
6. }
});
我们注意ontouch返回的是false,大家看下结果:
我们发现只打印了一条log,而且是只执行了一次ontouch方法,这是怎么回事呢?按照我们上面的分析ontouch方法返回false,dispatchTouchEvent方法中的if条件不成立,应该执行ontouchEvent方法,并会执行onclick方法,为何onclick方法没有执行呢?
让我们在看下ontouchEvent的源码,就可以得到答案。
1.public boolean onTouchEvent(MotionEvent event) {
2. final int viewFlags = mViewFlags;
3. if ((viewFlags & ENABLED_MASK) == DISABLED) {
4. // A disabled view that is clickable still consumes the touch
5. // events, it just doesn't respond to them.
6. return (((viewFlags & CLICKABLE) == CLICKABLE ||
7. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
8. }
9. if (mTouchDelegate != null) {
10. if (mTouchDelegate.onTouchEvent(event)) {
11. return true;
12. }
13. }
14. if (((viewFlags & CLICKABLE) == CLICKABLE ||
15. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
16. switch (event.getAction()) {
17. case MotionEvent.ACTION_UP:
18. boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
19. if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
20. // take focus if we don't have it already and we should in
21. // touch mode.
22. boolean focusTaken = false;
23. if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
24. focusTaken = requestFocus();
25. }
26. if (!mHasPerformedLongPress) {
27. // This is a tap, so remove the longpress check
28. removeLongPressCallback();
29. // Only perform take click actions if we were in the pressed state
30. if (!focusTaken) {
31. // Use a Runnable and post this rather than calling
32. // performClick directly. This lets other visual state
33. // of the view update before click actions start.
34. if (mPerformClick == null) {
35. mPerformClick = new PerformClick();
36. }
37. if (!post(mPerformClick)) {
38. performClick();
39. }
40. }
41. }
42. if (mUnsetPressedState == null) {
43. mUnsetPressedState = new UnsetPressedState();
44. }
45. if (prepressed) {
46. mPrivateFlags |= PRESSED;
47. refreshDrawableState();
48. postDelayed(mUnsetPressedState,
49. ViewConfiguration.getPressedStateDuration());
50. } else if (!post(mUnsetPressedState)) {
51. // If the post failed, unpress right now
52. mUnsetPressedState.run();
53. }
54. removeTapCallback();
55. }
56. break;
57. case MotionEvent.ACTION_DOWN:
58. if (mPendingCheckForTap == null) {
59. mPendingCheckForTap = new CheckForTap();
60. }
61. mPrivateFlags |= PREPRESSED;
62. mHasPerformedLongPress = false;
63. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
64. break;
65. case MotionEvent.ACTION_CANCEL:
66. mPrivateFlags &= ~PRESSED;
67. refreshDrawableState();
68. removeTapCallback();
69. break;
70. case MotionEvent.ACTION_MOVE:
71. final int x = (int) event.getX();
72. final int y = (int) event.getY();
73. // Be lenient about moving outside of buttons
74. int slop = mTouchSlop;
75. if ((x < 0 - slop) || (x >= getWidth() + slop) ||
76. (y < 0 - slop) || (y >= getHeight() + slop)) {
77. // Outside button
78. removeTapCallback();
79. if ((mPrivateFlags & PRESSED) != 0) {
80. // Remove any future long press/tap checks
81. removeLongPressCallback();
82. // Need to switch from pressed to not pressed
83. mPrivateFlags &= ~PRESSED;
84. refreshDrawableState();
85. }
86. }
87. break;
88. }
89. return true;
90. }
91. return false;
}
从第14行我们可以看出只要控件是可点击的,就会进入到第三个if中,而且当手指抬起是会调用一个叫performClick的方法,所以如果控件是button会执行performClick这个方法,而TextView是不会执行这个方法的因为TextView是不可点击的,所以textview会直接返回91行的false,一个触摸事件的完整流程是DOWN开始,UP结束,只有当中返回值一直是true才会从DOWN走到UP,如果当中出现false就会中断这个点击事件,因为TextView在ontouchEvent中返回了false,所以只会在触摸的时候响应一下,所以只打印出了一条日志。
差点忘了去执行performClick方法的button,我们看下performClick的源码
1.public boolean performClick() {
2. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
3. if (mOnClickListener != null) {
4. playSoundEffect(SoundEffectConstants.CLICK);
5. mOnClickListener.onClick(this);
6. return true;
7. }
8. return false;
9.}
可以看到onclick方法在这里面执行了,button会将触摸事件响应到底,从DOWN到UP。
好了,我们说了这么多简单做下总结:把 touch 事件分发给 View 是通过调用 View 的 dispatchTouchEvent 方法来实现,View 的 dispatchTouchEvent 方法对事件进行了处理,主要做了两步:
- 第一步把事件给 View 的 mOnTouchListener 来处理,通过调用 mOnTouchListener.onTouch 方法来处理事件
- 第二步如果 mOnTouchListener 没有把事件消耗掉,就继续把事件给 View 的 onTouchEvent 方法来处理
View 的 onTouchEvent 方法的作用主要有两个,一个是作用把 touch 事件转换为 click 事件,产生 click 调用;另外一个作用是处理 view 按压状态的变化和焦点的分。touch 事件转换为 click 事件是在 motionEvent 为 up 类型的时候,调用了 performClick 方法,在 performClick 方法中执行 mOnClickListener 的 onclick 方法来执行点击事件。