在Android中遇到多重嵌套时通常会导致 触摸监听的冲突,今天就来介绍一下解决方案,Android中的Touch事件的分发机制。
首先讲解一下分发机制的概念,什么是分发机制呢?分发机制就是对Touch事件消费权限的处理:
Android中发生一个Touch事件时,第一个事件必然是ACTION_DOWN事件,我们通常是直接在onTouchEvent()方法中直接处理这个事件以及之后的ACTION_MOVE和ACTION_UP事件,但当有两个或以上布局嵌套时还是这样处理的话就出现了各种各样的冲突问题,这个事件应该给谁消费?,这时就需要用到分发机制来为多重嵌套为View分配权限,Android Touch分发通过几个方法实现,分别是:
dispatchTouchEvent(MotionEvent ev)
onInterceptTouchEvent(MotionEvent ev)
onTouchEvent(MotionEvent ev)
当用户触碰屏幕后,第一个产生ACTION_DOWN事件,这个事件首先会被Activity捕获,然后分发到Windows接着才会被最外层的布局接收收到,并由其dispatchTouchEvent方法处理,然后在dispatchTouchEvent中可以通过返回值来决定这个事件接下来的去向,它可以选择停止这个事件,那么这个DOWN事件就此结束;或是选择消费这个事件(即自己处理这个事件);再或者将事件转交给子级的控件去处理,如果这么做,那么下一层的控件的dispatchTouchEvent就会捕获这个事件,然后重复上一级的操作,以此类推,直到有控件选择消费这个事件,就会停止分发并对事件进行处理
(当一个View消费了DOWN事件后,之后的MOVE、UP事件都会由它来处理,除非被父级拦截;反之如果一个View放弃了对DOWN的处理权,那么它将退出本次交互,在下一个DOWN事件发生之前,它不会在接收到其他事件,可以理解为只有DOWN事件会被分发)。
方法介绍:
dispatchTouchEvent(MotionEvent ev)
Touch事件发生后第一个捕获事件的方法返回值为true:停止事件,直接在dispatchTouchEvent内被消费,事件就此结束
返回值为false:当事件为DOWN时,表示放弃对事件的处理权,将事件返回给父级消费,这时事件会直接交给父级的onTouchEvent处理,且之后的MOVE、UP事件不会再传递到这里
而当事件不为DOWN时,表示DOWN已经被这个view消费了,那么当前处理权就在这个view,即使返回false放弃事件,也不会传递到父级,而是直接废弃,而且这只是表示放弃这一次MOVE,下一次的MOVE依然会传递到这里;返回值为super.dispatchTouchEvent(ev),这里分两种情况:
1、当前控件继承于ViewGroup时(可容纳子View):调用ViewGroup的 dispatchTouchEvent() 方法,事件被传递到当前控件的onInterceptTouchEvent()方法,在onInterceptTouchEvent中决定事件的去向。
2、当前控件直接继承与View时(最小单元,无子View):调用View的 dispatchTouchEvent() 方法,View为最小单元,不会存在子View了,所以无法继续分发,事件将被直接传递到当前View的onTouchEvent()中进行处理
onInterceptTouchEvent(MotionEvent ev)
事件拦截,从dispatchTouchEvent方法得到事件后,继续对事件进行分发返回值为true,分两种情况:
1、事件为DOWN或者DOWN事件已被当前控件消费时:直接消费此事件,将事件交由当前View的onTouch处理
2、DOWN事件已被子级或更低级的View消费时:拦截并消费事件,先向子级View发送CANCEL事件(取消监听),子级会继续分发CANCEL事件,知道子级和子级之下所有view都停止对事件的监听,然后当前View消费此事件,事件交由onTouch处理,并且在本次交互中(每一次的DOWN—UP是一个交互),剩下的事件都由当前控件处理,其子级不在参与,若当前View的父级在之后某次对事件进行分发时,在其onInterceptTouchEvent()中返回了true进行拦截,那么当前View的命运将和他的子级一样返回值为false:事件放行,将事件交给子级View处理。
返回值为super.onTouchEvent(ev) :默认拦截事件,由当前控件消费,事件由当前View的onTouchEvent()处理
onTouchEvent(MotionEvent ev)
事件消费,在这里对事件进行需要的处理,之后依然需要返回值返回值为true:消费事件并停止传递,事件到此结束。
返回值为false,前面说过只有DOWN事件会被分发,所以这里就产生了两种情况:
1、当前事件为DOWN:表示放弃DOWN事件,将事件交由父级的onTouchEvent()处理,且在本次交互中不会再收到其他事件。
2、当前事件不为DOWN:表示DOWN事件已被当前控件消费了,也就是说现在的事件处理权就在当前控件,所以不会再交给父级处理,直接终止此事件返回值为super.onTouch(ev),分三种情况:
1、当前事件为DOWN:激活onLongClick(),开始计时,达到触发时间标准时,onLongClick()事件会被触发
2、当前事件为UP:这里又分为两种情况 1、当onLongClick()事件还没有被触发,那么这里会触发onClick()事件,然后事件结束 2、如果onLongClick()已被触发,那么根据其返回值决定,如果onLongClick返回值为true:那么表示事件被onLongClick消费,事件就此结束;如果返回值为false:表示继续传递事件,将会触发onClick()事件,然后事件结束。
实例
这里用一个实例测试一下,目录如上
这里有三个自定义View,LayoutView2、LayoutView3是ViewGroup,继承与LinearLayout;MyView直接继承于View,嵌套方式为MyView–>LayoutView2–>LayoutView3,三个控件分别重写了事件分发需要的方法,并对每一种事件输出一个Log,代码我贴在文章结尾,先看看运行结果:
上面是我点击了一下MyView的结果,Log 中 1代表MyView、2代表LayoutView2、3代表LayoutView3
可以看到,如前文介绍
1、事件先由最外层的Layout3的dispatchTouchEvent方法捕获
2、然后将事件传递到了 onInterceptTouchEvent方法中,在onInterceptTouchEvent中放行
3、接着layout2的dispatchTouchEvent就得到了事件,然后交给onInterceptTouchEvent,继续放行
4、在MyView的dispatchTouchEvent接到事件后,它没有子级,所以事件直接被交给了onTouchEvent,最后在onLongClick触发之前出发了onClick,事件结束。
当前返回值是全部放行状态,大家可以更改dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,看看结果的变化,这里就不一一举例了,代码如下:
public class LayoutView3 extends LinearLayout {
private final String TAG = "DispatchTest";
public LayoutView3(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "3:dispatchTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "3:dispatchTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "3:dispatchTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "3:dispatchTouchEvent action:ACTION_CANCEL");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "3:onInterceptTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "3:onInterceptTouchEvent action:ACTION_MOVE");
float nowX = ev.getX();
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "3:onInterceptTouchEvent action:ACTION_UP");
//return true;
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "3:onInterceptTouchEvent action:ACTION_CANCEL");
break;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "3:onTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "3:onTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "3:onTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "3:onTouchEvent action:ACTION_CANCEL");
break;
}
return true;
}
}
public class LayoutView2 extends LinearLayout {
private final String TAG = "DispatchTest";
public LayoutView2(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "2:dispatchTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "2:dispatchTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "2:dispatchTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "2:dispatchTouchEvent action:ACTION_CANCEL");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch(action){
case MotionEvent.ACTION_DOWN:
Log.d(TAG,"2:onInterceptTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG,"2:onInterceptTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG,"2:onInterceptTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG,"2:onInterceptTouchEvent action:ACTION_CANCEL");
break;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch(action){
case MotionEvent.ACTION_DOWN:
Log.d(TAG,"2:onTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG,"2:onTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG,"2:onTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG,"2:onTouchEvent action:ACTION_CANCEL");
break;
}
return false;
}
}
public class MyView extends View {
private final String TAG = "DispatchTest";
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "1:onClick");
}
});
this.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.d(TAG, "1:onLongClick");
return true;
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "1:dispatchTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "1:dispatchTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "1:dispatchTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "1:dispatchTouchEvent action:ACTION_CANCEL");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "1:onTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "1:onTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "1:onTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "1:onTouchEvent action:ACTION_CANCEL");
break;
}
return super.onTouchEvent(ev);
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.sikang.dispatchtest.MainActivity">
<com.example.sikang.dispatchtest.LayoutView3
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.sikang.dispatchtest.LayoutView2
android:layout_width="1000px"
android:layout_height="1000px"
android:layout_gravity="center"
android:background="#FFFFFF">
<com.example.sikang.dispatchtest.MyView
android:id="@+id/myView"
android:layout_width="500px"
android:layout_height="500px"
android:layout_gravity="center"
android:background="#000000">
</com.example.sikang.dispatchtest.MyView>
</com.example.sikang.dispatchtest.LayoutView2>
</com.example.sikang.dispatchtest.LayoutView3>
</RelativeLayout>