首先我们先写个简单的例子来测试View的事件转发的流程~
1、案例
为了更好的研究View的事件转发,我们自定以一个MyButton继承Button,然后把跟事件传播有关的方法进行复写,然后添加上日志~
MyButton
[java]
view plain
copy
1. package
2.
3. import
4. import
5. import
6. import
7. import
8.
9. public class MyButton extends
10. {
11. private static final String TAG = MyButton.class.getSimpleName();
12.
13. public
14. {
15. super(context, attrs);
16. }
17.
18. @Override
19. public boolean
20. {
21. int
22.
23. switch
24. {
25. case
26. "onTouchEvent ACTION_DOWN");
27. break;
28. case
29. "onTouchEvent ACTION_MOVE");
30. break;
31. case
32. "onTouchEvent ACTION_UP");
33. break;
34. default:
35. break;
36. }
37. return super.onTouchEvent(event);
38. }
39.
40. @Override
41. public boolean
42. {
43. int
44.
45. switch
46. {
47. case
48. "dispatchTouchEvent ACTION_DOWN");
49. break;
50. case
51. "dispatchTouchEvent ACTION_MOVE");
52. break;
53. case
54. "dispatchTouchEvent ACTION_UP");
55. break;
56.
57. default:
58. break;
59. }
60. return super.dispatchTouchEvent(event);
61. }
62.
63.
64. }
在onTouchEvent和dispatchTouchEvent中打印了日志~
然后把我们自定义的按钮加到主布局文件中;
布局文件:
[html]
view plain
copy
1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2. xmlns:tools="http://schemas.android.com/tools"
3. android:layout_width="match_parent"
4. android:layout_height="match_parent"
5. tools:context=".MainActivity" >
6.
7. <com.example.zhy_event03.MyButton
8. android:id="@+id/id_btn"
9. android:layout_width="wrap_content"
10. android:layout_height="wrap_content"
11. android:text="click me" />
12.
13. </LinearLayout>
最后看一眼MainActivity的代码
[java]
view plain
copy
1. package
2.
3. import
4. import
5. import
6. import
7. import
8. import
9. import
10.
11. public class MainActivity extends
12. {
13. protected static final String TAG = "MyButton";
14. private
15. @Override
16. protected void
17. {
18. super.onCreate(savedInstanceState);
19. setContentView(R.layout.activity_main);
20.
21. mButton = (Button) findViewById(R.id.id_btn);
22. new
23. {
24. @Override
25. public boolean
26. {
27. int
28.
29. switch
30. {
31. case
32. "onTouch ACTION_DOWN");
33. break;
34. case
35. "onTouch ACTION_MOVE");
36. break;
37. case
38. "onTouch ACTION_UP");
39. break;
40. default:
41. break;
42. }
43.
44. return false;
45. }
46. });
47. }
48.
49.
50. }
在MainActivity中,我们还给MyButton设置了OnTouchListener这个监听~
好了,跟View事件相关一般就这三个地方了,一个onTouchEvent,一个dispatchTouchEvent,一个setOnTouchListener;
下面我们运行,然后点击按钮,查看日志输出:
[html]
view plain
copy
1. 08-31 06:09:39.030: E/MyButton(879): dispatchTouchEvent ACTION_DOWN
2. 08-31 06:09:39.030: E/MyButton(879): onTouch ACTION_DOWN
3. 08-31 06:09:39.049: E/MyButton(879): onTouchEvent ACTION_DOWN
4. 08-31 06:09:39.138: E/MyButton(879): dispatchTouchEvent ACTION_MOVE
5. 08-31 06:09:39.138: E/MyButton(879): onTouch ACTION_MOVE
6. 08-31 06:09:39.147: E/MyButton(879): onTouchEvent ACTION_MOVE
7. 08-31 06:09:39.232: E/MyButton(879): dispatchTouchEvent ACTION_UP
8. 08-31 06:09:39.248: E/MyButton(879): onTouch ACTION_UP
9. 08-31 06:09:39.248: E/MyButton(879): onTouchEvent ACTION_UP
我有意点击的时候蹭了一下,不然不会触发MOVE,手抖可能会打印一堆MOVE的日志~~~
好了,可以看到,不管是DOWN,MOVE,UP都会按照下面的顺序执行:
1、dispatchTouchEvent
2、 setOnTouchListener的onTouch
3、onTouchEvent
下面就跟随日志的脚步开始源码的探索~
2、dispatchTouchEvent
首先进入View的dispatchTouchEvent
[java]
view plain
copy
1. /**
2. * Pass the touch screen motion event down to the target view, or this
3. * view if it is the target.
4. *
5. * @param event The motion event to be dispatched.
6. * @return True if the event was handled by the view, false otherwise.
7. */
8. public boolean
9. if
10. return false;
11. }
12.
13. if (mOnTouchListener != null
14. this, event)) {
15. return true;
16. }
17. return
18. }
直接看13行:首先判断mOnTouchListener不为null,并且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接return true ; 也就是下面的onTouchEvent(event)不会被执行了;
那么mOnTouchListener是和方神圣,我们来看看:
[java]
view plain
1. /**
2. * Register a callback to be invoked when a touch event is sent to this view.
3. * @param l the touch listener to attach to this view
4. */
5. public void
6. mOnTouchListener = l;
7. }
其实就是我们在Activity中设置的setOnTouchListener。
也就是说:如果我们设置了setOnTouchListener,并且return true,那么View自己的onTouchEvent就不会被执行了,当然了,本例我们return false,我们还得往下探索 ;
已经解决一个常见的问题:View的onTouchListener和onTouchEvent的调用关系,相信大家应该已经明白了~let's go;继续往下。
接下来是View的onTouchEvent:
[java]
view plain
copy
1. /**
2. * Implement this method to handle touch screen motion events.
3. *
4. * @param event The motion event.
5. * @return True if the event was handled, false otherwise.
6. */
7. public boolean
8. final int
9.
10. if
11. // A disabled view that is clickable still consumes the touch
12. // events, it just doesn't respond to them.
13. return
14. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
15. }
16.
17. if (mTouchDelegate != null) {
18. if
19. return true;
20. }
21. }
22.
23. if
24. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
25. switch
26. case
27. boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
28. if ((mPrivateFlags & PRESSED) != 0
29. // take focus if we don't have it already and we should in
30. // touch mode.
31. boolean focusTaken = false;
32. if
33. focusTaken = requestFocus();
34. }
35.
36. if
37. // This is a tap, so remove the longpress check
38. removeLongPressCallback();
39.
40. // Only perform take click actions if we were in the pressed state
41. if
42. // Use a Runnable and post this rather than calling
43. // performClick directly. This lets other visual state
44. // of the view update before click actions start.
45. if (mPerformClick == null) {
46. new
47. }
48. if
49. performClick();
50. }
51. }
52. }
53.
54. if (mUnsetPressedState == null) {
55. new
56. }
57.
58. if
59. mPrivateFlags |= PRESSED;
60. refreshDrawableState();
61. postDelayed(mUnsetPressedState,
62. ViewConfiguration.getPressedStateDuration());
63. else if
64. // If the post failed, unpress right now
65. mUnsetPressedState.run();
66. }
67. removeTapCallback();
68. }
69. break;
70.
71. case
72. if (mPendingCheckForTap == null) {
73. new
74. }
75. mPrivateFlags |= PREPRESSED;
76. false;
77. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
78. break;
79.
80. case
81. mPrivateFlags &= ~PRESSED;
82. refreshDrawableState();
83. removeTapCallback();
84. break;
85.
86. case
87. final int x = (int) event.getX();
88. final int y = (int) event.getY();
89.
90. // Be lenient about moving outside of buttons
91. int
92. if ((x < 0
93. 0
94. // Outside button
95. removeTapCallback();
96. if ((mPrivateFlags & PRESSED) != 0) {
97. // Remove any future long press/tap checks
98. removeLongPressCallback();
99.
100. // Need to switch from pressed to not pressed
101. mPrivateFlags &= ~PRESSED;
102. refreshDrawableState();
103. }
104. }
105. break;
106. }
107. return true;
108. }
109.
110. return false;
111. }
代码还是比较长的,
10-15行,如果当前View是Disabled状态且是可点击则会消费掉事件(return true);可以忽略,不是我们的重点;
17-21行,如果设置了mTouchDelegate,则会将事件交给代理者处理,直接return true,如果大家希望自己的View增加它的touch范围,可以尝试使用TouchDelegate,这里也不是重点,可以忽略;
接下来到我们的重点了:
23行的判断:如果我们的View可以点击或者可以长按,则,注意IF的范围,最终一定return true ;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//...
return true;
}
接下来就是 switch (event.getAction())了,判断事件类型,DOWN,MOVE,UP等;
我们按照例子执行的顺序,先看 case MotionEvent.ACTION_DOWN (71-78行):
1、MotionEvent.ACTION_DOWN
75行:给mPrivateFlags设置一个PREPRESSED的标识
76行:设置mHasPerformedLongPress=false;表示长按事件还未触发;
77行:发送一个延迟为ViewConfiguration.getTapTimeout()的延迟消息,到达延时时间后会执行CheckForTap()里面的run方法:
1、ViewConfiguration.getTapTimeout()为115毫秒;
2、CheckForTap
[java]
view plain
copy
1. private final class CheckForTap implements
2. public void
3. mPrivateFlags &= ~PREPRESSED;
4. mPrivateFlags |= PRESSED;
5. refreshDrawableState();
6. if
7. postCheckForLongClick(ViewConfiguration.getTapTimeout());
8. }
9. }
10. }
在run方法里面取消mPrivateFlags的PREPRESSED,然后设置PRESSED标识,刷新背景,如果View支持长按事件,则再发一个延时消息,检测长按;
[java]
view plain
copy
1. private void postCheckForLongClick(int
2. false;
3.
4. if (mPendingCheckForLongPress == null) {
5. new
6. }
7. mPendingCheckForLongPress.rememberWindowAttachCount();
8. postDelayed(mPendingCheckForLongPress,
9. ViewConfiguration.getLongPressTimeout() - delayOffset);
10. }
[java]
view plain
copy
1. class CheckForLongPress implements
2.
3. private int
4.
5. public void
6. if (isPressed() && (mParent != null)
7. && mOriginalWindowAttachCount == mWindowAttachCount) {
8. if
9. true;
10. }
11. }
12. }
可以看到,当用户按下,首先会设置标识为PREPRESSED,如果在115毫秒内抬起了,UP时会移除CheckForTap这个回调(UP时会分析);
如果115后,没有抬起,会将View的标识设置为PRESSED且去掉PREPRESSED标识,然后发出一个检测长按的延迟任务,延时为:ViewConfiguration.getLongPressTimeout() - delayOffset(500ms -115ms),这个115ms刚好时检测额PREPRESSED时间;也就是用户从DOWN触发开始算起,如果500ms内没有抬起则认为触发了长按事件:
1、如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true;才把mHasPerformedLongPress置为ture;
2、否则,如果没有设置长按回调或者长按回调返回的是false;则mHasPerformedLongPress依然是false;
好了DOWN就分析完成了;大家回个神,下面回到VIEW的onTouchEvent中的ACTION_MOVE:
2、MotionEvent.ACTION_MOVE
86到105行:
87-88行:拿到当前触摸的x,y坐标;
91行判断当然触摸点有没有移出我们的View,如果移出了:
1、执行removeTapCallback();
2、然后判断是否包含PRESSED标识,如果包含,移除长按的检查:removeLongPressCallback();
3、最后把mPrivateFlags中PRESSED标识去除,刷新背景;
[java]
view plain
copy
1. private void
2. if (mPendingCheckForTap != null) {
3. mPrivateFlags &= ~PREPRESSED;
4. removeCallbacks(mPendingCheckForTap);
5. }
6. }
这个是移除,DOWN触发时设置的PREPRESSED的检测;即当前触发时机在DOWN触发不到115ms时,你就已经移出控件外了;
如果115ms后,你才移出控件外,则你的当前mPrivateFlags一定为PRESSED且发送了长按的检测;
就会走上面的2和3;首先移除removeLongPressCallback()
private void removeLongPressCallback() {
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
}
然后把mPrivateFlags中PRESSED标识去除,刷新背景;
好了,MOVE我们也分析完成了,总结一下:只要用户移出了我们的控件:则将mPrivateFlags取出PRESSED标识,且移除所有在DOWN中设置的检测,长按等;
下面再回个神,回到View的onTouchEvent的ACTION_UP:
3、MotionEvent.ACTION_UP
26到69行:
27行:判断mPrivateFlags是否包含PREPRESSED
28行:如果包含PRESSED或者PREPRESSED则进入执行体,也就是无论是115ms内或者之后抬起都会进入执行体。
36行:如果mHasPerformedLongPress没有被执行,进入IF
38行:removeLongPressCallback();移除长按的检测
45-50行:如果mPerformClick如果mPerformClick为null,初始化一个实例,然后立即通过handler添加到消息队列尾部,如果添加失败则直接执行 performClick();添加成功,在mPerformClick的run方法中就是执行performClick();
终于执行了我们的click事件了,下面看一下performClick()方法:
[java]
view plain
copy
1. public boolean
2. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
3.
4. if (mOnClickListener != null) {
5. playSoundEffect(SoundEffectConstants.CLICK);
6. this);
7. return true;
8. }
9.
10. return false;
11. }
久违了~我们的mOnClickListener ;
if (mOnClickListener != null) {
mOnClickListener.onClick(this);
return true;
}
别激动,还没结束,回到ACTION_UP,
58行:如果prepressed为true,进入IF体:
为mPrivateFlags设置表示为PRESSED,刷新背景,125毫秒后执行mUnsetPressedState
否则:mUnsetPressedState.run();立即执行;也就是不管咋样,最后mUnsetPressedState.run()都会执行;
看看这个UnsetPressedState主要干什么:
private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
}
} public void setPressed(boolean pressed) {
if (pressed) {
mPrivateFlags |= PRESSED;
} else {
mPrivateFlags &= ~PRESSED;
}
refreshDrawableState();
dispatchSetPressed(pressed);
}
把我们的mPrivateFlags中的PRESSED取消,然后刷新背景,把setPress转发下去。
ACTION_UP的最后一行:removeTapCallback(),如果mPendingCheckForTap不为null,移除;
4、总结
好了,代码跨度还是相当大的,下面需要总结下:
1、整个View的事件转发流程是:
View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。
2、onTouchEvent中的DOWN,MOVE,UP
DOWN时:
a、首先设置标志为PREPRESSED,设置mHasPerformedLongPress=false ;然后发出一个115ms后的mPendingCheckForTap,如果115ms内抬起手指,触发了UP,则不会触发click事件,并且最终执行的是UnsetPressedState对象,setPressed(false)将setPress的传递下去;这种情况很少发生,可能只会在压力测试的时候会发现无法触发click事件;
b、如果115ms内没有触发UP,则将标志置为PRESSED,清除PREPRESSED标志,同时发出一个延时为500-115ms的,检测长按任务消息;
c、如果500ms内(从DOWN触发开始算),则会触发LongClickListener:
此时如果LongClickListener不为null,则会执行回调,同时如果LongClickListener.onClick返回true,才把mHasPerformedLongPress设置为true;否则mHasPerformedLongPress依然为false;
MOVE时:
主要就是检测用户是否划出控件,如果划出了:
115ms内,直接移除mPendingCheckForTap;
115ms后,则将标志中的PRESSED去除,同时移除长按的检查:removeLongPressCallback();
UP时:
a、如果115ms内,触发UP,此时标志为PREPRESSED,则执行UnsetPressedState,setPressed(false);会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;
b、如果是115ms-500ms间,即长按还未发生,则首先移除长按检测,执行onClick回调;
c、如果是500ms以后,那么有两种情况:
i.设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;
ii.没有设置onLongClickListener或者
onLongClickListener.onClick
返回false,则
点击事件OnClick事件依然可以触发;
d、最后执行mUnsetPressedState.run(),将setPressed传递下去,然后将PRESSED标识去除;
最后问个问题,然后再运行个例子结束:
1、setOnLongClickListener和setOnClickListener是否只能执行一个
不是的,只要setOnLongClickListener中的onClick返回false,则两个都会执行;返回true则会屏幕setOnClickListener
最后我们给MyButton同时设置setOnClickListener和setOnLongClickListener,运行看看:
[java]
view plain
copy
1. package
2.
3. import
4. import
5. import
6. import
7. import
8. import
9. import
10. import
11. import
12. import
13.
14. public class MainActivity extends
15. {
16. protected static final String TAG = "MyButton";
17. private
18. @Override
19. protected void
20. {
21. super.onCreate(savedInstanceState);
22. setContentView(R.layout.activity_main);
23.
24. mButton = (Button) findViewById(R.id.id_btn);
25. new
26. {
27. @Override
28. public boolean
29. {
30. int
31.
32. switch
33. {
34. case
35. "onTouch ACTION_DOWN");
36. break;
37. case
38. "onTouch ACTION_MOVE");
39. break;
40. case
41. "onTouch ACTION_UP");
42. break;
43. default:
44. break;
45. }
46.
47. return false;
48. }
49. });
50. new
51. {
52. @Override
53. public void
54. {
55. "onclick",Toast.LENGTH_SHORT).show();
56. }
57. });
58.
59. new
60. {
61. @Override
62. public boolean
63. {
64. "setOnLongClickListener",Toast.LENGTH_SHORT).show();
65. return false;
66. }
67. });
68. }
69.
70.
71. }
效果图:
可以看到LongClickListener已经ClickListener都触发了~
最后,本篇博文完成了对View的事件分发机制的整个流程的说明,并且对源码进行了分析;
当然了,View结束,肯定到我们的ViewGroup了,请点击:Android ViewGroup事件分发机制