Android事件分发机制详解
Posted 王三的猫阿德
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android事件分发机制详解相关的知识,希望对你有一定的参考价值。
转载注明出处:http://blog.csdn.net/xiaohanluo/article/details/52416141
1. 概述
android日常研发时,与View接触占据相当多的时间,而关于View的知识,主要集中在View的绘制和View对于点击事件的处理。关于View的绘制过程,可以查看一下这篇文章的介绍;关于View处理点击事件,可能有人会认为在onTouchEvent()
这个方法处理点击事件就行了,不错,具体的处理过程确实是在这个方法中,但是点击事件在View间是怎么分发的?怎么确定当前View想要处理点击事件?这些问题在本篇文章中都会一一解决。
点击事件的分发,在具体的View中是有细小差别的,比如说LinearLayout中事件的分发和RelativeLayout中事件的分发就有一些不同的地方。所以本篇只介绍一个通用的事件分发机制,不针对具体View的源代码进行分析。
2. 点击事件类型
点击事件类型有好多种,参考MotionEvent
类,在这里主要介绍四种常见的事件类型。
类型 | 说明 |
---|---|
ACTION_DOWN | 手指接触到屏幕事件 |
ACTION_UP | 手指抬起离开屏幕事件 |
ACTION_MOVE | 手指在屏幕上面滑动事件 |
ACTION_CANCEL | 点击事件由于某种原因取消,比如说手指滑到屏幕外 |
一个正常完整的点击事件一般是从 ACTION_DOWN
事件(手指接触到屏幕)开始,到ACTION_UP
事件(手指抬起离开屏幕)结束,中间可以有滑动操作,如下所示。
ACTION_DOWN —> (ACTION_MOVE —> ACTION_MOVE —> ACTION_MOVE) —> ACTION_UP
注意手指滑动事件是一个连续的事件,在滑动过程中会一直触发,并不是只触发一次。
3. 方法介绍
点击事件分发机制中,一般会由三个方法控制,并不是上面所说的关注onTouchEvent()
方法就行了。
- dispatchTouchEvent(),事件的分发方法,一般由父布局调用,将点击事件传递到子View。返回true,代表事件被消费;返回false,表示事件未被消费,事件会继续传递下去。
- onInterceptTouchEvent(),是否拦截点击事件,如果返回true,表示拦截事件,调用自身
onTouchEvent()
处理点击事件;如果返回false,不拦截点击事件,则将点击事件传递到子View。 - onTouchEvent(),处理点击事件的具体方法
注意:View类和Activity类中仅仅有dispatchTouchEvent()和onTouchEvent()两个方法,并没有onInterceptTouchEvent()方法;上述三个方法在ViewGroup中都存在
注意:此文中”消费”一词的意思并不是指调用了onTouchEvent()方法,而是指onTouchEvent()方法或者OnTouchListener()返回了true,表示事件被消费
4. 点击事件分发过程
4.1 Activity事件处理
最先获取点击事件的是Activity,也就是所有View获取到的点击事件都是由Activity传递下去的,Activity会调用顶层的ViewGroup的dispatchTouchEvent()
方法,将事件分发给ViewGroup。看一下Activity这块的源代码。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
当接收到ACTION_DOWN
事件时候,调用了自身的onUserInteraction()
方法,这个方法是个空方法,然后通过调用顶层ViewGroup的dispatchTouchEvent ()
方法将事件传递给ViewGroup。如果在ViewGroup中将事件消费了,就直接返回true;如果没有消费,最终会调用Activity的onTouchEvent()
方法。也正如上面所说Activity中没有onInterceptTouchEvent()
方法。
4.2 ViewGroup事件处理
ViewGroup获取到点击事件后处理过程会根据具体的ViewGroup有不同的操作,这个过程都有一些细小的差别,但是大体思路是不变的,用伪代码来描述ViewGroup处理点击事件的过程。
public boolean dispatchTouchEvent(MotionEvent ev) {
// 如果拦截点击事件,不将该事件传递给子View,同时调用本身的onTouchEvent方法来处理事件,并返回布尔值标记是否消费该事件,
if (onInterceptTouchEvent(ev)) {
return onTouchEvent(ev);
}
// 如果不拦截点击事件,将事件传递给子View,并返回处理结果的布尔值标记是否消费该事件
if (isConsumed = child.dispatchTouchEvent(ev)){
return true;
}
// 如果子View也没有消费事件,事件又会从底层向上层传递,最终到activity
return onTouchEvent(ev);
}
ViewGroup拿到点击事件后,首先会调用onInterceptTouchEvent()
方法来判断是否需要拦截该事件,防止该事件继续传递到子View。如果需要拦截该事件,则返回true,调用本身的onTouchEvent()
方法处理该事件;如果不需要拦截该事件,调用子View的dispatchTouchEvent()
方法,将事件传递给子View。如果存在多层ViewGroup嵌套,事件的传递过程也是同样的。
4.3 View事件处理
将事件传递给最底层的View后,例如TextView,看看内部是怎么处理的,源代码太多,截取了比较重要的一段源代码,如下。
public boolean dispatchTouchEvent(MotionEvent event) {
// 其他逻辑
....
// 过滤一些不必要的事件
if (onFilterTouchEventForSecurity(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;
}
贴这一段的源代码主要是为了查看OnTouchListener
相对于onToucheEvent()
的优先级,可以看见,如果View设置了OnTouchListener
,并且它消费了点击事件,就不会调用View的onToucheEvent()
方法,顺便说一下OnClickListener
会在onTouchEvent()
方法内部被调用。整个调用的优先级如下。
OnTouchListener > onToucheEvent() > OnClickListener
4.4 点击事件分发小结
上面对Activity、ViewGroup、View对事件的处理过程进行了详细的介绍,主要集中于dispatchTouchEvent()
、onInterceptTouchEvent()
、onTouchEvent()
三个方法,从下面这个列表来总结一下。
Touch事件相关方法 | 功能说明 | Activity | ViewGroup | View |
---|---|---|---|---|
dispatchTouchEvent() | 事件分发,返回true,代表事件已经被消费;返回false,代表事件未被消费,事件会回传给父布局处理。 | 有该方法 | 有改方法 | 有该方法 |
onInterceptTouchEvent() | 事件拦截,返回true,代表当前控件要处理事件;返回false,代表当前控件不拦截事件,事件将会下发给子View | 无该方法 | 有该方法 | 无该方法 |
onTouchEvent() | 事件处理,返回true,代表事件被消费;返回false,代表事件未被消费 | 有该方法 | 有该方法 | 有该方法 |
5. Touch日志分析
新建一个Activity,复写里面的dispatchTouchEvent()
、onTouchEvent()
方法,仅仅添加日志,不做其他任何处理。
public class TouchDemoActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_touch);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "activity dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
}
boolean result = super.dispatchTouchEvent(ev);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "activity onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.onTouchEvent(event);
return result;
}
}
Activity的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wang.demo.touch.view.TouchLayout
android:id="@+id/touch_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wang.demo.touch.view.TouchView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.wang.demo.touch.view.TouchLayout>
</LinearLayout>
TouchLayout继承LinearLayout,并复写了dispatchTouchEvent()
、onInterceptTouchEvent()
、onTouchEvent()
三个方法,在这三个方法里面添加日志。
public class TouchLayout extends LinearLayout {
// 构造方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
}
boolean result = super.dispatchTouchEvent(ev);
return result;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout onInterceptTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
}
boolean result = super.onInterceptTouchEvent(ev);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "layout onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.onTouchEvent(event);
return result;
}
}
TouchView直接继承View,并复写了dispatchTouchEvent()
、onTouchEvent()
方法
,添加日志。
public class TouchView extends View {
// 构造方法
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TouchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "view dispatchTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.dispatchTouchEvent(event);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d(Constants.LOG_TAG_TOUCH, "view onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
}
boolean result = super.onTouchEvent(event);
return result;
}
}
5.1 事件未被消费
上面代码中,都是调用控件的默认方法对事件处理,也就是说,事件并没有被我们消费。点击屏幕并滑动最后抬起手指,看一下日志输出。
图-1 事件未被消费-日志
以上是关于Android事件分发机制详解的主要内容,如果未能解决你的问题,请参考以下文章
Android开发——事件分发机制详解---微信鱼虾蟹源码搭建