这是我参与更文挑战的第2天,活动详情查看: 更文挑战
首先,事件分发的对象是谁?
事件。
当用户触摸屏幕时( View 或 ViewGroup 派生的控件),将产生点击事件(Touch事件)
Touch事件相关细节,比如触摸位置,时间,手势等等,会被封装成 MotionEvent 对象。
Touch 事件主要有以下几种:
事件 | 简介 |
---|---|
ACTION_DOWN | 手指 初次接触到屏幕 时触发。 |
ACTION_MOVE | 手指 在屏幕上滑动 时触发,会多次触发。 |
ACTION_UP | 手指 离开屏幕 时触发。 |
ACTION_CANCEL | 事件 被上层拦截 时触发。 |
事件列:从手指接触屏幕至手指离开屏幕,这个过程产生一系列时间,任何时间都是以Down事件开始,UP事件结束,中间会有无数Move事件。
也就是说,当一个 MotionEvent 产生后,系统需要把这个事件传递给一个具体View去处理。
什么是事件分发?
要知道什么是事件分发,其实也就是对MotionEvent 事件的分发过程,也就是当一个 手指 按下之后,系统需要把这个事件传递给一个具体的 View,而这个传递的过程就是分发过程。
分发顺序也就是 Activity(Window)-> ViewGroup -> View.
三大方法: - >(入门)
说到分发过程,就不得不说这三个方法了:
- dispatchTouchEvent,
- onInterceptTouchEvent
- onTouchEvent
先介绍一下这三个方法分别是干啥的:
dispatchTouchEvent
用来进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View 的 onTouchEvent 和 下级View的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。
onInterceptTouchEvent
在 dispatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
onTouchEvent
在 'dispatchTouchEvent '方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View 无法再次接受到事件。
用一个图来解释
√
表示有该方法。
X
表示没有该方法。
类型 | 相关方法 | ViewGroup | View |
---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ |
事件拦截 | onInterceptTouchEvent | √ | X |
事件消费 | onTouchEvent | √ | √ |
看到这,不知道你们有没有这个疑问,为啥View会有 dispatchTouchEvent 方法?
一个View 可以注册很多监听器吧,例如单击,长按,触摸事件(onTouch),并且View 本身也有 onTouchEvent 方法,那么问题来了,这么多事件相关的方法应该由谁管理,所以View也会有这个 方法。
他们之间的关系我们可以通过以下的伪代码来看:
// 点击事件产生后,会直接调用dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {
//代表是否消耗事件
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
//如果onInterceptTouchEvent()返回true则代表当前View拦截了点击事件
//则该点击事件则会交给当前View进行处理
//即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;
} else {
//如果onInterceptTouchEvent()返回false则代表当前View不拦截点击事件
//则该点击事件则会继续传递给它的子元素
//子元素的dispatchTouchEvent()就会被调用,重复上述过程
//直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}
return consume;
}
复制代码
通过上面的伪代码我们可以发现,对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时他的 dispatchTouEvent 就会调用,如果这个 ViewGroup 的 onInterceptTouchEvent方法返回true,就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 处理,即它的 onTouchEvent 方法会被调用,如果这个 ViewGroup 的onInterceptTouchEvent 方法返回false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的 子元素,接着子元素 dispatchTouEvent 方法就会被调用。如此反复知道事件最终被处理。
用一张搬运过来的事件分发流程图来说明一下:
当一个View需要处理事件时,如果它设置了 OnTouchListener, 那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回false,则当前View 的onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会被调用。由此可见,给View设置 onTouchListener,其优先级比 onTouchEvent 还要高。在 onTouchEvent 方法中,如果当前设置有 onClickListener,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 onClickListener,其优先度最低,即处于事件传递的尾端.
通过以下源码即可看出:
public boolean dispatchTouchEvent(MotionEvent event) {
......
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;
}
复制代码
关于事件传递机制,我们可以总结出以下结论,根据这些结论能更好的理解整个传递机制:(摘录自Android开发艺术探索)
- 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的的一系列时间,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。
- 正常情况下,一个事件序列只能被一个 View 拦截且消耗。这一条原因可以参考(3),因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个 View同时处理,但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件 通过 onTouchEvent 强行传递给其他View处理。
- 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。这条也很好理解,就是说当一个 View 决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不再调用这个View的 onInterceptTouchEvent 去询问它是否要拦截了。
- 某个View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件( onTOuchEvent 返回了 false ) ,那么同一事件序列中的其他事件都不会交给 它来处理,并且事件将重新交由它的父元素去处理,即父元素的 onTouchEvent 会被调用。意思就是事件一旦交给一个 View 处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序猿一件事,如果事情没有处理好,短期内上级就不敢再把事情交给程序猿去做了。二者道理差不多。
- 如果View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
- ViewGroup 默认不拦截任何事件。Android 源码中ViewGroup 的 onInterceptTouchEvent 方法默认返回false.
- View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
- View 的 onTouchEvent 默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false).View 的 longClickable 属性默认为 false,clickable 属性要分情况,比如Button 的clickable 属性默认为true,而 TextView 的 clickable 属性默认为 false.
- View 的 enable 属性不受影响 onTouchEvent 的 默认返回值,哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个 为 true,那么它的 onTouchEvent 就返回 true.
- onClick 会发生的前提是当前View 是可点击的,并且它收到了 down 和 up 的事件。
- 事件传递过程是由外而向的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。
实例: ->(实践)
结合上面的结论,我们来用实例来演示一下
首先用这样一个图来看
这是一个简单的布局,Activity里面一个LinearLayout,用来代替ViewGroup,内部是一个Button,没什么说的。
我们要从如下几个方面入手,探究上面那些结论。
- 默认情况下的事件分发
- 拦截时的分发情况
- 如果viewagroup 不拦截 Down事件,即还是 Button 来处理,但它拦截接下来的move事件(也就是半路拦截),那么接下来情况又会是咋样?
- 如果全部都不消费事件,事件最终由谁来安排。
- onTouch中返回 true或者false,对onTouchEvent有什么影响吗。
上代码啦:
首先继承自 LinearLayout,为了重写相应的 三大方法。
/**
* @author Petterp on 2019/7/2
* Summary:重写LinearLayout相应方法
* 邮箱:1509492795@qq.com
*/
public class LinearLayoutView extends LinearLayout {
public LinearLayoutView(Context context) {
super(context);
}
public LinearLayoutView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 事件分发
*
* @param ev
* @return 是否消费当前事件
* true-> 消费事件,后续事件继续分发到该view
* false->
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
/**
* 事件拦截,即自己处理事件
*
* @param ev
* @return 是否拦截当前事件
* true->事件停止传递,执行自己onTouchEvent,该方法后续不再调用
* false -> 事件继续向下传递,调用子view.dispatchTouchEvent(),该方法后续仍被调用
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("demo", "viewgroup-onInterceptTouchEvent: "+ ViewActivity.mode);
return ViewActivity.mode;
}
/**
* 处理点击事件
*
* @param event
* @return 是否消费当前事件
* true-> 事件停止传递,后续事件由其处理
* false-> 不处理,事件交由父 onTouchEvent()处理,此view不再接受此事件列的其他事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
}
复制代码
接着是Activity:
public class ViewActivity extends AppCompatActivity {
//mode标记位
public static boolean mode=false;
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view);
Button btn_1=findViewById(R.id.btn_t1);
LinearLayoutView linearLayout=findViewById(R.id.ln_group);
//LinearLayout 即viewGroup
linearLayout.setOnClickListener(v -> Log.e("demo","viewgroup-onClick"));
linearLayout.setOnTouchListener((v, event) -> {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e("demo","viewgroup-手指按下");
break;
case MotionEvent.ACTION_UP:
Log.e("demo","viewgroup-手指放开");
break;
case MotionEvent.ACTION_MOVE:
Log.e("demo","viewgroup-手指移动");
break;
}
return false;
});
//button 即子view
btn_1.setOnClickListener(v -> Log.e("view-demo","onClick"));
btn_1.setOnTouchListener((v, event) -> {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e("demo","view-手指按下");
break;
case MotionEvent.ACTION_UP:
Log.e("demo","view-手指放开");
break;
case MotionEvent.ACTION_MOVE:
Log.e("demo","view-手指移动");
break;
case MotionEvent.ACTION_CANCEL:
Log.e("demo","view-被父view截断了");
break;
}
return false;
});
}
/**
* 重写Activity onTouchEvent,
* 模拟子view不消费事件时的处理情况
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("demo","Activity-自己消化");
return super.onTouchEvent(event);
}
}
复制代码
相应的注释简洁明了,不用多说了吧。
xml
<?xml version="1.0" encoding="utf-8"?>
<com.petterp.studybook.View.LinearLayoutView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ln_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".View.ViewActivity">
<Button
android:id="@+id/btn_t1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
tools:ignore="HardcodedText" />
</com.petterp.studybook.View.LinearLayoutView>
复制代码
首先看看默认情况下的事件分发过程:
手指-> 点击Button,再点击空白处(即Viewgroup)。观察日志打印:
结论:默认情况下,viewgroup 拦截器返回false,事件会传递到子view的 dispatchTouchEvent 然后继续分发,直到最后被 子view 的onTouchEvent 所消费,这时候会调用 onclick方法,所以 onclick处于优先级最低。
拦截情况下的事件分发,比如,ViewGroup 的onInterceptTouchEvent返回true呢?
更改代码
//mode标记位
public static boolean mode=true;
复制代码
默认这里是false,当然这个是我自己写的一个标记位,真实情况肯定不是这样。只是为了模拟效果。现在改为true,这样的话LinearLayout 的onInterceptTouchEvent 将返回true,也就是ViewGroup 消费了此事件。
手指 -> 点击Button,再点击 空白处(即ViewGroup),观察日志打印:
可以发现,viewGroup已经消费了此次事件,这时无论点击什么位置,事件都不会传递到子view。
结论:当 dispatchTouchEvent 拦截了此次事件,那么接下来的事件序列都不会向下传递。都由此view处理。
如果我们先不拦截,当点击之后再拦截子view事件,这时候又会是什么情况?
修改代码:
//mode标记位
public static boolean mode=false;
...
btn_1.setOnTouchListener((v, event) -> {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e("demo","view-手指按下");
mode=true;
break;
...
}
return false;
});
复制代码
手指 -> button,然后稍微移动一下松开:
是不是发现此次事件序列在被拦截时传递了一个 ACTION_CANCEL 给子view,而以后后续事件都不会再向下传递。
结论:当一个事件 被 onInterceptTouchEvent 返回true 中途拦截时,会传递 ACTION_CANCEL 给view的 onTouchEvent方法。后续的 move等事件,会直接传递给 拦截者 的 onTouchEvent( ) 方法。而且后续事件不会在传递给 其 onInterceptTouchEvent 方法,该方法一旦返回一次true,就再也不会被调用了。
如果都不处理,消费此次事件,会是怎样的呢?
修改代码:
这里为了演示,因为默认的 onInterceptTouchEvent 返回true,懒得修改Button,所以我们尝试用ViewGroup拦截事件,然后不消费它,也就是 onTOuchEvent 返回false。
//mode标记位
public static boolean mode=false;
LinearLayoutView ->更改代码
public boolean onTouchEvent(MotionEvent event) {
Log.e("demo","viewgroup-我不消费");
return false;
}
复制代码
手指-> button 按下轻轻移动:
结论:这也就是我们常说的责任链模式,层层传递事件,决定分发dispatchTouchEvent,最终由onTouchEvent接收,如果子不消费,就继续向上,直到Activity自己消费。Activity这里,其实无论返回true还是false,都会消费事件。
onTouch中返回 true或者false,对onTouchEvent有什么影响吗?
先将代码恢复如初,然后更改
Activity:
linearLayout.setOnTouchListener((v, event) -> {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e("demo","viewgroup-手指按下");
break;
case MotionEvent.ACTION_UP:
Log.e("demo","viewgroup-手指放开");
break;
case MotionEvent.ACTION_MOVE:
Log.e("demo","viewgroup-手指移动");
break;
}
return true;
});
LinearLayout:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean mode=super.onInterceptTouchEvent(ev);
Log.e("demo", "viewgroup-onInterceptTouchEvent: "+ mode);
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean mode=super.onTouchEvent(event);
Log.e("demo","viewgroup-onTouchEvent");
return mode;
}
复制代码
手指-> 按下空白区域再松开:
接着修改代码
Activity:
linearLayout.setOnTouchListener((v, event) -> {
...
return false;
});
复制代码
手指-> 按下空白区域再松开:
结论:可以发现 onTouch方法优先于 onTouchEvent执行。具体的原因可以看我下一篇 Android事件分发源码解析。
感谢
- GcsSloop
- Android开发艺术探索
更多Android开发知识请访问—— CloudBook .