Android事件分发机制

学习android开发,我想大多数人都是从findViewById()这个函数开始的。这个函数是用来绑定到你界面上所放置的控件的。对于一个Button控件来说,在绑定好后,你通常会给它设置一个监听事件。然后在onClick()函数里写你需要完成的动作。这样你在点击Button后,onClick()函数中的代码就会执行。

mButton.setOnClickListener(new View.OnClickListener()
{
    @Override
    public void onClick(View v)
    {
        //点击button,将打印出log
        Log.w(TAG,"click");
    }
});

我们在控制发现,点击后,打印出了log,和我们想象中的一致。
这里写图片描述
然后在后来的学习中,你会发现Button不只能设置OnClickListener(),它还有很多的监听事件,其中一个叫做setOnTouchListener()的方法,里面也有一个onTouch()的方法。同样在里面打印出log。

mButton.setOnTouchListener(new View.OnTouchListener()
{
    @Override
    public boolean onTouch(View v, MotionEvent event)
    {
        Log.w(TAG,"touch");
        return false;
    }
});

点击button按钮查看日志。
这里写图片描述
我们发现,先打印了两次touch,然后才打印了click。其实你如果多试几次的话,还有可能会先打印出三次touch,这是什么原因呢?那是因为在你触摸button的时候,会执行onTouch函数,在你手指离开屏幕的时候也会触发onTouch函数,如果你还在屏幕上滑动的话也会执行onTouch函数。所以才会有两次或者三次log。
我们发现onTouch函数与onClick函数还是有所不同的,它有返回值,而且默认为false。如果我们将返回值改成true的话会怎么样呢?

mButton.setOnTouchListener(new View.OnTouchListener()
{
    @Override
    public boolean onTouch(View v, MotionEvent event)
    {
        Log.w(TAG,"touch");
        return true;
    }
});

这里写图片描述
点击过后,从控制台上发现,并没有打印click日志。所以猜测在onTouch函数中返回true的话,会导致onClick不会执行。
首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。所以我们可以去看看这个方法。
我们知道所有控件都是直接或间接继承于view的,而这个dispatchTouchEvent方法也是在view中(viewGroup重写了dispatchTouchEvent)。

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

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ............
        ............
        return result;
 }

view中的dispatchTouchEvent方法中代码很多,但对于一般的控件来说都会执行到上面的代码,首先会定义一个ListenerInfo,由名字可以知道这是一个各种监听事件的封装。接着进行判断。其中有四个条件,都为true的时候才会将result设置为true。

第一个条件因为对li初始化了,所以不为空。
第二个条件是判断ListenerInfo 里面的mOnTouchListener是不是为空,看到这里我们是否有些熟悉,其实这就是我们在给button设置setOnTouchListener时对其赋了值,也就是说只要我们给控件注册了touch事件,mOnTouchListener就一定被赋值了。
第三个条件是(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,所以也是true。但是某些不可点击控件,这里将不为true。
第四个条件是mOnTouchListener.onTouch(this, event)的返回值,看到这是不是有种明白的感觉。如果你在onTouch中返回的是false,result将依旧为false。如果在onTouch中返回的是true,result将为true。
接着又会进入到一个判断 if (!result && onTouchEvent(event)),他有两个判断条件。

第一个result如果为真,直接跳出,如果result为假,会执行onTouchEvent(event)。也就是说如果在上一个判断中,将result改为true,那么将不会执行onTouchEvent(event)。结合我们修改onTouch返回值为true后不执行onClick这一现象来分析,我们有理由相信onClick()函数在onTouchEvent(event)事件中。
第二个参数为onTouchEvent(event)返回值,如果为true,修改result为true,最终dispatchTouchEvent返回true。如果为假dispatchTouchEvent返回假。

那么我们现在来看看onTouchEvent()的代码。

public boolean onTouchEvent(MotionEvent event) {
        .............
        .............
        .............
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                ...............
                ...............
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClick();
                }
                .................
                return true;
            }

        return false;
    }

onTouchEvent里面的代码更多,所以我们看一些关键代码。首先第一个if中来判断这个是否是一个可以点击的控件,如果不是直接返回false。如果是进入一个switch判断,最终会执行到performClick()函数。然整体返回true。

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }
}

同理,如果我们注册了OnClickListener这个监听,最终会执行到mOnClickListener.onClick(this)这个函数,而onClick就是你最终写代码的地方。

我们梳理一下dispatchTouchEvent返回true或false的几种情况:
返回true:
1.没有注册OnTouchListener监听,且onTouchEvent返回值为真(控件可以点击)。
2.注册了OnTouchListener监听,但在onTouch返回为false,且onTouchEvent返回值为真(控件可以点击)。
3.注册了OnTouchListener监听,在onTouch返回为true。
返回false:
1.没有注册OnTouchListener监听,且onTouchEvent返回值为假。
2.注册了OnTouchListener监听,但在onTouch返回为false,且onTouchEvent返回值为假(控件可以点击)。

那么view中的dispatchTouchEvent返回值对android中事件分发机制有什么影响呢?
在上面我们提到过,viewgroup中重写了dispatchTouchEvent这个方法。而且我们知道android界面是由一层套一层的,所以事件不只是在一个view中传递,还会再不同控件和不同层级进行传递。而可以确定的是,在不同层级中,事件是从上往下传,从viewgroup往view中传递的。那么事件又是如何保证能准确传的我们希望的控件上的呢?这也与viewgroup的dispatchTouchEvent方法有关系。

因为viewgroup的dispatchTouchEvent太长太复杂了,因为笔者水平有限,这里就不贴源码一个个分析,我大致来讲一下它的执行过程。

1.首先它会判断当前viewgroup是否对事件进行拦截,判断的依据是“disallowIntercept”(是否不允许拦截,默认为false)和”onInterceptTouchEvent“(是否拦截事件,默认为false)。这两个值都可以通过相应的函数进行修改。如果该viewgroup允许且拦截了这个事件,则将会交给自己处理,而最终会调用return super.dispatchTouchEvent(ev)方法,也就是它的父类view的dispatchTouchEvent(ev)方法,就会按view分发机制执行。如果不拦截该事件,就将进入第二步。

2.对当前viewgroup下的view或viewgroup进行遍历,判断当前遍历的是不是正在点击的。如果是的话,调用它的dispatchTouchEvent,如果当前点击的是一个viewgroup的话,就会调用viewgroup的dispatchTouchEvent(从第一步又开始执行),如果是view的话,就调用view的dispatchTouchEvent方法。
但最终都会返回一个boolen类型的值,如果返回来的值是true,表示事件消费了,然后向上层返回true,但如果返回的是false,表示没有消费,也会执行return super.dispatchTouchEvent(ev),向上层返回super.dispatchTouchEvent(ev)的值。

说起来很抽象,网上有一张图很形象,我就借花献佛在这里贴出来帮助大家更好的理解(来自Kelin)。
这里写图片描述

总结

总觉得自己已经很理解Android的事件分发机制了,可是在写起来还是十分的费劲。在看viewgroup的dispatchTouchEvent源码的时候,受到了特别大的折磨。想想还是因为自己的能力不够,很多代码都没法知道它的意思,只能抽丝剥茧,阅读比较重要,比较关键的部分进行分析。纵观这篇文章,感觉自己还是没有完全将自己想说的给清楚的说出来,所以大家看起来可能会有很大的困惑。希望以后我能将自己的想法表达清楚。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值