事件传递虽然算不上某个单独的知识点,但是在实际项目开发中肯定会碰到,如果不明白其中的原理,那在设计各种滑动效果时就会感到很困惑。
关于事件的传递,我们可能会有以下疑问:
事件是如何传递的
事件是如何处理的
自定义view的时候,事件也冲突了怎么解决
带着这三个疑问,我们来总结一下事件传递机制是怎么回事。
一、事件分发的原理:
1、事件是如何传递的:
(1)首先由Activity分发,分发给根View,也就是DecorView(DecorView为整个Window界面的最顶层View)
(2)然后由根View分发到子的View
如下图所示:
再来看下面这张图:(这张图是整个事件传递机制的核心)
上图显示:
在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截。onInterceptTouchEvent方法:
返回true代表不允许事件继续向子View传递,将会触发当前View的onTouchEvent(),进行事件的消费;
返回false代表不对事件进行拦截,事件可以传递给孩子
默认返回false
2、事件是如何处理的:
再来看下面这张图:
上图显示:子View中如果将传递的事件消费掉,父类的ViewGroup中将无法接收到任何事件。
二、onTouch和onClick事件同时发生的问题:
首先这里要解释一下各种概念,避免混淆。
1、各种概念:
事件:
混合体(可能是点击事件也可能是触摸事件)。
触摸事件:
按下、滑动和离开
点击事件:
按下、停留一会儿和离开
触摸onTouch事件和点击onClick事件有什么关系?
(1)执行先后不一样。触摸事件先执行
(2)触摸事件返回值影响点击事件(前者影响后者,而后者不影响前者)
2、onTouch和onClick事件同时执行:
如果按钮的onTouch和onClick方法同时执行,会有什么效果呢?我们通过代码来看一下:
1 import android.app.Activity;
2 import android.os.Bundle;
3 import android.util.Log;
4 import android.view.MotionEvent;
5 import android.view.View;
6 import android.widget.Button;
7
8 public class MainActivity extends Activity {
9
10 private static final String TAG = "MainActivity";
11 private Button btn;
12
13 @Override
14 protected void onCreate(Bundle savedInstanceState) {
15 super.onCreate(savedInstanceState);
16 setContentView(R.layout.activity_main);
17 btn = (Button) findViewById(R.id.btn);
18
19 //按钮的touch触摸事件
20 btn.setOnTouchListener(new View.OnTouchListener() {
21 @Override
22 public boolean onTouch(View v, MotionEvent event) {
23 switch (event.getAction()) {
24 case MotionEvent.ACTION_DOWN: //按下的动作
25 Log.d(TAG, "btn is MotionEvent.ACTION_DOWN");
26 break;
27 case MotionEvent.ACTION_MOVE: //滑动的动作
28 Log.d(TAG, "btn is MotionEvent.ACTION_MOVE");
29 break;
30 case MotionEvent.ACTION_UP: //离开的动作
31 Log.d(TAG, "btn is MotionEvent.ACTION_UP");
32 break;
33 }
34
35 return false; //默认的返回值
36 }
37 });
38
39 //按钮的点击事件
40 btn.setOnClickListener(new View.OnClickListener() {
41 @Override
42 public void onClick(View v) {
43 Log.d(TAG, "btn is click");
44 }
45 });
46 }
47
48 }
上方代码中,按钮btn既包含了onTouch事件,也包含了onClick事件,现在运行程序,点击按钮,后台打印的日志如下:
通过上方日志我们可以看到,onTouch事件是比onClick事件先执行的。
备注:这里提示一下,如果我们仅仅只是用手指点击按钮,然后马上松开,onTouch事件中只会执行ACTION_DOWN和ACTION_UP动 作;如果用手机点击按钮,并且手指还在按钮上滑动了一会儿,那么滑动的过程中,ACTION_MOVE动作就会不停的执行。现在我们应该能明白这三个动作 的含义了吧?
3、只执行onTouch事件,不执行onClick事件:
如果按钮的onTouch和onClick方法同时执行,在有些情况下不太满足产品的需求。那如果只想执行onTouch事件,不执行onClick事件,该怎么做呢?很简单,只需要在上方代码中,将第39行的代码改为return true,就行了,即:将onTouch方法的返回值改为true,就会只执行onTouch事件,不执行onClick事件。改完代码之后,后台的运行效果如下:
为什么这样改代码就可以了呢?这就需要从源码的角度来理解了。
button按钮中没有dispatchTouchEvent方法,需要去它的父类View.java中去找dispatchTouchEvent方法。源码如下所示:
上图分析:红框部分的onTouch()方法默认是返回false,所以就会执行蓝框部分的代码,即:调用onTouchEvent()方法。而onTouchEvent方法中,会在ACTION_UP动作里面会去初始化onClick事件。如下图所示:
于是onClick事件就得到了执行。
三、onClick和onLongClick事件能同时发生:
我们通过代码来演示一下。
1、onTouch事件、onLongClick事件、onClick事件默认是同时执行:(执行的先后顺序:onTouch > onLongClick > onClick)
完整版代码如下:
1 import android.app.Activity;
2 import android.os.Bundle;
3 import android.util.Log;
4 import android.view.MotionEvent;
5 import android.view.View;
6 import android.widget.Button;
7
8 public class MainActivity extends Activity {
9
10 private static final String TAG = "MainActivity";
11 private Button btn;
12
13 @Override
14 protected void onCreate(Bundle savedInstanceState) {
15 super.onCreate(savedInstanceState);
16 setContentView(R.layout.activity_main);
17 btn = (Button) findViewById(R.id.btn);
18
19 //按钮的touch事件
20 btn.setOnTouchListener(new View.OnTouchListener() {
21 @Override
22 public boolean onTouch(View v, MotionEvent event) {
23
24 switch (event.getAction()) {
25 case MotionEvent.ACTION_DOWN: //按下的动作
26 Log.d(TAG, "btn is MotionEvent.ACTION_DOWN");
27 break;
28
29 case MotionEvent.ACTION_MOVE: //滑动的动作
30 Log.d(TAG, "btn is MotionEvent.ACTION_MOVE");
31 break;
32
33 case MotionEvent.ACTION_UP: //离开的动作
34 Log.d(TAG, "btn is MotionEvent.ACTION_UP");
35 break;
36 }
37
38 return false; //默认的返回值
39 }
40 });
41
42
43 //按钮的onLongClick事件
44 btn.setOnLongClickListener(new View.OnLongClickListener() {
45 @Override
46 public boolean onLongClick(View v) {
47
48 Log.d(TAG, "btn is onLongClick");
49
50 return false; //默认的返回值
51 }
52 });
53 //按钮的onClick事件
54 btn.setOnClickListener(new View.OnClickListener() {
55 @Override
56 public void onClick(View v) {
57 Log.d(TAG, "btn is onClick");
58 }
59 });
60 }
61
62 }
运行程序后,长按按钮,后台日志如下:
源码比较长,就不贴出来了,通过查看源码我们得知,当onTouch事件中的ACTION_DOWN动作执行180ms之后,就会执行onLongClick事件。
那我们现在知道了,如果在一个按钮上按下的时间过长,onLongClick事件会比onClick事件先执行。
2、只执行onTouch事件和onLongClick事件,不执行onClick事件:
为了实现这种逻辑,也很简单,只需要将上方的第50行代码改为return true就行了,即:将onLongClick方法的返回值改为true,就不会执行onClick事件了。改完代码之后,后台的运行效果如下:
为什么这样改代码就可以了呢?这就需要从源码的角度来理解了。在View.java中的dispatchTouchEvent方法里,ACTION_UP动作里面对onLongTouch事件进行了处理,具体源码就不展示出来了,这个有点复杂。
四、事件传递机制调用顺序:
ViewGroup的事件传递方法:
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
View的事件传递方法:
- View的dispatchTouchEvent
- View的onTouchEvent
注意,只有父的ViewGroup容器才有onInterceptTouchEvent方法。这也很好理解,最小的那个子的view没必要再拦截了,因为无法继续向下传递事件,是否拦截已经没有意义了。
接下来,我们用LinearLayout代表ViewGroup,用Button代表子View,然后去重写LinearLayout和Button中的事件传递方法,看一下各个方法的调用顺序。代码如下:
(1)MyLinearLayout.java:(重写LinearLayout中的事件传递方法)
1 package com.example.smyhvae.touchdemo;
2
3 import android.content.Context;
4 import android.util.AttributeSet;
5 import android.util.Log;
6 import android.view.MotionEvent;
7 import android.widget.LinearLayout;
8
9 /**
10 * Created by smyhvae on 2015/9/11.
11 */
12 public class MyLinearLayout extends LinearLayout {
13
14 private static final String TAG = "MainActivity";
15
16 public MyLinearLayout(Context context) {
17 super(context);
18 }
19
20 public MyLinearLayout(Context context, AttributeSet attrs) {
21 super(context, attrs);
22 }
23
24 public MyLinearLayout(Context context, AttributeSet attrs, int defStyle) {
25 super(context, attrs, defStyle);
26 }
27
28 @Override
29 public boolean dispatchTouchEvent(MotionEvent ev) {
30
31 switch (ev.getAction()) {
32 case MotionEvent.ACTION_DOWN: //按下的动作
33 Log.d(TAG, "ViewGroup dispatchTouchEvent ACTION_DOWN");
34 break;
35 case MotionEvent.ACTION_MOVE: //滑动的动作
36 Log.d(TAG, "ViewGroup dispatchTouchEvent ACTION_MOVE");
37 break;
38 case MotionEvent.ACTION_UP: //离开的动作
39 Log.d(TAG, "ViewGroup dispatchTouchEvent ACTION_UP");
40 break;
41 }
42
43 return super.dispatchTouchEvent(ev);
44 }
45
46 @Override
47 public boolean onInterceptTouchEvent(MotionEvent ev) {
48
49 switch (ev.getAction()) {
50 case MotionEvent.ACTION_DOWN: //按下的动作
51 Log.d(TAG, "ViewGroup onInterceptTouchEvent ACTION_DOWN");
52 break;
53 case MotionEvent.ACTION_MOVE: //滑动的动作
54 Log.d(TAG, "ViewGroup onInterceptTouchEvent ACTION_MOVE");
55 break;
56 case MotionEvent.ACTION_UP: //离开的动作
57 Log.d(TAG, "ViewGroup onInterceptTouchEvent ACTION_UP");
58 break;
59 }
60
61 return super.onInterceptTouchEvent(ev);
62 }
63
64 @Override
65 public boolean onTouchEvent(MotionEvent event) {
66
67 switch (event.getAction()) {
68 case MotionEvent.ACTION_DOWN: //按下的动作
69 Log.d(TAG, "ViewGroup onTouchEvent ACTION_DOWN");
70 break;
71 case MotionEvent.ACTION_MOVE: //滑动的动作
72 Log.d(TAG, "ViewGroup onTouchEvent ACTION_MOVE");
73 break;
74 case MotionEvent.ACTION_UP: //离开的动作
75 Log.d(TAG, "ViewGroup onTouchEvent ACTION_UP");
76 break;
77 }
78
79 return super.onTouchEvent(event);
80 }
81 }
(2)MyButton.java:(重写Button中的事件传递方法,注意:这里面没有onInterceptTouchEvent方法)
1 package com.example.smyhvae.touchdemo;
2
3 import android.content.Context;
4 import android.util.AttributeSet;
5 import android.util.Log;
6 import android.view.MotionEvent;
7 import android.widget.Button;
8
9 /**
10 * Created by smyhvae on 2015/9/11.
11 */
12 public class MyButton extends Button {
13 private static final String TAG = "MainActivity";
14
15 public MyButton(Context context) {
16 super(context);
17 }
18
19 public MyButton(Context context, AttributeSet attrs) {
20 super(context, attrs);
21 }
22
23 public MyButton(Context context, AttributeSet attrs, int defStyle) {
24 super(context, attrs, defStyle);
25 }
26
27 @Override
28 public boolean dispatchTouchEvent(MotionEvent event) {
29 switch (event.getAction()) {
30 case MotionEvent.ACTION_DOWN: //按下的动作
31 Log.d(TAG, "View dispatchTouchEvent ACTION_DOWN");
32 break;
33 case MotionEvent.ACTION_MOVE: //滑动的动作
34 Log.d(TAG, "View dispatchTouchEvent ACTION_MOVE");
35 break;
36 case MotionEvent.ACTION_UP: //离开的动作
37 Log.d(TAG, "View dispatchTouchEvent ACTION_UP");
38 break;
39 }
40
41 return super.dispatchTouchEvent(event);
42 }
43
44 @Override
45 public boolean onTouchEvent(MotionEvent event) {
46 switch (event.getAction()) {
47 case MotionEvent.ACTION_DOWN: //按下的动作
48 Log.d(TAG, "View onTouchEvent ACTION_DOWN");
49 break;
50 case MotionEvent.ACTION_MOVE: //滑动的动作
51 Log.d(TAG, "View onTouchEvent ACTION_MOVE");
52 break;
53 case MotionEvent.ACTION_UP: //离开的动作
54 Log.d(TAG, "View onTouchEvent ACTION_UP");
55 break;
56 }
57
58 return true;
59 }
60 }
上方代码中,将onTouchEvent方法的返回值修改为true(59行),表示这个子的view希望消费这个事件。
(3)activity_main.xml:
<com.example.smyhvae.touchdemo.MyLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.smyhvae.touchdemo.MyButton
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按钮"/>
</com.example.smyhvae.touchdemo.MyLinearLayout>
上面的xml中,将我们自定义的MyLinearLayout和MyButton用上了。
(4)MainActivity.java:
1 package com.example.smyhvae.touchdemo;
2
3 import android.app.Activity;
4 import android.os.Bundle;
5 import android.widget.Button;
6
7 public class MainActivity extends Activity {
8
9 private static final String TAG = "MainActivity";
10 private Button btn;
11
12 @Override
13 protected void onCreate(Bundle savedInstanceState) {
14 super.onCreate(savedInstanceState);
15 setContentView(R.layout.activity_main);
16 btn = (Button) findViewById(R.id.btn);
17 }
18 }
分析之前,我们先记住下面这句话:(记住这句话,分析下面的日志就好理解了)
在Android中,一切事件处理的开始都是从Down事件开始的,如何你处理了Down事件,其他的事件就都收不到了。
1、按照上面的代码,后台日志如下:
通过上图的箭头处可以看到,事件是传递给了子view去消费。
2、上面的代码中,将MyLinearLayout.java中onTouchEvent方法返回值修改为true,将MyButton.java中的onTouchEvent方法返回值修改为false,后台日志如下:
通过上图的箭头处可以看到,事件是传递给了子view,子view说它不消费了,于是又回传给父的ViewGroup,ViewGroup消费了这个事件。
3、上面的代码中,将MyLinearLayout.java中onTouchEvent方法返回值修改为true,将MyLinearLayout.java中onInterceptTouchEvent方法返回值修改为true,后台日志如下:
通过上图的箭头处可以看到,此时ViewGroup已经将事件拦截了,所以根本就不会传递给子的Veiw,父的ViewGroup自己把事件给消费掉了。