文章内容参考任玉刚老师的安卓开发艺术探索,并不是照搬,是我在理解基础上的重新总结。
首先要明白View的事件分发机制,这里的事件具体指的就是MotionEvent,我们可以通过getAction判断这个事件是按下抬起还是move事件。
还有一个概念:事件序列。一个事件序列指的是从按下到抬起的一系列事件的总和。
下面看与view的事件分发相关的三个方法:
boolean dispatchTouchEvent(MotionEvent ev): 负责事件的分发,返回值表示是否消耗本事件
boolean onInterceptTouchEvent(MotionEvent ev): 表示是否拦截本事件。需要注意的是,每一次进行事件拦截判断的时候都要调用这个方法,但是一旦决定拦截当前事件,当 前事件序列后面的所有事件都会被本view拦截下来,不会再向下分发,且这个方法不会再被调用。(这一点很重要)
boolean onTouchEvent(MotionEvent ev): 对点击事件的处理方法,返回值表示是否消耗事件,false的话则无法再次收到当前事件序列的事件。需要注意的是,如果view添加了 onTouchListener,则执行listener中的onTouch方法,如果onTouch返回false才会继续执行onTouchEvent。我们平时设置的onClickLisenter中的方法就在onTouchEvent方法 中执行。
用一段伪代码表示三者的关系:
public boolean dispatchTouchEvent(MotionEvent mv){
boolean consume=false;
if(onInterceptTouchEvent(ev)){
consume=onTouchEvent(ev);
} else{
consume=child.dispatchTouchEvent(ev);
}
return consume;
}
首先可以看到分发方法默认返回false,也就是不消耗本事件,继续往下分发。此外,viewgroup拦截方法的默认值也是false,也就是不拦截。(注意viewgroup和view的区别)
首先判断是否拦截事件,如果拦截就进行处理否则分发给子组件。
然后直接看滑动冲突问题,比如常见的水平滑动的viewpager作为根布局,其中几页又有竖直滑动的listview。但是viewpager已经默认解决了这个问题,所以使用它的时候不用考虑这个。还有水平滑动的里面含有一个水平滑动的等问题。其实不管冲突类型多么复杂,其本质都是事件分发,只要掌握了事件分发的过程就可以轻松解决。
解决这个问题又有两种思路,一种是外部拦截法,即在父组件中判断是否拦截事件,不拦截再交给子组件。通俗的说,就是看父组件是否需要这个事件序列,如果要的话直接扣下,不要的话再给子组件。另一种是内部拦截法是直接把事件交给子组件,子组件进行判断,如果子组件需要就让父组件不在拦截,如果父组件需要就让父组件一直拦截。相比较来说外部拦截法更加简单。
1.先来看外部拦截法:重写父布局的onInterceptTouchEvent方法
public boolean onInterceptTouchEvent(MotionEvent mv){
boolean intercept=false;
switch(mv.getAction()){
case MotionEvent.ACTION_DOWN:
intercept=false;
break;
case MotionEvent.ACTION_MOVE:
if (父组件需要这个事件){
intercept=true;
} else {
intercept=false;
}
break;
case MotionEvent.ACTION_UP:
intercept=false;
break;
default:
break;
}
return intercept;
}
按下操作不能判断这个事件序列的形态,所以不能拦截,拦截的话接下来的事件直接全交给父布局了。抬起操作也没有什么意义,所以也不拦截。
主要是move事件的判断,一旦判断是父布局需要的就全部拦截留下,否则交给子组件。
这种思想也比较符合事件分发机制的过程。
2.再来看看较为复杂的内部拦截法:
首先重写子组件的事件分发方法:
public boolean dispatchTouchEvent(MotionEvent mv){
switch (mv.getAction()){
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父布局需要){
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(mv);
}
然后重写父布局的拦截方法:
public boolean onInterceptTouchEvent(MotionEvent mv){
int action = mv.getAction();
if (action==MotionEvent.ACTION_DOWN){
return false;
} else{
return true;
}
}
首先依然是父布局把按下事件交给子组件,子组件通过request...方法设置一个标记位(详见安卓开发艺术探索),使得父布局拦截失效(不过这个标记位对按下事件无效),这样接下来的事件全部开始直接交给子组件。子组件在移动事件中进行判断,如果是父布局需要的就再通过上面的方法恢复父布局的拦截,而父布局的拦截已经把几个事件都设置为true,也就是全部拦截,这样后面的事件序列就都交给父布局了。总结一下的话就是父布局优先了,因为父布局拦截方法中只要决定拦截的话剩下的事件也就全都留下了。所以两种方法中判断的都是父布局是否需要。
ps:至于判断事件应该归谁就要看具体情况了,可以从事件中抽取坐标计算位移量速度等值做出判断。