android事件分发机制源码简析

题记

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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值