当手指触摸屏幕后会产生一系列的事件(如点击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)
- public boolean onInterceptTouchEvent(MotionEvent event)
- public boolean onTouchEvent(MotionEvent event)
- public void setOnTouchListener(OnTouchListener listener)
- public void setOnClickListener(OnClickListener listener)
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
}
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()方法则不会被调用。
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,父容器下一次事件开始时又可以拦截事件了
}
}
}
/**
* 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);
}
}
<?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>