Anroid开发艺术探索(View事件分发)

来源:互联网 发布:网络博客怎么开设平台 编辑:程序博客网 时间:2024/06/06 01:21

一、View的事件分发机制

1、点击事件的传递规则

   在介绍点击事件传递规则之前,首先我们要明白这里要分析的对象就是MotionEvent,即点击事件.所谓的点击事件分发实际上就是对MotionEvent事件的分发过程。当一个MotionEvent产生了之后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent。

public boolean dispatchTouchEvent(MotionEvent e)

   用来进行事件的分发。如果事件能够传递给当前的View,那么这个方法就一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消费当前事件。

public boolean onInterceptTouchEvent(MotionEvent e)

   在上述方法内部调用,用来判断是否拦截某个事件,如果当前VIew拦截了某个事件,那么在同一个事件序列中此方法不会再次被调用,返回结果表示是否拦截当前事件。

public boolean onTouchEvent(MotionEvent e)

   在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消费当前事件,如果不消耗,则在同一次事件序列中,当前View无法再次接收到事件。
上述三个方法到底有什么区别呢?它们的关系可以用如下伪代码表示:

public boolean dispatchTouchEvent(MotionEvent e){    boolean consume  = false;    if(onInterceptTouchEvent(e)){        consume = onTouchEvent(e);    }else{        consume = child.dispatchTouchEvent(e);    }    return consume;}

上面的伪代码已经将三者的关系表示的淋漓尽致。通过上面的伪代码,我们也可以大致的了解点击事件的传递规则:
   对于一个根ViewGroup来说,点击事件产生后,首先会传递给本身。这时候它的dispatchTouchEvent就会被调用。如果这个ViewGroup的onInterceptTouchEvent的方法返回true就表示它要拦截当前事件,接着就将事件交给这个View的onTouchEvent。如果这个ViewGroup的onInterCeptTouchEvent方法返回false就表示它不拦截当前事件,这时候当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。
   当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法就会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent的方法会被调用。如果返回true,那么onTouchEvent方法将不会被调用。由此可见,给View设置的OnTouchListener,其优先级比onTouchEvent要高。在OnTouchEvent方法中,如果当前设置的有OnclickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的OnClickListener,其优先级最低,即处于事件的传递的尾端。

OnTouchListener > onTouchEvent > OnClickListener

   当一个点击事件产生后,它的传递过程遵循如下规律:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接受到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的OnTouchEvent返回false,那么它的父级容器的onTouchEvent将会被调用,以此类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。即Activity的onTouchEvent方法会被调用。我们可以换一种思路来看这个过程。假如点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(事件分发的过程),结果这个程序员搞不定(onTouchEvent返回false),那现在如果要解决这个难题,那么只能交给水平更高的上级去解决(上级的onTouchEvent被调用),如果上级再搞不定,那么继续上抛问题。


关于事件传递机制,这里给出一些结论:

1、同一个事件序列是指从手指接触屏幕的那一刻起,直到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up结束。

2、正常情况下,一个事件序列只能被一个View拦截并且消费。这个原因可以参考3,因为一旦一个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的时间不能分别由两个View同时处理,但是通过某种特殊手段可以做到。比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。

3、某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不再会被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它处理,因此就不再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。

4、某个View一旦开始处理事件,如果它不消费ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中的其他事件都不再会交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给了一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下序列都不再会交给它。

5、如果View不消费除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续接收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

6、ViewGroup默认不拦截任何事件,Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。

7、View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。×××××××

8、View的onTouchEvent默认都会消费事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性都默认为false,clickable属性要分情况,比如button默认为true,textView默认为false。

9、View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。

10、onClick会发生的前提是当前View是可以点击的,并且它收到了down和up事件。

11、事件的传递过程是由外向内的,即事件总是先传递给父元素,然后再有父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。


2、事件分发的源码解析

1、Activity对点击事件的分发过程

   点击事件用MotionEvent来表示,当一个点击事件操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件的派发,具体的工作是由Activity内部的Window来完成的。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得。我们先从Activity的dispatchTouchEvent开始分析。

public boolean dispatchTouchEvent(MotionEvent ev) {    if (ev.getAction() == MotionEvent.ACTION_DOWN){        onUserInteraction();    }    if (getWindow().superDispatchTouchEvent(ev)){        return true;    }    return onTouchEvent(ev);}

   现在分析上面的代码。首先事件开始交给Activity所依附的Window进行分发,如果返回为true,整个事件循环就结束了,返回false表示没有人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。
接下来看Window是如何将事件传递给ViewGroup的,通过源码我们知道,window是个抽象类,而window的superDisptachTouchEvent方法也是个抽象方法,因此我们必须找到window的实现类才对。

public abstract boolean superDispatchTouchEvent(MotionEvent event);

   那么到底window的实现类是什么呢?其实是phoneWindow,这一点WIndow的源码中也可以看出来。在Window的说明中,有这么一段话:


Abstract base class for a top_level window look and behavior policy.An instance of this class should be used as the top-level view added to the window manager.It provides standard UI policies such as a background,title area,default key processing,etc.
The only existing implementation of this abstract class is android.policy PhoneWindow,which you should instantiate when needing a Window.Eventually that class will be refactored and a factory method added for creating Window instances without knowing about a particular implementation.


   上面这段话的大致意思是:window类可以控制顶级View的外观和行为策略,它的唯一实现位于android.policy.PhotoWindow中,当你要实例化这个window类的时候,你并不知道它的细节,因为这个类会被重构,只有一个工厂方法可以使用。尽管这看起来有点模糊,不过我们可以看一下android.policy.PhoneWindow这个类,尽管实例化的时候此类会被重构,仅是重构而已,功能是类似的。
由于Window的唯一实现是PhoneWindow,因此接下来看一下PhoneWimdow是如何处理点击事件的。如下所示:

public boolean superDispatchTouchEvent(MotionEvent event){    return mDecor.superDispatchTouchEvent(event);}

到这里逻辑就很清晰了,PhoneWindow将事件直接传递给了DecorView,这个DecorView是什么呢?

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker//This is the top-level view of window,cntaining the window decor.private DecorView mDecor;@Overridepublic final View getDecorView(){    if (mDecor == null){        installDecor();    }    return mDecor;}

   我们知道,通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)这种方式就可以获取Activity所设置的View,这个mDecor显然就是getWindow().getDecorView()返回的View,而我们通过setContentView设置的View是它的一个子View.目前事件传递到了DecorView这里,由于DecorView继承自FramLayout且是父View,所以最终事件会传递给View。换句话说,事件肯定会传到View,不然应用如何响应点击事件呢?从这里开始,事件已经传递到定义View了,即在Activity中通过setContentView所设置的View。另外顶级View也叫做根View,顶级View一般来说都是ViewGroup。


3、顶级View对点击事件的分发过程


   关于点击事件如何在View中进行分发,上一节已经做了详细的介绍,这里再大致回顾一下。点击事件到达顶级View(一般是ViewGroup)以后,会调用ViewGroup的dispatchTouchEvent方法,然后的逻辑是这样的:如果顶级ViewGroup来接事件即onInterceptTouchEvent返回true,则事件由ViewGroup处理,这时如果ViewGroup的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。也就是说如果都提供的话,onTouch会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了mOnClickListener,则onclick会被调用。如果顶级ViewGroup不拦截事件,则事件会传递给它说在的点击事件链上的子View,这时子View的disptachTouchEvent会被调用。到此为止,事件已经从顶级View传递给了下一层VIew,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。
   首先看ViewGroup对点击事件的分发过程,其主要实现在ViewGroup的dispatchTouchEvent方法中,这个方法比较长,这里分段说明,先看下面一段,很显然它描述的是当前View是否拦截点击事情这个逻辑。

//check for interceptionfinal boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null){    final boolean disallowIntercept = (mGroupFlags & FALG_DISALLOW_INTERCEPT) != 0;    if(!disallowIntercept){        intercepted = onInterceptouchEvent(ev);        ev.setAction(action);//restore action in case it was changed    }else{    intercepted = false;    }}else{    //There are no touch targets and this action is not an initial down     //so this view group continues to intercept touches    intercepted = true; }

待续。。
备注:节选自Android开发艺术探索–任玉刚

阅读全文
0 0
原创粉丝点击