您的位置:首页 > 移动开发 > Android开发

Android事件分发机制(一)

2017-08-29 15:29 302 查看
当手指触摸屏幕后会产生一系列的事件(如点击DOWN、移动MOVE、抬起UP等),事件的信息记录在MotionEvent(手势事件)对象中。这里说的事件分发机制,其实指的是MotionEvent的分发过程。当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View(ViewGroup继承自View),这个传递的过程就是事件分发过程。在这里先明确的说一下,当我们点击一个View的时候,手势事件并不是直接传给该View,而是经过了事件分发才传递到该View。

在细说事件分发机制之前,先说下View和ViewGroup关系,方便后续理解。Android的UI界面都是由View和ViewGroup,及他们的派生类组合而成的。其中,View是所有组件的基类,也就说ViewGroup本身也继承自View(所以,View包含了ViewGroup)。ViewGroup是容纳其他组件的容器。常用的布局RelativeLayout、LinearLayout、FrameLayout等都是继承父类ViewGroup来实现的。

好的,现在真正开始分析事件分发机制了。事件分发的过程由三个重要的方法共同完成:

public boolean dispatchTouchEvent(MotionEvent event)
处理事件的分发,所有View都有该方法。返回结果表示是否消耗当前事件。(下面两个方法都在dispatchTouchEvent方法里面被调用)
public boolean onInterceptTouchEvent(MotionEvent event)

用于拦截事件,只有ViewGroup才有该方法。在dispatchTouchEvent()方法中调用。返回结果表示是否拦截当前事件。主要作用是ViewGroup向其子控件分发手势事件之前,对相关事件进行拦截。 如果ViewGroup拦截了某个事件,那么在同一个事件序列(从手指按下到抬起发生的事件为一个事件序列)当中,此方法不会被再次调用。
public boolean onTouchEvent(MotionEvent event)

处理手势事件,所有View都有该方法。在dispatchTouchEvent()方法中调用。返回结果表示是否消耗当前事件。

然后再介绍下跟触摸事件相关的其他两个常用方法:

public void setOnTouchListener(OnTouchListener listener)
触摸事件传到当前的View时,回调该监听器。

public void setOnClickListener(OnClickListener listener)

当前的View被单击时,回调该监听器。(手指按下马上抬起为一个单击手势)

重点来了!上述几个方法的关系可以区分View和ViewGroup,用下面的伪代码表示。(理解下面的伪代码很重要,解释了事件分发机制的原理)

public boolean onTouchEvent(MotionEvent event) {
if (mDisable) { // 不可用则返会mClickable
return mClickable;
}
if (mClickable) {
if (event.getAction() == MotionEvent.ACTION_UP && 识别到为单击事件) {
mOnClickListenr.onClick(this);
}
return true; // 可点击的状态下一定返回true
}
return false; // 不可点击的状态下一定返回false
}


如果View是不可点击的(mClickable == false),则onTouchEvent()方法返回false,否则返回true.当可点击时,在手指抬起时,如果识别到是单击手势(按下马上抬起)则回调OnClickListener.onClick()

对于View
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {  // 通过调用setOnTouchListener()设置mOnTouchListener
return true;
}
return onTouchEvent(event); // 当识别到到单击手势时,OnClickListener在这里被回调(此时的动作为ACTION_UP)

}

在非容器类型的View中,dispatchTouchEvent()方法里面首先会回调TouchListener.onTouch()方法,如果该方法消费了事件返回true,则dispatchTouchEven()结束并返回true,onTouchEvent()方法则不会被调用。

对于ViewGroup
private View mTargetView = null; // 消费了ACTION_DOWN的目标控件
private boolean mDisallowIntercept = false; // 控制不允许容器拦截事件,默认为允许

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) { // ACTION_DOWN事件
mTargetView = null; // 一个事件序列开始,重置为空
if (!mDisallowIntercept  // 允许拦截。这里对mDisallowIntecept取反,当它的值为false时取反后条件才为真
&& onInterceptTouchEvent(ev)) { // 当前容器判断是否拦截
return super.dispatchTouchEvent(ev); // 调用上面View的dispatchTouchEvent()
} else {
for (int i = 0; i < childrens; i++) { // 把事件逐个分发给包含当前手势事件坐标的子控件
child = childrens[i];
if (child.dispatchTouchEvent(ev)) { // 子控件又递归调用dispathTouchEvent方法;如果事件被消费则停止向下分发
mTargetView = child; // 找到消费了事件的目标控件
return true;
}
}
return super.dispatchTouchEvent(ev); // // 调用上面的View.dispatchTouchEvent()
}

} else { // 其他事件
if (mTargetView == null) { // 说明ACTION_DOWN没有被消费或者中途事件被拦截了,则直接交由父容器处理
return super.dispatchTouchEvent(ev);
} else {
if (!mDisallowIntercept // 允许拦截。这里对mDisallowIntecept取反,当它的值为false时取反后条件才为真
&& onInterceptTouchEvent(ev)) { // 当前容器继续判断是否拦截
ev.setAction(MotionEvent.ACTION_CANCEL); // 通知目标控件事件被拦截了
mHasIntercepted = true; // 已拦截
mTargetView.dispatchTouchEvent(ev);
mTargetView = null; // 将目标控件置空,后续事件交由当前容器自己出来
return true;
} else { // 不拦截,事件继续交给目标控件
return mTargetView.dispatchTouchEvent(ev);
}
}

// 手指抬起
if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_MOVE) {
mDisallowIntercept = false; // 手指抬起时,重置为false,父容器下一次事件开始时又可以拦截事件了
}
}
}


dispatchTouchEvent()是事件分发的关键,其他方法都在这里被直接或间接调用。当一个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么后续的同一事件序列(从这次按下到手指抬起发生的一序列事件)中的其他事件都不会再分发给它去处理,而是交给它的父容器去处理。即如果当前容器下发的ACTION_DOWN的事件没有一个子控件去消费(此时mTargetView ==
null),则后续的事件则不会继续分发,直接由当前容器自己处理。如果子控件消费了事件,则后面容器仍有权限去拦截事件(通过onInterceptTouchEvent()方法判断是否拦截),不下发给子控件。可见,所有传到子控件的事件,都会经过它的父容器。

如果子控件不想父容器拦截事件,在收到ACTION_DOWN事件时调用父容器的requestDisallowInterceptTouchEvent(true),每次一个事件序列开始前mDisallowIntercept都被重置为false了,所以必须在ACTION_DOWN中通知父容器不要拦截事件。这里并不是让onInterceptTouchEvent()方法返回false,而是让onInterceptTouchEvent()根本不被执行。代码的第7行和第25号,if(条件一
&& 条件二 ),这里使用了逻辑运算符&&,“与”的作用,如果条件一为false,则条件二就完全不用去判断,if直接为false。这就能解释了,当mDisallowIntercept == true 时,也就是!mDisallowIntercept ==  false,onInterceptTouchEvent()就没有被执行的原因。

总之,MotionEvent是通过dispatchTouchEvent()方法一层层地分发下去,如果事件中途被消费,则停止分发。

在开发中,通常都是在Activity的onCreate()中调用setContentView(R.layout.custom_layout)来实现想要的页面布局。页面都是依附在窗口Window之上的,而DecorView即是窗口最顶层的视图。DecorView本身也继承FrameLayout,它里面的布局如下所示。



我们调用setContentView(),就是就是把我们的布局放在id为content的FrameLayout容器里,这也是为什么这个方法叫setContentView,而不是setView或其他名字啦。

而Activity中有两个方法 dispatchTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent event),它们俩的关系跟前面介绍的一样。下面是Activity中的方法注释。

/**
* Called to process touch screen events.  You can override this to
* intercept all touch screen events before they are dispatched to the
* window.  Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// getWindow()返回的是PhoneWindow的实例,查看PhoneWindow的代码,其实这里调用的是DecorView.dispatchTouchEvent()
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}


可见,触摸事件会优先分发到Activity的dispatchTouchEvent()中,然后通过调用 getWindow.dispatchTouchEvent()将事件继续分发到 DecorView中,再一步步分发到其他控件,如下图所示。



当一个事件产生后,传递顺序如下:Activity->Window->View,按照分发机制去分发事件。

现在我们用事实说话,写个简单的例子,验证上面所说的。

TouchActivity.java

public class TouchEventActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_touch);

// button1
View button1 = findViewById(R.id.button1);
button1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("Test", "button1::OnTouchListener");
return false;
}
});
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("Test", "button1::OnClickListener");
}
});

// button2
View button2 = findViewById(R.id.button2);
button2.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("Test", "button2::OnTouchListener");
return true; // 这里返回true,消费当前事件
}
});
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("Test", "button2::OnClickListener");
}
});

// button3
View button3 = findViewById(R.id.button3);
button3.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("Test", "button3::OnTouchListener");
return false;
}
});
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("Test", "button3::OnClickListener");
}
});
button3.setClickable(false);  // button3设置为不可点击!!!!!!
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("Test", "Activity::dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("Test", "Activity::onTouchEvent");
return super.onTouchEvent(event);
}
}


MyLayout.java

public class MyLayout extends LinearLayout {

public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("Test", "MyLayout::onInterceptTouchEvent");
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("Test", "MyLayout::dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("Test", "MyLayout::onTouchEvent");
return super.onTouchEvent(event);
}
}


activity_touch.xml

<?xml version="1.0" encoding="utf-8"?>
<com.example.huangziwei.myapplication.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button1"
/>

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button2"
/>

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button3"
/>
</com.example.huangziwei.myapplication.MyLayout>


运行界面:



点击BUTTON1时的输出Log:



点击BUTTON2时的输出Log:



还记得一开始说的吗?当我们点击一个View的时候,手势事件并不是直接传给该View,而是经过了事件分发才传递到该View。当点击按钮时,触摸事件首先传给Activity,经过一系列的分发,最后才传到点击的按钮上。如果在这分发的过程中,事件被拦截了,点击的按钮就没有反应。前面所说的就是剖析这个分发的过程是如何进行的。

BUTTON2不同于BUTTON1的地方是,BUTTON2在OnTouchListener里面拦截了事件,按照最上面说的伪代码,将导致onTouchEvent()方法不被执行,间接导致OnClickListener没被回调。

然后我们再看看点击BUTTON3时的输出Log:



BUTTON3不同于BUTTON1的地方是,BUTTON3设置为不可点击。这里需要再说明的是,当一个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么后续的同一事件序列中的其他事件都不会再分发给它去处理,而是交给它的父容器去处理。因此我们可以从Log看到,当触摸事件ACTION_DOWN开始时,会分发给所有的控件,如果都没有消费该事件,则后续的ACTION_UP直接交给了最顶层的Activity去处理,这样Activity的onTouchEvetn就能执行到了。

好的,终于写完了!!!事件分发机制的原理也就这么一回事了。接下来,我会再写一篇文章,从源码分析事件分发机制,也仅是再进一步验证上面所说的,以及加深下印象罢了。然后再稍微讲一下,根据事件分发机制如何去处理滑动冲突问题,相当于对所学知识的一个应用吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Android 事件分发