在上一篇文章安卓开发之事件处理机制中提到了安卓中事件被激发后需要被分发然后处理,前篇文章提到了基于监听和基于回调两种事件处理方式,这次就来学习下事件分发机制以及与事件处理的关系。
在学习之前先看一些安卓控件的基础知识,之后再系统学习,参考《Android开发艺术探索》、《Android群英传》。
Android中的每个控件都会在界面中占得一块矩形的区域,在Android中控件大致被分为两类,即ViewGroup控件与View控件。ViewGroup 控件作为父控件可以包含多个View控件,并管理其包含的View 控件。通过ViewGroup,整个界面上的控件形成了一个树形结构,这也就是我们常说的控件树,上层控件负责下层子控件的测量与绘制,并传递交互事件。通常在Activity中使用的findViewById()方法,就是在控件树中以树的深度优先遍历来查找对应元素。在每棵控件树的顶部,都有一个ViewParent对象,这就是整棵树的控制核心,所有的交互管理事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。View树结构如下图所示:
我们基于上图的模型来理解事件分发机制。接上篇文章,我们这里所说的事件就是指MotionEvent,事件分发实际上是对MotionEvent事件的分发过程。即当一个MotionEvent事件产生了以后,系统需要把这个事件传递给一个具体的 View去处理,这个传递的过程就是事件的分发,点击事件的分发过程由三个很重要的方法来共同完成: dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面先介绍一下这几个方法:
理解事件分发的过程,可以将上面的图简化成下图所示,想像成一个公司,总经理是ViewGroupA,部长是ViewGroupB,你是底层View
那么公司现在接到一个事件了,首先会传给总经理ViewGroupA,这时总经理ViewGroupA的dispatchTouchEvent方法就会被调用,如果总经理ViewGroupA的onInterceptTouchEvent方法返回为true,就说明他觉得这个事件子元素例如部长和你都不能处理,那么他自己就拦截这个事件然后处理,总经理的onTouchEvent方法就会被调用,当然默认情况下ViewGroup都不会拦截事件的,也就是返回值默认为false,如果返回为false的话,说明他觉得事件可以交给子元素处理,那么他不会拦截这个事件,然后当前事件就会继续传递给它的子元素(例如ViewGroupB),接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。当然也存在事件最后虽然交给你View来处理了,但是你也不能处理的情况,也就是onTouchEvent返回为false的情况,这时候就只能把事情交给上级去处理了,直到某上级处理完事件。看到这里是不是和上篇文章里基于回调的事件传播部分(如下图所示)说的感觉有点像呢?
当一个View要处理某个事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会首先被回调。这时事件如何处理要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不
会被调用。由此可见,给View设置的OnTouchListener,其优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。可以看出平时常用的OnClickListener, 其优先级最低,即处于事件传递的尾端。
当然上面的流程是整个事件分发中的一部分,在Android中产生点击事件后,遵循的顺序实际上为:Activity -> Window -> 顶级View,即事件总是先传递给Activity, Activity 再传递给Window,最后Window再传递给顶级View。顶级View 接收到事件后,就会按照上面说的事件分发流程去分发事件。最后如果onTouch和View的onTouchEvent方法返回都为false的,就会交给父容器的onTouchEvent处理,如果Activity的子元素一路都返回false无法处理事件的话,那么最终这个事件就会由Activity来处理,因为它是最终的大领导,所以不管Activity能不能成功处理,也不会再由其他对象来处理该事件了。
这里面提到了Window,如下图所示,每个Activity 都包含一个 Window对象,在Android中Window对象通常由PhoneWindow来实现。PhoneWindow 将一个DecorView 设置为整个应用窗口的根View 。DecorView作为窗口界面的顶层视图,封装了一些窗口操作的通用方法。可以说,DecorView将要显示的具体内容呈现在了PhoneWindow上, 这里面的所有View 的监听事件,都通过WindowManagerService来进行接收,并通过Activity 对象来回调相应的onClickListener。在显示上,它将屏幕分成两部分,一个是TitleView, 另一个是ContentView。 看到这里,大家一定看见了一个非常熟悉的布局ContentView。 它是一个id为content 的Framelayout,activity_ main.xml 就是设置在这样一个 Framelayout 里。Android中通过在Activity中使用setContentView()方法来设置一个 布局,在调用该方法后,ActivityManagerService会回调onResume()方法, 此时系统才会把整个DecorView 添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制,布局内容就算真正显示出来了。
要理解这几者之间的关系,参考Activity、View、Window的理解一篇文章就够了
Activity就像工匠,Window就像是窗户,View就像是窗花,LayoutInflater像剪刀,Xml配置像窗花图纸。
回到正题,模拟上面模型图写一个Demo来验证下上面的流程,代码参考《安卓群英传》。
Demo:
新建ViewGroupA.java:
public class ViewGroupA extends LinearLayout {
public ViewGroupA(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("MEvent:","ViewGroupA dispatchTouchEvent执行了");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("MEvent:","ViewGroupA onInterceptTouchEvent执行了");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("MEvent","ViewGroupA onTouchEvent执行了");
return super.onTouchEvent(event);
}
}
ViewGroupB.java:
public class ViewGroupB extends LinearLayout {
public ViewGroupB(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("MEvent:","ViewGroupB dispatchTouchEvent执行了");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("MEvent:","ViewGroupB onInterceptTouchEvent执行了");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("MEvent","ViewGroupB onTouchEvent执行了");
return super.onTouchEvent(event);
}
}
MyView.java:
public class MyView extends View {
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("MEvent:","View dispatchTouchEvent执行了");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("MEvent","View onTouchEvent执行了");
return super.onTouchEvent(event);
}
}
Activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.demo.mevent.ViewGroupA
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="#ff0033">
<com.demo.mevent.ViewGroupB
android:layout_width="200dp"
android:layout_height="200dp"
android:gravity="center"
android:background="#336699">
<com.demo.mevent.MyView
android:layout_width="100dp"
android:layout_height="100dp"
android:clickable="true"
android:background="#ffff00"/>
</com.demo.mevent.ViewGroupB>
</com.demo.mevent.ViewGroupA>
</LinearLayout>
UI界面效果如下:
点击中间View后查看Logcat:
可以看到事件的传递流程: ViewGroupA首先得到点击事件,并由它的dispatchTouchEvent方法来分发,由于它的onInterceptTouchEvent方法默认返回false没有做出拦截,因此事件就传递给了子元素ViewGroupB,而同样由于ViewGroupB的onInterceptTouchEvent方法在它的dispatchTouchEvent方法分发事件时没有做出拦截,所以事件最终被传递给MyView,由MyView来处理这个事件。
如果MyView的onTouchEvent方法返回值为false,也就是它也无法处理这个事件:
这时候再点击如下图所示:
MyView的onTouchEvent方法虽然执行了,但是它无法处理,于是交给部长ViewGroupB,由它的onTouchEvent来处理这个事件,但是由于部长ViewGroupB的onTouchEvent也没有成功处理这个事件,所以这个事件又传递给总经理ViewGroupA,由它的onTouchEvent方法来处理这个事件,这时候由于它是大领导,所以不管ViewGroupA能不能成功处理,也不会再有其他对象来处理该事件了。
假如部长ViewGroupB的onTouchEvent返回为true,也就是他成功处理了这个事件了,这种情况下就不用ViewGroupA去处理了:
如果在事件分发的过程中部长ViewGroupB拦截了这个事件,也就是onInterceptTouchEvent返回了true,这时候就没你View什么事了:
事件传递到部长ViewGroupB就终止传递,由ViewGroupB的onTouchEvent方法来处理事件,这时候假如部长ViewGroupB的onTouchEvent返回为true表明他已经成功处理了这个事件,因此也不会再返回给ViewGroupA的onTouchEvent来处理了。
Demo看完了,让我们再从源码角度分析下上述的过程。参考Android笔记-从ViewGroup的dispatchTouchEvent源码分析事件分发机制
前面说到,当点击事件发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件分发,具体的工作是由Activity 内部的Window来完成的。Window会将事件传递给DecorView,DecorView一般就是当前界面的底层容器( 即setContentView 所设置的View 的父容器),因此首先从Activity的dispatchTouchEvent开始分析。
public boolean dispatchTouchEvent(MotionEvent ev) {
// 一般事件列开始都是DOWN按下事件
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
// 若getWindow().superDispatchTouchEvent(ev)返回true,则Activity.dispatchTouchEvent()就返回true
// 整个事件循环就结束了,否则返回为false意味着没人能处理,那么就会继续往下调用Activity.onTouchEvent方法
}
return onTouchEvent(ev);
}
注释中说的很清楚了,首先事件交给Activity 所附属的Window 进行分发,如果返回为true,整个事件循环就结束了,否则Activity.onTouchEvent方法会被执行。
那么Window是如何将事件传递给ViewGroup 的呢?在理解这个问题之前先要知道一个事,Window其实是个抽象类,它里面的superDispatchTouchEvent方法是个抽象方法,因此必须找到它的实现类,这个实现类就是我们上面说的PhoneWindow,也就是此处的Window类对象实际上指的是PhoneWindow类对象(多态),所以我们直接去看PhoneWindow的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
// mDecor是顶层View(DecorView)的实例对象
return mDecor.superDispatchTouchEvent(event);
}
可以看到PhoneWindow 将事件直接传递给了前面说到过的DecorView,DecorView类是PhoneWindow类的一个内部类,它继承自FrameLayout,是所有界面的父类,而FrameLayout是ViewGroup的子类,所以DecorView的间接父类是ViewGroup。之后的过程就和前面说的事件分发一样了,会先调用ViewGroup的dispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
// 调用父类的方法即ViewGroup的dispatchTouchEvent方法,将事件传递到ViewGroup去处理
return super.dispatchTouchEvent(event);
}
点击事件达到顶级View (一般是ViewGroup) 以后,会调用ViewGroup 的dispatchTouchEvent方法,那么接上文就看下ViewGroup的dispatchTouchEvent方法,这里直接看伪代码,详细可以参考:
public boolean dispatchTouchEvent(MotionEvent event) {
if(onInterceptTouchEvent(event)){//是否拦截
return onTouchEvent(event);
}
//没有拦截
if(child==null){
//没有子控件
return onTouchEvent(event);
}else{
//执行子控件的dispatchTouchEvent
boolean consume= child.dispatchTouchEvent(event);
if(!consume){//子控件没有消费事件,执行当前view的onTouchEvent
return onTouchEvent(event);
}else{
return false;
}
}
}
大致逻辑和前面说的一样:如果顶级ViewGroup 拦截事件即onInterceptTouchEvent返回为值true, 那么事件就直接由ViewGroup 处理,这时如果ViewGroup 的mOnTouchListener被设置,则onTouch会首先被调用,否则onTouchEvent会被调用。如果顶级ViewGroup的onInterceptTouchEvent返回值为false即它不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用。到此为止,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级ViewGroup是一致的,如此循环,完成整个事件的分发。
那么接下来就直接看子View的dispatchTouchEvent方法,当然这里包括前面对于不同安卓版本来说代码是不同的,只是看下大致的思想:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
......
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;
}
从上面可以大致了解View对事件的处理过程,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用。如果没有设置OnTouchListener或者onTouch方法返回false,那么onTouchEvent就会被调用,我们直接看View的onTouchEvent方法中对于点击事件的处理:
public boolean onTouchEvent(MotionEvent event) {
......
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
// 若当前的事件为抬起View(主要分析)
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
...// 种种判断
performClick();
break;
// 若当前的事件为按下View
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
//若当前的事件为结束事件
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
// 若当前的事件为滑动View
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
removeLongPressCallback();
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
// 若该控件可点击,就一定返回true
return true;
}
// 若该控件不可点击,就一定返回false
return false;
}
public boolean performClick() {
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
当ACTION_ UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick 方法内部会调用它的onClick方法。大致流程就是这样,具体的可以去看源码深入分析。