正式开始之前的准备工作:
之前也写过一篇《android事件拦截处理机制详解》的博客,通过博客的名字也可以知道那篇博客只是分析了android控件View对事件的拦截和处理,简单的总结如下:
1)父View优先拦截当前事件,拦截不成功就让子View对当前事件进行拦截。
2)如果拦截成功的话,就会沿着子view到父View的路径查找onTouchEvent返回true的那个子View,让该子View对该事件进行处理;
3)同时如果某一个View对当前事件拦截成功的话,当前事件就不会继续分发给这个View的子View。
一直在说事件,那么事件到底是指什么?这里所说的事件是指手指按下(down)、移动(move)、抬起(up)此为一个事件集合或者说是事件序列,从手指接触屏幕开始到手指离开屏幕结束。所以本篇所说的事件序列或者事件集合是指从手指刚接触屏幕到离开屏幕的那一瞬间产生的各个事件:
事件序列为:ACTION_DOWN-->ACTION_MOVE-->ACTION-->...->ACTION_UP事件。
上面的总结很简单,详细的分析以及说明都在《android事件拦截处理机制详解》这篇文章里,有兴趣的可以看一下。
其实那篇博客说的有点简单了,只是涉及到View的层面而没有涉及到Activity和Window层面。本篇就在此基础上加上对事件源码的分析来进行补充说明。
通过《Activity+Window+View简单说明》和《Context简单说明》可以知道三者之间的关系以及关系的由来:
准备工作已经完成,闲言少叙书归正传吧。
和拦截处理机制详解一样,为了系统的研究android对事件的处理,我也写了一个小demo对不同的情况进行测试并结合源码分析(多说一句,其实看源码确实很枯燥,有时候因为水平有限有的部分看不懂而查阅大量资料,笨人有笨法:结合demo测试验证和理解,虽然效率低但是效果不错),可以得出如下的结论(至于结论的由来,下面会说明):
1)android对事件分发的顺序为:Activity-->PhoneWindow->DecorView->yourView;
2)android控件对事件处理的优先级:onTouch>onTouchEvent>onClick
(道理很简单,只有先触摸Touch才可以产生事件(TouchEvent) ,而后才可以是Click事件
android既然可以对事件进行拦截,肯定有某个方法对事件进行的传递或者分发(以前我总是说事件传递,但是看了各种资料都说是事件分发,在此统一一下就用“分发”这个名词吧)。完成事件分发功能的方法由Activity的dispatchTouchEvent(MotionEvent ev)l来负责:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//该方法为空方法,直接忽略之
onUserInteraction();
}
//把事件ev交给phoneWindow来处理
if (getWindow().superDispatchTouchEvent(ev)) {
//表明整个事件到此结束,处理完毕
return true;
}
//说明该事件在View中没有得到处理,由Activity自己处理
//至于怎么处理博客后面后有说明
return onTouchEvent(ev);
}
上述分发事件的方法dispatchTouchEvent,先把事件分发给Window,通过之前写的博客知道这个Window其实就是PhoneWindow,那就看看PhoneWindow方法都做了些什么:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
很简单,就是把此次事件直接很光棍的传给DecorView,这个View是所有视图的根视图,Activity界面中你能见到的各个View都是DecorView的子View。到此为止事件已经分发到View上面,View获取到事件后有两个选择:处理和不处理该事件,如果处理该事件那事件就不会继续向其子View分发下去;否则就继续分发下去交给子View对该事件做同样的判断,其实就是个递归的过程。在这里就可以先用如下的伪代码也表示一下事件处理流程,然后参照这个伪代码来看本篇博客下面的内容会更容易理解点:
public boolean dispatchTouchEvent(event){
//如果当前View对此事件拦截成功
if(this.onInterceptTouchEvent(event)){
//由当前View对此事件进行处理
//true 表示消耗了该事件,false表示没有xiaohao该事件
return onTouchEvent(event);
}else{//没有拦截成功
//交给子类来分发拦截处理
return child.dispatchTouchEvent(event);
}
}
其实通过上面的代码也可以得出这个结论:当Activity中所有的视图View都不处理该事件的是就交给Activity的onTouchEvent方方来处理。该DecorView对此次事件又进行分发,让子View对该事件进行拦截和处理,具体可阅读《android事件拦截处理机制详解》,DecorView的分发事件代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Callback cb = getCallback();
return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
.dispatchTouchEvent(ev);
}
上面的代码通过super.dispatchTouchEvent(ev)调用了DecorView的父类FrameLayout,该类倒是没有重写dispatchTouchEvent而是由它的父类ViewGroup实现:在分析ViewGroup分发事件之前还得说两结论:
1)ViewGroup永远不会对拦截,因为他的onInterceptTouchEvent(MotionEvent ev)始终返回的是false!这样DecorView对到来的事件MotionEvent就只有分发到子View并由子View进行拦截和处理此事件了.
2)View包括直接继承于View的子类因为其父类View没有onInterceptTouchEvent方法,所以没法对事件进行拦截,如果这种View获取到了事件,那么就会执行onTouchEvent方法(当然这也是有条件的,这个前提条件在对下面onTouch方法作用的时候会有说明)。
比如Button的直接父类TextVew的父类为View,那么当Button获取事件的时候其执行分发和处理的时候调用dispatchTouchEvent,就先分析一下View的这个方法都做了什么以供博客后面的篇幅用:
public boolean dispatchTouchEvent(MotionEvent event) {
if (!onFilterTouchEventForSecurity(event)) {
return false;
}
//从这里看出,onTouch方法是优先于onTouchEvnent方法的
//如果onTouch方法返回了true的话,那么onTouchEvent方法就没法执行
//相应的onClick方法也不会去执行
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
//如果这里返回true的话,那么也表明此次事件被处理了
return true;
}
//回调onTouchEvent方法
return onTouchEvent(event);
}
通过上面的方法我们可以知道几个信息,按照方法的执行顺序,可以知道onTouchListener的onTouch方法优先于onTouchEvent执行,这个onTouch是否能实行取决于你的View有有没有调用setOnTouchListener方法设置OnTouchListener。如果onTouch方法返回的true,那么View的dispatchTouchEvent方法就返回true而结束执行,onTouchEvent方法就不会得到执行;因为onClick方法通过下面的分析也知道是在onTouchEvent方法中执行的,所以此时onClick方法也不会执行了。所以当我们为Button做如下处理的时候,Button的onClick事件就不会执行了:
//因为该方法返回true,所以dispatchTouchEvent方法会结束执行
//进而导致Button的onClick方法也没机会执行
btn.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
doClick();
}
});
以上简单的说下onTouch和onTouchEvent和onClick的执行顺序以及onTouch的返回值对onTouchEvent和onClick的影响,如果不对Button设置onTouchListener的话程序会执行View的onTouchEvent方法:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP://抬起事件才会执行onClick
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
..........
if (!mHasPerformedLongPress) {
.........
if (!focusTaken) {
.........
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//这也就说明了onTouchEvent优先于onClick执行,因为onClick方法就在onTouchEvent方法里执行
//验证了上面的结论
if (!post(mPerformClick)) {
performClick();
}
}
}
...........
}
break;
}
//此时返回了true,此View处理了该事件,事件不会再往下进行分发
return true;
}
到此为止关于View对事件的分发和处理算是简单的分析完毕,也可以得到一个结论:如果View类的的dispatchTouchEvent返回true的话,就表明有某个View对该起事件负责(进行处理),记住这一点对下面的分析有帮助。
通过View类的onTouchEvent方法我们也很容易得到如下两个结论,该结论主要有上方代码的如下语句得来的:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE))
这句代码的意思是说如果这个view是可点击的(clickable=true或者longClickable=true)那么这样的View最终都会消耗当前事件而让View的dispatchTouchEvent返回true,这样的View就是下面将要说道的消耗事件的target!!!否则,即如果一个View既不是clickable也不是longClickable的话,那么这个View不会消耗该事件。
下面继续分析在ViewGroup的dispatchTouchEvent方法,该方法中开始有这么一段:
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
//如果不允许当前View拦截该事件或者没有拦截该事件
//就让当前View的子类去分发和拦截,递归过程
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
//遍历ViewGroup的子View,在此为遍历DecorView的子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
.........
//交给子View的dispatchTouchEvent进行对此事件的分发拦截。
//如果返回true说明child处理了此事件,递归调用
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
//说明已经找到处理此次事件的子View,用mMotionTarget记录
mMotionTarget = child;
//找到目标,此次down事件结束。
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
上诉代码的for循环很详细的说明了View对事件进行分发的过程,当然for循环之所以能得以执行使用两个前提条件的:
1)事件为ACTION_DOWN事件,在Down事件中才去进行分拦截发事件并寻找消耗事件的target View,!
2)disallowIntercept ,这个属性可通过requestDisallowInterceptTouchEvent(boolean disallowIntercept )来设置,通过观察期具体实现可以知道该方法的作用就是子View干预父View的事件分发过程,当然对于ACTION_DOWN事件子类是不能干预父类的,因为if条件为(disallowIntercept ||!onInterceptTouchEvent(ev))为或操作;或者当前的View没有拦截成功该事件。如果一个ViewGroup的disallowIntercept 为true,说明其子View要求后续的一系列事件其父View不得进行拦截,直接交给子类进行拦截。
:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
//省略部分代码
// Pass it up to our parent
if (mParent != null) {
//当前View干预其父View对后续事件的分发
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
如果当前的ViewGroup(为了方便再次称之为ParentView)允许对此次事件进行拦截或者ParentView没有对此事件拦截成功(ParentView的onInterceptTouchEvent返回false)简而言之就是如果ParentView不拦截处理该事件,就把该事件分发到ParentView的若干子类中去,循环遍历它的子类,来寻找是否有某个子类对处理该事件。可以参照上面的伪代码和流程图来理解之。
如果找到了这样的View,就对该View用一个变量mMotionTarget进行标识。如果在当前的ParentView的子View中没有找到处理该事件的子View会怎么办呢?在ViewGroup里面的dispatchTouchEvent在上面的for循环之后有如下代码:
//如果没有找到处理事件的View
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
//ViewGroup调用View的事件分发方法,之所以能进入当前的View是因为当前的View的父View没有拦截成功此事件。
return super.dispatchTouchEvent(ev);
}
从上面的代码可以看出:如果在ParentView的子类中没有找到能处理问题的那个view,就调用parentView的父View的dispatchTouchEvent方法。我们应该知道之所以parentView能分发和拦截事件,前提是它的父类本来没有拦截事件的能力或如本身拦截事件的方法返回了false,所以沿着view树最终会调用的View类的dispatchTouchEvent,那么又回归到本篇博客的A)View类对事件的处理那一部分了。
注意前面讲的for循环查找的重大前提是:在down事件中,且我们要明白在手指接触屏幕到手指离开屏幕会产生一系列事件,一个down(ACTION_DOWN)事件,数个move(ACTION_MOVE)事件,和一个 UP事件。寻找到目标事件之后,之后的一些列事件都交给这个target消耗,比如move事件等。当然我们是可以通过让target调用requestDisallowInterceptTouchEvent方法来干预父类关于事件分发过程。或者在在适当的情况下让target父View的onInterceptEvent返回true或者false,来解决滑动问题事件的冲突问题:
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
最后某个View(target)如果开始处理事件,在手指离开屏幕之前的什么move事件啦,up事件啦都会交给这个View(target)来处理,因为在ViewGroup的diapatchTouchEvent代码的最后会执行:
//把事件交给目标视图来处理
return target.dispatchTouchEvent(ev);
并且本View的onInterceptTouchEvent是不会调用了。也就是说如果有一个view处理该事件,那么down之后的一系列move事件和up事件都自动交给该view处理,因为该view已经是targetView 了,所以不会对后续事件序列或者事件集合进行拦截操作,只会调用dispatchTouchEvent和onTunchEvent来处理该事件!而onInterceptTouchEvent事件不会调用。简单的举个例子,如图:
假设上图中由D进行事件的处理,也就是说D的onInterceptTouchEvent和onTouchEvent均返回true,D的父View A ,B ,C的这两个方法都返回false,那么在这个布局中D就是上面所说的 target.在首次的Down事件中会执行查找target的操作:(注:下图中分发事件指的是执行了dispatchTouchEvent,拦截事件指的是onInterceptTouchEvent,消耗事件为onTouchEvent方法)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(null, "B--分发事件");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(null, "B--拦截事件");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(null, "B--处理事件");
return super.onTouchEvent(event);
}
注意D此时的打印,D为此事件的target View,拦截和处理了该次down事件,那么如果此时手指继续移动的话将会打印如下的Log:
注意上图是手指滑动后打印的log,可以发现D只负责分发和处理该事件,而没有像down事件那样进行拦截,所以上面的log打印可以清晰的说明上面的结论:
1)targetView只会对一个事件序列拦截一次,即只拦截down事件或者说由down事件负责查找targetView
2)一旦targetView被找到,down事件之后的一些列事件都由target View负责消耗,而target并不对后续事件进行再次拦截
3)当然在down事件之后的后续事件还是会先由父View进行分发拦截,也即是说文章开头所说的事件序列中把每一个事件单独来看的话,都会由父View来进行拦截和分发的,只不过到后续事件到传到target的时候直接进行处理而少了拦截的过程而已,因为在父类查找target的时候已经拦截过一次,这点很重要,也是解决滑动冲突的关键点,比如滑动的时候根据合适的时机来判断是否让父View进行事件拦截和处理。只不过省下了对targetView的寻找,因为在down事件中已经寻找到了target并有mMotionTarget变量进行了标识,(通过上面的对ViewGroup的dispatchTouchEvent源码的解析就可以表明出来)
写到此处,android事件的处理已经很明确了,本篇博客到此就结束了,感觉有点啰嗦,如果不正确的地方欢迎批评指正,共同学习。