之前看郭神的事件分发机制解析博客,深入的从源码角度学习了一下Android的事件分发机制。
如果还没看过,那么请先去郭神的博客学习下:http://blog.csdn.net/guolin_blog/article/details/9097463
了解到事件从ViewGroup向子View分发的过程中,需要走一个判断,如果这个判断语句没有过,那么事件将被父布局给拦截掉,这个判断语句是:
if(disallowIntercept || !onInterceptTouchEvent(ev))
其中disallowIntercept这个标志表示是否禁用事件拦截功能,默认情况都是false,所以一般情况下,是否拦截事件就得看后面的!onInterceptTouchEvent(ev)
这个语句的正负值了,假设我们把这里的onInterceptTouchEvent(ev)
重写并始终返回为true,那么子view将会永远得不到事件的分发,所有的事件都会被ViewGroup拦截。
平时开发过程中可能会有这种需求:在触控的时候,按下那下(ACTION_DOWN)需要子View进行事件反馈,但在滑动(ACTION_MOVE)或者抬起(ACTION_UP)的时候需要父布局进行拦截操作,这个时候就要用到我们的requestDisallInterceptRouchEvent(boolean disallowIntercept)
方法了,该方法可以在子类通过调用getParent().requestDisallInterceptRouchEvent(true)
来让父布局禁用拦截事件功能,从而父布局忽略该事件之后的一切Action,话不多说,来看看代码:
首先先自定义一个布局,继承自LinearLayout,同时我们重写了onInterceptTouchEvent方法,让父类拦截ACTION_MOVE和ACTION_UP,如下所示:
public class MyLayout extends LinearLayout{
public MyLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false;
case MotionEvent.ACTION_MOVE: //表示父类需要
return true;
case MotionEvent.ACTION_UP:
return true;
default:
break;
}
return false; //如果设置拦截,除了down,其他都是父类处理
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d("TAG", "You down layout");
break;
case MotionEvent.ACTION_UP:
Log.d("TAG", "You up layout");
break;
case MotionEvent.ACTION_MOVE:
Log.d("TAG", "You move layout");
}
return true;
}
}
然后,我们再自定义一个按钮,同时重写它的onTouchEvent()方法,打印一下action的log,同时我们在ACTION_DOWN中加上 getParent().requestDisallowInterceptTouchEvent(true)
,表示在DOWN的时候屏蔽父类的事件屏蔽,如下所示:
public class MyButton extends Button {
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
Log.d("TAG", "You down button");
break;
case MotionEvent.ACTION_UP:
Log.d("TAG", "You up button");
break;
case MotionEvent.ACTION_MOVE:
Log.d("TAG", "You move button");
}
return true;
}
}
这里需要提一点,retuan true
是告诉系统当前的View需要处理这次的touch事件,以后的系统发出的ACTION_MOVE,ACTION_UP还是需要继续监听并接收的,而且这次的action已经被这个View给消费掉了,父层的ViewGroup将不会走onTouchEvent了。所以每一个action最多只能有一个onTouchEvent接口返回true。如果return false,这个action会传向父级,调用父级View的onTouchEvent。但是这一次的touch事件之后发出的任何action,该View都不会再接受,onTouchEvent在这一次的touch事件中再也不会触发,也就是说一旦View返回false,那么之后的ACTION_MOVE,ACTION_UP等ACTION就不会在传入这个View,但是下一次touch事件的action还是会传进来的。所以在这边把返回值统一置为true,避免子View主动的将事件回传给父层ViewGroup
打开主布局文件activity_main.xml,加入我们刚刚写的自定义布局:
<LinearLayout 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.weaponzhi.viewgroupinterrupteventtest.MainActivity">
<com.weaponzhi.viewgroupinterrupteventtest.MyLayout
android:id="@+id/myLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.weaponzhi.viewgroupinterrupteventtest.MyButton
android:id="@+id/myButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试onTouchEvent" />
com.weaponzhi.viewgroupinterrupteventtest.MyLayout>
LinearLayout>
运行时候,效果图如下所示:
这个时候,我们点击一下按钮,再滑动一下按钮,控制台打印如下:
你会发现,父布局的事件全部都被子View消费掉了,即使父布局在UP和MOVE的时候已经明确的把onInterceptTouchEvent方法返回为true。
细致的分析下出现这种情况的原因,首先,根据源码,事件传递过来后,先进入父布局的disPatchTouchEvent()方法,第一个Action(ACTION_DOWN)的时候,首先调用了onInterceptTouchEvent方法,这个时候,还是返回的是false,所以事件被传递到了子View中,子View接收到后,第一时间将父布局的disallowIntercept 设置为true,所以当本次touch事件之后的action再次进入到父布局disPatchTouchEvent的时候,条件语句if(disallowIntercept || !onInterceptTouchEvent(ev))
始终为true,并且将忽略到后面!onInterceptTouchEvent(ev)
的判断,导致之后的一切action都将被分发到子View中,除非在之后子View的onTouchEvent中将该disallowIntercept重新设置为false。
这里需要注意的是,因为父布局的onInterceptTouchEvent()方法会比该次事件下子View的onTouchEvent()方法要更早进行(如果该事件会分发给子View的话),所以,如果子View设置getParent().requestDisallowInterceptTouchEvent(true)代码的那次事件的onInterceptTouchEvent()返回的是true,那么父布局依然会拦截事件,子View的这次禁用父布局拦截事件请求将会失败。
例如,如果像下面代码那样,在ACTION_DOWN的时候onInterceptTouchEvent返回true,即使子View的onTouchEvent()始终请求禁用拦截,父布局依然会拦截到所有的事件:
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return true;//父布局拦截事件
case MotionEvent.ACTION_MOVE: //表示父类需要
return false;
case MotionEvent.ACTION_UP:
return false;
default:
break;
}
return false; //如果设置拦截,除了down,其他都是父类处理
}
这是这种情况下,子View的onTouchEvent()方法:
public boolean onTouchEvent(MotionEvent event) {
//如果子View被分发了事件,请求父布局不要在之后拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d("TAG", "You down button");
break;
case MotionEvent.ACTION_UP:
Log.d("TAG", "You up button");
break;
case MotionEvent.ACTION_MOVE:
Log.d("TAG", "You move button");
}
return true;
}
运行后,进行之前的操作,控制台如下:
你会发现,事件已经完全被父层拦截了
源码地址:https://github.com/WeaponZhi/ViewGroupInterruptEventTest/