android事件分发机制源码简析

来源:互联网 发布:linux ntp同步 编辑:程序博客网 时间:2024/06/06 06:27

题记

android事件分发这块已有众多大神的各种解析,我就取个“简析”吧。本来就不打算写得多详细,权当是自己的一点总结。

前言

这块内容网上和书上都看过蛮多,一直都是似懂非懂。幸而找到郭神的两篇大作。立论清晰、分析入理、总结到位。附上地址:http://blog.csdn.net/sinyu890807/article/details/9097463如果仅仅是拾人牙慧,甚而把他人写的拿来重新组织下也无甚意思。本文是我自己阅读源码的一些见解。

正文

先前看过《android群英传》对事件机制的概括,用的是部门模型。大致意思是:Viewgroup到其子view的事件传递,类似于一个公司中:总经理-》部长-》员工之间的任务下达和工作报告。看过后觉得作者说得很对,但就是有好多疑问没法用这套模型来解释。然后,通过自己阅读源码,我觉得用“甩锅”模型来描述android的事件分发机制或者更为贴切。(这里权当是我的大言不惭)下面就多个控件层叠的情况讨论下事件分发,即:一个Viewgroup中还有子View的情况。PS:所有源码基于android_4.4

流程

事件流程图
这是我整理的事件分发流程图,下面的叙述围绕它展开。

Viewgroup.dispatchTouchEvent

从Viewgroup的dispatchTouchEvent走起,当你touch最外层的Viewgroup时,首先会调用这个,我要叙述的也仅局限于此,不去关心事件如何被采集之类。下面分段叙述ViewGroup.dispatchTouchEvent中的源码。
    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;                }            } else {                // There are no touch targets and this action is not an initial down                // so this view group continues to intercept touches.                intercepted = true;            }

如果disallowIntercept没被enable的话,onInterceptTouchEvent将会被首先调用以完成对事件的拦截。intercepted 的值取决于返回值,并作为后续是否分发事件的判断标志,如下代码所示。

if (!canceled && !intercepted) {    ......    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {        addTouchTarget(...)//包含对mFirstTouchTarget的赋值。    }    ......}......if (mFirstTouchTarget == null) {    // No touch targets so treat this as an ordinary view.    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);} else {    ......}/**     * Transforms a motion event into the coordinate space of a particular child view,     * filters out irrelevant pointer ids, and overrides its action if necessary.     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.     */private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {    ......    if (child == null) {        handled = super.dispatchTouchEvent(event);    } else {        handled = child.dispatchTouchEvent(event);    }    ......}

android_4.4的代码和人家博客里几年前的已经大不相同了。
大致流程:
1)通过检测intercepted来判断事件是否被拦截,如未被拦截则找到相应的child来处理本次事件。至于按什么规则来寻找child,我觉得应该是遍历所有child以定位当前点击位置属于那个child(这部分未分析源码验证)。
2)在继续之前先需要明白dispatchTransformedTouchEvent这个函数的作用。其作用就是根据child参数是否为null,来判别是调用child.dispatchTouchEvent还是Viewgroup自身的dispatchTouchEvent。
3)承接1),如果child.dispatchTouchEvent返回false或者未找到相应child,则mFirstTouchTarget ==null,则后续调用dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS)。可以看到child参数为null,其实就是调用了Viewgroup自身的dispatchTouchEvent来处理本次事件。
4)如果child.dispatchTouchEvent返回true,则调用addTouchTarget完成对mFirstTouchTarget 的赋值,Viewgroup的dispatchTouchEvent也就不会再被调用。
ps:如果child本身也是Viewgroup则递归此过程。从整体流程可以看出:只要有一个child处理了本次事件,则位于上层的Viewgroup就不再关心本次事件的处理了。就好比:一个事件就是一个锅,一层层甩下去,爱谁接谁接,实在没人接么只好自己来。

然后,我们可以查看下TextView的源码(Button等继承自它),并未重写View的dispatchTouchEvent。也就是说最后事件的处理由View.dispatchTouchEvent完成。

View.dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {    ......    if (li != null && li.mOnTouchListener != null            && (mViewFlags & ENABLED_MASK) == ENABLED            && li.mOnTouchListener.onTouch(this, event)) {        result = true;    }    if (!result && onTouchEvent(event)) {        result = true;    }    ......}

1)优先mOnTouchListener ,如果你设置了mOnTouchListener 并且返回true的话,onTouchEvent就不会被执行。提前剧透下:onTouchEvent是负责具体检测事件的(如:click等),也就是说你注册的OnClickListener也就无效了。
2)无论是mOnTouchListener或者onTouchEvent返回了true,那dispatchTouchEvent也就返回true,那本次事件就被消费掉了,Viewgroup就不处理了。
3)经测试:以一次滑动为例–按下、滑动、抬起。dispatchTouchEvent开始的返回值会影响后续事件的接收。也就是说:mOnTouchListener.onTouch和onTouchEvent都返回false,那表示该View不想处理本次事件,则后续的MOVE和UP不会再被通知。(此部分非阅读源码分析得到,是由测试得出,后续会附上测试例程)

onTouchEvent

onTouchEvent具体负责事件的处理,就是人家告诉你:按下了、按着呢、抬起来了。然后据此判断是否是:click事件、longClick事件等等。怎么判断的不作讨论。

public boolean onTouchEvent(MotionEvent event) {    switch (action) {        case MotionEvent.ACTION_UP:            ......            if (mPerformClick == null) {                mPerformClick = new PerformClick();            }            if (!post(mPerformClick)) {                performClick();            }            ......            break;        case MotionEvent.ACTION_DOWN:            ......            checkForLongClick(0, x, y);            ......            break;    }           return true;}private final class PerformClick implements Runnable {        @Override        public void run() {            performClick();        }    }private final class CheckForLongPress implements Runnable {        private int mOriginalWindowAttachCount;        private float mX;        private float mY;        @Override        public void run() {            if (isPressed() && (mParent != null)                    && mOriginalWindowAttachCount == mWindowAttachCount) {                if (performLongClick(mX, mY)) {                    mHasPerformedLongPress = true;                }            }        }        public void setAnchor(float x, float y) {            mX = x;            mY = y;        }        public void rememberWindowAttachCount() {            mOriginalWindowAttachCount = mWindowAttachCount;        }    }

此处的代码就很好理解了。当你点击的时候,最后那一下UP触发click事件。按的时间持续足够了触发longClick事件。
尤其注意到的是,android对事件处理的思路就是:应该在线程中进行,以便不影响UI主线程的体验。但click事件会根据post的返回值来确定是否直接performClick,而longClick则必定是通过post来传递。进而引出另外一个问题:post出去的Runnable是在何时被执行。因为mOnClickListener中是可以更新UI的,那此处的Runnable肯定不同于一般的线程,我觉得应该类似于handler的消息机制,这个有待后续深究。
PS:此处还有一点当你同时注册了longClick和click需要注意:longClick是有返回值的,且其返回值影响mHasPerformedLongPress的值,mHasPerformedLongPress决定了click是否被调用,此处应该是为了防止longClick和click在某个临界上被同时触发。所以longClick中最好还是返回true,以避免此类情况。

总结

1,继承自Viewgroup的容器类提供onInterceptTouchEvent方法来决定是否对事件拦截。如果拦截则会调用Viewgroup自身的事件处理。
2,如果不拦截则会找到能处理该事件的child,如果找不到或者child不处理还是得Viewgroup来处理。
3,child对起始事件的拒收影响后续的接收。
4,对于onTouchEvent,一旦你处理该次事件它就返回true。所谓的处理就是:只要View是Clickable的就行。也就是说:不管是否注册了click或者longClick事件,只要没有关掉view的Clickable,那本次事件就算你处理了,Viewgroup就收不到事件了。
5,遗留:mOnClickListener和mOnLongClickListener涉及到的post Runnable问题。
6,在下一篇会写一个例程验证本文提到的所有点。

最后的归纳:开头提到的《android群英传》中的部门模型是把onTouchEvent当成向上层的Viewgroup提交工作报告的途径。可以通过override来决定通知上层本次工作是否已被完成。本意没错,但总觉得有点痒的不挠挠痛的地方。因为,这没有解释:常用的注册OnClickListener后,listener怎么被调用,Viewgroup和child同时注册的话,谁的listener该被调用。我觉得android遵循的就是:为事件找到处理者(把锅甩出去)。
0 0