android中touch事件分发机制

最近找工作,总被问到事件的分发机制,这个事件理解起来是有点麻烦,虽然也工作很长时间了,但有时被也会被面试官给绕进去,所以就打算总结下事件的分发机制,其实当我们看过源码后,就很容易理解了。


Touch触摸是触摸手机最基本的操作,系统正是通过分析Touch事件来响应用户的操作。一次完整的触摸包括Down,Move,up。其中Down和up只会出现一次,Move会执行多次。


Touch事件是从上往下一级一级的传递。该事件由系统服务WMS获取后分发给指定的活动窗口,由活动的根View开始分发Touch事件。整个过程是一个递归的方式。
start:
首先调用ViewGroup的dispatchTouchEvent方法,如果是down事件,则清空上次处理该事件的对象mMotionTarget(为了处理MOVE之类的事件,做的缓存)。

调用onInterceptTouchEvent方法,这个方法只有ViewGroup类有,具体view没有,该方法的作用是判断是否需要拦截该消息,如果返回的是true,那么消息传递结束,调用该ViewGroup对象的onTouchEvent方法。如果返回的是false,说明该ViewGroup没有消费事件,事件继续往下走。

因为触摸事件是窗口坐标值,所以需要将坐标值转换为view自己的坐标体系。转换结束后,使用for循环遍历,该ViewGroup的所有子view,读取子view的坐标体系,即子view所占的大小,是个Rect对象,上下左右,拿到这个值后,根据上面转换好的坐标,判断点击的坐标是否包含在当前子view中,如果不包含,直接开始下一个子view。

如果坐标包含在子view中,则调用子view的dispatchTouchEvent,如果子view还是ViewGroup类型的,那么开始从上面标有 start 处递归调用。如果是具体view,则首先判断是否通过setTouchEventListener设置值,如果设置了,那么调用监听者的onTouch方法,如果该方法返回的是true,则直接返回true,不在调用该view的OnTouchEvent方法,如果返回false,则调用该view的OnTouchEvent方法。并把该方法当作dispatchTouchEvent的返回者返回。

具体view的dispatchTouchEvent处理结束后,子view的dispatchTouchEvent如果返回的是true,该view的父view会将该view对象保存到mMotionTarget,同时结束到本次down事件,如果放回的是false,则继续for循环(个人认为此时可以退出for循环,因为感觉没用,难道是担心有view覆盖的原因吗),开始下一个子view。
end:
for结束后(由于该过程是同步的,所以在执行这个过程中不会有其他的事件发送过来),判断mMotionTarget是否为空,如果为空,说明没有找到目标子view,所以调用当前view(一定是ViewGroup对象的,而且是循环体所在的view对象)的super.dispatchTouchEvent方法,这个是View基类的,实现和具体view的处理逻辑一样,首先判断是否通过setTouchEventListener设置值,如果设置了,那么调用onTouch方法,如果该方法返回的是true,则直接返回,不在调用该view的OntouchEvent方法,如果返回false,则调用该view的OntouchEvent方法。并把该方法当作 dispatchTouchEvent的返回者返回 ,交给该ViewGroup的dispatchTouchEvent 的for循环(递归调用结束一个),这就说明,如果所有的子view不消费事件,那么view会消费该事件,不管onInterceptTouchEvent的返回结果是true还是false。同时结束本次事件。 至此,一个Down事件就处理结束,处理结束后会通知WMS,此时WMS开始派发下一个事件。


如果过来的事件是move或者up事件,首先判断down处理逻辑得到mMotionTarget是否为空,也就是说down处理中,是否找到的接收事件的子view。

如果为空:说明没有找到目标子view,所以调用当前view(一定是ViewGroup对象的,而且是循环体所在的view对象)的super.dispatchTouchEvent方法,这个是View基类的,实现和具体view的处理逻辑一样, 首先判断是否通过setTouchEventListener设置值,如果设置了,那么调用onTouch方法,如果该方法返回的是true,则直接返回,不在调用该view的OntouchEvent方法,如果返回false,则调用该view的OntouchEvent方法。并把该方法当作 dispatchTouchEvent的返回者返回 ,交给该ViewGroup的 dispatchTouchEvent 的for循环(递归调用结束一个),这就说明,如果所有的子view不消费事件,那么view会消费该事件,不管onInterceptTouchEvent的返回结果是true还是false。同时结束本次事件。

如果不为空:这个时候mMotionTarget的直接父类只走dispatchTouchEvent事件,但是mMotionTarget的爷爷及老爷还会走dispatchTouchEvent 和onInterceptTouchEvent事件,暂时没弄明白,有明白的解释一下,谢谢。那么本次的move和up事件继续由该子view处理,这样的逻辑我们可以想到,因为同一个事件,应该有同一个view处理,而不是down事件是一个view处理,move和up事件是一个view处理。所以不为空的情况下,直接由mMotionTarget的dispatchTouchEvent处理去。

同时我们知道,在activity中还可以注册ontouchEventListener,那么他什么时间执行了?他执行的时间就是在view中没有找到消费该事件的view时,则交给acitivity去处理


简单的总结下其实就是:
如果父view的onInterceptTouchEvent返回的是true,那么子view永远拿不到touch事件,同时子view的onclick事件也不会处理,因为onclick事件是在view的onTouch事件中根据条件调用的,同时如果重写view的onTouchEvent方法,而没有调用super.OntouchEvent.那么onclick事件也不会处理。

如果给一个view设置了onTouchEventListener,同时设置了OnclickListener,而在onTouchListener的onTouch方法返回的是true,这个时候onClick事件不走,因为这个时候不调用onTouchEvent方法,而系统调用onclick事件在onTouchEvent中捕获到up事件时,根据条件判断执行的。


下面我就来说说touch事件
Touch事件分为三种,down、move、up。在一个完整的Touch事件中down和up只有一次,move可以有多次。
消息的分发是从父View到子View,直到某个View消费该事件为止,当所有的子View都不消费该事件,事件将回传。
Activity也可以处理事件,但是只有在所有View都不消费该事件后才有资格。

dispatchTouchEvent
是整个Touch分发策略,所有Touch事件都在该方法体中完成。
它会先通过onInterceptTouchEvent来判断是否需要拦截事件,如果不拦截,将会调用子View的dispatchTouchEvent,如果拦截,则调用自身的onTouchEvent事件。

onInterceptTouchEvent
用于判定事件是否需要往下传递,需要注意的是,当一个down事件在此被拦截后,或者子View都没消费掉,仍旧由自身消费,则后续所有的事件都会直接由自身处理,不需要再经过onInterceptTouchEvent的判断

Touch事件的流程控制是由dispatchTouchEvent来控制的,逻辑如下:
View的dispatchTouchEvent:

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        //判断mOnTouchListener是否存在,并且控件可点的情况下,执行onTouch,如果onTouch返回true,就消耗该事件
        return true;
    }
    //如果以上条件都不成立,则把事件交给onTouchEvent来处理
    return onTouchEvent(event);
}

1.判断以下条件是否成立
是否注册了OnTouchListener
控件是否为enable,即可响应触摸事件
2.如果以上两个条件成立
调用OnTouchListener的onTouch方法
3.如果1的两个条件不成立,或者2的onTouch返回false,则调用onTouchEvent
总结:view中的Touch事件是由OnTouchListener来优先处理,其次才有onTouchEvent来处理


ViewGroup的dispatchTouchEvent:

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();//获取事件
    final float xf = ev.getX();//获取触摸坐标
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;//获取当前需要偏移的偏移量量
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;    //当前ViewGroup的视图矩阵

    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//是否禁止拦截

    if (action == MotionEvent.ACTION_DOWN) {//如果事件是按下事件
        if (mMotionTarget != null) {    //判断接受事件的target是否为空
            //不为空肯定是不正常的,因为一个事件是由DOWN开始的,而DOWN还没有被消费,所以目标也不是不可能被确定,
            //造成这个的原因可能是在上一次up事件或者cancel事件的时候,没有把目标赋值为空
            mMotionTarget = null;    //在此处挽救
        }
        //不允许拦截,或者onInterceptTouchEvent返回false,也就是不拦截。注意,这个判断都是在DOWN事件中判断
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            //从新设置一下事件为DOWN事件,其实没有必要,这只是一种保护错误,防止被篡改了
            ev.setAction(MotionEvent.ACTION_DOWN);
            //开始寻找能响应该事件的子View
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {//如果child可见,或者有动画,获取该child的矩阵
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        // 设置系统坐标
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        if (child.dispatchTouchEvent(ev))  {//调用child的dispatchTouchEvent
                            //如果消费了,目标就确定了,以便接下来的事件都传递给child
                            mMotionTarget = child;
                            return true;    //事件消费了,返回true
                        }
                    }
                }
            }
            //能到这里来,证明所有的子View都没消费掉Down事件,那么留给下面的逻辑进行处理
        }
    }
    //判断是不是up或者cancel事件
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
             if (isUpOrCancel) {
        //如果是取消,把禁止拦截这个标志位给取消
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }


    final View target = mMotionTarget;
    if (target == null) {//判断该值是否为空,如果为空,则没找到能响应的子View,那么直接调用super的dispatchTouchEvent,也就是View的dispatchTouchEvent
        ev.setLocation(xf, yf);
        return super.dispatchTouchEvent(ev);
    }

    //能走到这里来,说明已经有target,那也说明,这里不是DOWN事件,因为DOWN事件如果有target,已经在前面返回了,执行不到这里
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {//如果有目标,又非要拦截,则给目标发送一个cancel事件
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setAction(MotionEvent.ACTION_CANCEL);//该为cancel
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
            //调用子View的dispatchTouchEvent,就算它没有消费这个cancel事件,我们也无能为力了。
        }
        //清除目标
        mMotionTarget = null;
        //有目标,又拦截,自身也享受不了了,因为一个事件应该由一个View去完成
            return true;//直接返回true,以完成这次事件,好让系统开始派发下一次
    }

    if (isUpOrCancel) {//取消或者UP的话,把目标赋值为空,以便下一次DOWN能重新找,此处就算不赋值,下一次DOWN也会先把它赋值为空
        mMotionTarget = null;
    }
    //又不拦截,又有目标,那么就直接调用目标的dispatchTouchEvent
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);

    return target.dispatchTouchEvent(ev);
    //也就是说,如果是DOWN事件,拦截了,那么每次一次MOVE或者UP都不会再判断是否拦截,直接调用super的dispatchTouchEvent
    //如果DOWN没拦截,就是有其他View处理了DOWN事件,那么接下来的MOVE或者UP事件拦截了,那么给目标View发送一个cancel事件,告诉它touch被取消了,并且自身也不会处理,直接返回true
    //这是为了不违背一个Touch事件只能由一个View处理的原则。
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值