前言
说到安卓的事件分发,大多数人都很难说的很清楚,当然也包括我,之前只是记住了几个结论,什么隧道传递,冒泡处理,什么 dispatchxxx是用来传递事件的,onInterceptxxx是用来拦截事件的,onTouch事件是用来处理事件的,说的门门是道,但是在自己实现逻辑的时候依然会遇到比较懵逼的问题。
现在有做一个需求,是图片的下拉关闭功能,现在很多app都有这个功能,体验性很好,就像下面这张图片
这个图片的实现效果将在接下来的博客中进行讲解,这篇博客只讲在开发中遇到的问题。
我复写了OnInterceptTouchEvent方法但是Move分支没执行
讲道理,这个问题的出现,是有点出乎我的意料的。按照示例图来看,这个是ViewPager,但是同时支持了手势的下滑功能。这样,我们就能很自然的想到去拦截move事件,然后判断在手势下滑的时候,将事件拦截,然后交给自己的OnTouchEvent进行处理。
所以我们可以很自然的写下如下代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = MotionEvent.ACTION_MASK & ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastDownX = ev.getRawX();
mLastDownY = ev.getRawY();
Log.d(TAG,"onInterceptTouchEvent "+"MotionEvent.ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
float downX = ev.getRawX();
float downY = ev.getRawY();
float dx = Math.abs(downX - mLastDownX);
float dy = Math.abs(downY - mLastDownY);
if (Math.sqrt(dx * dx + dy * dy) > mTouchSlop && dy > dx) {
Log.d(TAG,"onInterceptTouchEvent "+"MotionEvent.ACTION_MOVE");
return onTouchEvent(ev);
} else {
return super.onInterceptTouchEvent(ev);
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
break;
}
Log.d(TAG,"onInterceptTouchEvent"+super.onInterceptTouchEvent(ev));
return super.onInterceptTouchEvent(ev);
}
核心代码 Math.sqrt(dx * dx + dy * dy) > mTouchSlop && dy > dx 滑动的距离大于系统的最小滑动距离切 y轴方向滑动的距离大于x轴方向的距离,将其拦截,交给OnTouch事件处理,所以接下来,我们复写onTouchEvent事件
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastDownX = ev.getRawX();
mLastDownY = ev.getRawY();
Log.d(TAG,"onTouchEvent "+"MotionEvent.ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
float downX = ev.getRawX();
float downY = ev.getRawY();
float dx = Math.abs(downX - mLastDownX);
float dy = Math.abs(downY - mLastDownY);
Log.d(TAG,"onTouchEvent "+"MotionEvent.ACTION_MOVE");
if (Math.sqrt(dx * dx + dy * dy) > mTouchSlop && dy > dx) {
// todo 开始图片的缩放动画以及关闭逻辑
handleToCloseView();
return true;//表明事件被消费了
} else {
return super.onTouchEvent(ev);
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
Log.d(TAG,"onTouchEvent "+"ACTION_CANCEL:\n" +
"ACTION_UP:");
break;
}
// Log.d(TAG,"super.onTouchEvent(ev)"+super.onTouchEvent(ev));
return super.onTouchEvent(ev);
}
就这样,我们的伪代码就完成了,感觉天意无缝呢。打印下log发现,
为啥 onInterceptTouchEvent(ev)中的move事件没执行?我可以保证我的手势操作没问题啊,难道是因为我判断有问题,打断点!!!
结果很失望,不是判断逻辑出错,而是压根就没走进该分支。不对啊,不符合常理,在我印象里,onInterceptxxx是用来拦截事件的,onTouch事件是用来处理事件的,如果我们需要自己处理事件,那我们肯定要复写onInterceptxxx来拦截,现在压根都没走进分支,怪哉,但是OnTouchEvent的Move分支执行了,为啥呢?百度之。
有罗里吧嗦讲一堆事件分发的,看的云里雾里,有直接给答案的,说在子布局里直接设置clickable为true的,那就找最简单的,直接设置clickable为true,实验之:
执行了,这下符合逻辑了,但是不科学啊,我不可能在每个子布局里设置这个属性吧,平时用的控件里我也没这么搞过。所以,为啥呢?我们去源码一窥究竟。
入口 dispatchTouchEvent
毕竟事件都是从这里进行分发的,非常幸运的的是ViewPager的源码是很完整的,并没有爆红的情况,我们在里面找该方法,发现并有这个方法,(你在这个源码里打断点你也会发现OnInterceptTouchEvent 的move分支也没执行,当然这个肯定不是代码出错了,至于为什么没执行,和我们遇到的原因一样的。),下面我们开始介绍为什么我们没有接收到Move事件,为什么加了clickable属性,我们就能接到 。所以我们就需要在他的父类ViewGroup里找该方法,源码很晦涩,我只贴主要代码段
可以看到 OnInterceptTouchEvent 的执行条件有三个,其中disallowIntercept属性不用管,默认是false;除非你调用了requestDisallowInterceptTouchEvent(true)方法。接下来讲的内容前提条件都是disallowIntercept为false
另外两个是down事件和 target变量,两者是或的关系,这个就解释了,down事件来了 ,一定会执行OnInterceptTouchEvent ,但是move事件来了呢?第一个条件已经不符合,就看第二个是不是mFirstTouchTarget != null,这个是关键点。那么这个值是在哪里赋值的呢?看源码
记住这个canceled和intercepted变量,第一个肯定是false了,因为我们整个事件链还没有结束,那么intercepted呢?也为false,因为我们并没有拦截down事件,默认返回时false,(intercepted = onInterceptTouchEvent(ev)),所以该分支一定会走到。接下来这个if分支里会遍历子View,然后不断分发事件。主要源代码
其中 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) 方法就是子View的事件分发逻辑,
可以看到调用了child.dispatchxxxxx()方法。
那么这个返回值靠什么决定呢?是子view的OnTouchEvent方法返回值.翻源码进去看看.
可以看到参数clickable只要为true就会走到该分支,至于里面做了什么,我们完全不用管,因为执行了这么多逻辑后,他还是返回了true,(这个clickable的值是哪里决定的呢,就是setclickable方法,也就是布局里的clickable属性。
然后再返回ViewGroup中的dispatchxxxx方法)所以onTouchEvent就返回了true,所以这个分支就进去了,
看标红部分,然后点击源码:
mFirstTouchTarget 被赋值,所以不为null,然后下次Move事件来临时候
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
判断逻辑成立,会再次走入onInterceptTouchEvent(ev)方法,所以这下也能接收到Move事件啦~
总结 (disallowIntercept为false情况下)
- 1.OnInterceptTouchEvent 方法不是每次都执行的,但是down事件一定会执行,;
- 2.想要执行 OnInterceptTouchEvent 的move分支,一定需要子View来消费事件,这样父View才能进行下一次的判断要不要拦截其他事件。否则会直接进行子View的分发工作,然后调用子View的ontouchEvent事件,如果子View不进行处理,那么将交给其上级来处理,绕过OnInterceptTouchEvent 方法。见源码
那么问题来了
我处理事件分发一定要重写OnInterceptTouchEvent 方法来拦截吗
答:不一定,但是99.9%的情况下你需要重写,1是因为你不确定子View会不会拦截事件,2是为了代码的健壮性。
因为事情的分发是隧道传递,冒泡处理的,假设你自定义ViewGroup,该ViewGroup不被其他控件所嵌套,(这里控件指的是可能存在滑动冲突的控件),那么,要看你自定义的Viewgroup里的子控件有没有消费事件,如果消费了,我们需要再适合的时候进行拦截。否则事件会被子View消费掉。
如果我只写OnTouchEvent事件来处理逻辑,但是没有写拦截的逻辑,那么我的逻辑会受影响吗
答:不一定。跟上面说的一样,假设你的子View里没有进行事件的消费,根据冒泡原则,他会让事件向上处理,这时候假设你只写了这个onTouchEvent事件,然后你的自定义ViewGroup处理了事件并返回了true,你的功能并不会受影响,受影响的是你的健壮性。
为什么会总结出这两点来呢,因为有一段臭名昭著的伪代码,让我误解了,虽然他能恰好的表示dispatchxxx oninterceptxxx ontouchxxx 之间的关系,但是让我误解为只有拦截了事件才能执行OntouchEvent,伪代码如下:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume =false;
if (onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}
在我看来,他应该这么写:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume;
if (evevt.getAction==MotionEvent.ACTION_DOWN||mFirstTarget!=null ){
if (!requstdisallow){
consume =onInterceptTouchEvent(event);
}
}
if (consume){
//不再将事件分发给子View,执行自己的onTouchEvent事件
}else {
//如果这时候child.dispatchTouchEvent(event)为true,那么mFirstTarget将被赋值不为null;
//下次move事件来的时候回走进onInterceptTouchEvent(event)
return child.dispatchTouchEvent(event) ;
}
//执行自己的OnTouch事件(指的是冒泡处理,在不拦截的时候,事件会优先分发给子View,
//子View若不处理,会交给父View的onTouchEvent处理,如果拦截了,就会走自己的onTouchEvent(event))
return onTouchEvent(event);
//......冒泡......//
//......冒泡......//
//......冒泡......//
}
你不拦截事件,自己的ontouchevent事件也会被执行,因为只要你的子View不消耗事件,事件就会被冒泡处理。
你拦截了事件,事件将不分发给子View,同时,自己的OnTouchEvent事件会被执行。
这是我自己的理解,欢迎勘误~