一步步理解Android事件分发机制

回想一下,通常在Android开发中,我们最常接触到的是什么东西?显然除了Activity以外,就是各种形形色色的控件(即View)了。
与此同时,一个App诞生的起因,终究是根据不同需求完成与用户的各种交互。而所谓的交互,本质就是友好的响应用户的各种操作行为。
所以说,有很多时候,一个控件(View)出现在屏幕当中,通常不会是仅仅为了摆设,而是还要能够负责响应用户的操作。
以最基本的例子而言:现在某一个界面中有一个按钮(Button),而每当用户点击了该按钮,我们的程序将做出一定回应。
那么,如果我们还原“点击按钮”这一行为,可以视作其根本实际就是:用户的手指 与屏幕上该按钮所处的位置发生了某种接触。
所以简单来说,我们可以将用户每次与屏幕发生接触,视作是一次“触摸事件”。而对于每次“触摸事件”的回应处理,就构成了所谓的“交互”。
由此,也就引出了我们今天在此文里想要弄清楚的一个知识点,即“Android的事件分发机制”。接着,我们就一步一步的来走进它。

为什么需要分发?

在一切开始之前,我们先考虑一个问题。那就是为什么我们发现 关于这个知识点总是被描述为“事件分发”而不是“事件处理”?试想以下情况:

小明注册了一家公司开始了自己的创业之旅。这天,一位客户上门为小明的公司带来了第一单业务。我们可以将“这单业务”视作一次“触摸事件”。
小明乐坏了,赶紧开始着手这单业务。对应来说,我们可以理解为对“触摸事件”的处理。是的,目前为止我们都没发现任何和“分发”相关的东西。
其实,我们不难想象到之所以没有出现任何“分发”相关的东西,是因为现在公司只有小明独自一人,除了自己处理,他别无它选。
对于“分发”这个词本身就是以一定数量作为基础的。比如小明的公司经过发展扩大到了十个人的规模,划分了部门。这天又有新的业务来了。
这个时候小明就有很多选择了,他可以选择自己来处理这单业务;当然也可以选择将业务派发下去让员工来处理。这就是我们所谓的“分发”。

这个案例对应到Android中来说是一样的,如果我们能保证当前屏幕上永远只有唯一的一个View,那么对于“触摸事件”的处理就没什么好说的了。
但显然一个界面中通常都不会只有一个“独苗”,它会涉及到不定量的View(ViewGroup)的组合。这个时候当“事件”发生,就不那么容易处理了。
这其实也是为什么,“Android的事件分发机制”是我们从菜鸟到进阶的过程中绕不开必须掌握的一个知识点。
但实际上也没关系,当我们明白了“小明创业”的这个例子,实际也就已经掌握了关于事件分发最基本的原理。

看事件如何分发?

我们说了所有与用户发生交互的行为都是在手机屏幕,即某个界面中产生的。而Android的界面中的元素,无非就是View与ViewGroup。
与之同时,我们关注的另一个关键点是代表操作行为的“触摸事件”,这对应到英文中来说似乎就是“TouchEvent”。
OK,在我们目前两眼一抹瞎的情况下。不妨试着以“TouchEvent”为关键字,到View与ViewGroup类中去研究研究相关的方法。

最终,我们单独提出以下几个需要我们理解的方法,它们也是事件分发机制的原理所在:

  • dispatchTouchEvent
  • onTouchEvent
  • onInterceptTouchEvent (只存在于ViewGroup)

好的,截止现在我们还不太明白这几个方法的具体作用。只知道如果单独从命名上看,它们似乎分别代表分发、处理和拦截类似的作用。

起源在哪?

从现在开始,我们要正式的一步步研究一个触摸事件的处理过程。我们先来看一个十分基础但具有一定代表性的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:tag="group_a"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:tag="group_b"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Button
            android:tag="view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>
</LinearLayout>

但如果是这样,我们还是无法研究其事件的分发过程,所以我们自定义两个类分别继承LinearLayout以及Button类。
而我们要做的工作也很简单,就是在我们之前提及的与“事件分发”相关的方法中,分别加上类似如下的日志打印:

Log.d(getTag().toString(),”onTouchEvent”);

然后我们将布局文件中的LinearLayout和Button分别替换成我们自己定义的类,就搞定了。此时,我们运行程序后,对按钮进行点击,得到如下日志:

group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_b: dispatchTouchEvent
group_b: onInterceptTouchEvent
view: dispatchTouchEvent
view: onTouchEvent

一口气吃不成一个胖子,我们这里首先只关注一点,那就是:当我们点击按钮后,最先被触发的是“group_a“也就是最外层layout的dispatchTouchEvent方法。这个道理其实并不难理解,就像“小明创业”一样,如果一单业务来临,自然应该先由最高级别的“小明”知会。

也就是说,当一个“触摸事件”产生,将最先传送到最顶层View。这看上去合情合理,但我们又不难想到,如果谈到“最顶层”的话:
那么,我们所有的View,即我们整个布局文件,它实际也有一个载体,那就是它的宿主Activity。因为你一定记得”setContenView(R.layout.xxx)”。
那么,我们想象一下,“触摸事件”会不会最先被传送到Activity呢?打开Activity的源码我们惊喜的发现:它也包含dispatchTouchEvent和OnTouchEvent方法。OK,我们接着要做的,自然就是在我们的Activity类中覆写这两个方法,也加上日志打印,再次运行测试得到如下日志:

MainActivity: dispatchTouchEvent
group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_b: dispatchTouchEvent
group_b: onInterceptTouchEvent
view: dispatchTouchEvent
view: onTouchEvent

通过日志,我们不难发现,正如我们推测的一样,当触摸事件产生,的确是最先传递给Activity的。
这个时候,又要提到“小明”了。没错,当有业务来临,肯定是先到达“公司”层面,然后才是最高负责人“小明”。

打开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来分发事件的。

if (getWindow().superDispatchTouchEvent(ev))

Window自身是一个抽象类,而其superDispatchTouchEvent也是一个抽象方法。其唯一实现存在PhoneWindow类中。
而在PhoneWindow当中,superDispatchTouchEvent的实现是这样的:

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

也就是,这时候事件的分发会传递到mDecor,即Android视图结构中所谓的“DecorView”当中,而DecorView自身本就是FrameLayout。
所以,这个时候事件分发的本质就很简单了,它回归到了FrameLayout,即ViewGroup的事件分发。

P.S: DecorView涉及到了Android的UI界面架构的知识。我们通常可以最简单的理解为一个Activiy就是一个界面。

但其实随着对Android越来越熟悉,我们也可以很容易猜测到,实际上肯定不仅仅如此。那么我们可以这样简单理解:

  • Activity会附属在PhoneWindow上;这其中会首先包裹一个DecorView(翻译就是装饰视图),它本身是一个FrameLayout。
    (所以,我们其实可以理解为,DecorView才是我们界面中真正位于最顶层的父View)
  • 而同时,通常来说,DecorView中会有两个子View,分别是:TitleView及ContentView。
  • 其中,TitleView简单来说就是我们平时所说的ActionBar(TitleBar)的那一列。而ContentView我们就熟悉了:“setContenView(R.layout.xxx)”

好了,话到这里,我们先对我们目前的收获进行总结,很简单:
当一个“触摸事件”产生,会首先传递到当前Activity。Activity会通过其所属Window,找到其中DecorView,从而开始逐级向下分发事件。

继续传递

回忆我们点击按钮时,所产生的输出日志:

MainActivity: dispatchTouchEvent
group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_b: dispatchTouchEvent
group_b: onInterceptTouchEvent
view: dispatchTouchEvent
view: onTouchEvent

我们发现,经由MainActivity进行传递,最终会首先到达该Activity的ContentView,即我们定义的布局文件中的最外层LinearLayou上。
这之后发生的事,我们通过日志观察的现象似乎是:事件在继续向下传递,直到最后到达了最里的Button,才通过onTouchEvent进行了处理。
所以,我们大胆猜测:一个“触摸事件”发生,会逐级dispatchTouchEvent,直至到达最底层的View,然后通过onTouchEvent进行处理。
这看上去是说得通的,但唯一的缺陷就在于,onInterceptTouchEvent 这个名字透露着拦截事件的方法,似乎并没有得到什么发挥?

那么,我们干脆直接打开ViewGroup中onInterceptTouchEvent的源码:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

我们发现源码特别简单,唯一值得注意的是,该方法有一个布尔型的返回值,并且默认的返回值是false。
秉承着一贯的“手贱主义”的思想,我们将我们自定义的LinearLayou类中的onInterceptTouchEvent返回值修改为true。然后再次测试:

MainActivity: dispatchTouchEvent
group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_a: onTouchEvent

我们发现日志打印已经发生了改变,通过日志我们观察到的是:事件传递到group_a后不再向下传递了,直接通过group_a的onTouchEvent进行处理。
这一现象其实是合理的,它告诉了我们:onInterceptTouchEvent的返回结果将决定事件是否被拦截在当前View层面。返回true则拦截;否则继续传递。
而onInterceptTouchEvent的返回结果之所以能实现上述效果,可以在ViewGroup类中的dispatchTouchEvent方法里找到答案。
ViewGroup当中的dispatchTouchEvent源码很多,也很复杂,我们很难去读个透彻。但通过部分关键代码,我们可以知道它的原理是如下:
ViewGroup里,dispatchTouchEvent里会调用到onInterceptTouchEvent方法,并通过判断其返回结果决定下一步操作:

  • 如果onInterceptTouchEvent返回true,则会调用onTouchEvent方法处理触摸事件。
  • 如果onInterceptTouchEvent返回false,则会通过child.dispatchTouchEvent的形式向下分发事件。

到现在,看上去我们基本已经对事件的整个分发流程都触碰到了。但我们想象这样一种情况:
小明的公司接收了一单新的业务,并分发给了底下的小王去做。但这时候的问题可能是:
小王收到通知后发现自己完成不了。或者说小王给出了一套方案,但他对该方案的信心并不足。
这样的问题应该怎么解决,很显然,小王应该“报告老板”。这单业务最后的处理方案可能还是得您来拿。

这对应到我们的程序中,应该如何来实现呢?我们发现,onTouchEvent这个方法也有一个布尔型的返回值。
现在,我们试着将我们之前自定义的Button类里的onTouchEvent的返回值修改为固定返回false。再次运行程序测试:

MainActivity: dispatchTouchEvent
group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_b: dispatchTouchEvent
group_b: onInterceptTouchEvent
view: dispatchTouchEvent
view: onTouchEvent
group_b: onTouchEvent
group_a: onTouchEvent

我们发现,事件在经过button类里onTouchEvent处理后, 又回传到上层目录继续处理了。
由此我们可以知道:onTouchEvent的返回值也是另一种“拦截”,不同的是之前我们说的是拦截事件继续向下传递。
而这里,是在表明,这个事件“我”是否能够完全处理。如果能,则返回true,那么事件经过“我”处理后就结束了。否则返回false,再让父View去处理。

好了,这里我们先总结一下到目前为止,我们掌握到的关于事件分发机制的流程:

  • 当一个触摸事件产生,会首先传递到其所属Activity。Activity将负责将事件向下进行分发。
  • 该触摸事件将逐级的依次向下传递,直到传递至最底层View,然后进行处理。
  • ViewGroup在向下传递事件的时候,通过onInterceptTouchEvent的返回值来判断是否拦截事件。
  • 如果确定拦截事件,那么此次一系列的触摸事件都会通过该ViewGroup的onTouchEvent方法进行处理,并不再向下传递
  • onTouchEvent的返回结果决定了事件在此次处理之后,是否需要回传到上一级的View。

好了,我们继续看。我们肯定也注意到了,说了这么多,但我们之前关于事件分发的重点,都是放在上一层View向下层View分发事件的过程。
也就是说,之前我们的重点是在分析ViewGroup的dispatchTouchEvent方法,我们说了该方法会判断是否拦截事件,而决定是否继续向下传递事件。
当就拿我们此文中的例子来说,当事件最终传递到button类当中。我们不难想象到,这时dispatchTouchEvent的工作肯定与之前有所不同。
因为Button自身只是一个View,它不会存在所谓的子视图。也就说,这个时候事件你肯定要做出处理了,那么还分发个什么劲呢?

要得到这个问题的答案,最好的方式当然就是打开View类里的dispatchTouchEvent方法的源码看看,这里我们只截取我们关心的代码部分:

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            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;
            }
        }

我们看到,首先会获取一个ListenerInfo类型的变量。该类型实际就是封装了各种类型的Listener。随后会开启第一次判断:
如果此对象不为空,mOnTouchListener不为空,且(mViewFlags & ENABLED_MASK) ==ENABLED,则会执行mOnTouchListener。
这其中(mViewFlags & ENABLED_MASK) ==ENABLED是判断控件状态是否是ENABLE的,默认情况肯定是的。
mOnTouchListener看着有些眼熟,如果我们查看源码,你就更眼熟了,因为我们发现它是通过如下代码进行赋值的:

    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

接着看代码,我们发现如果li.mOnTouchListener.onTouch(this, event)执行的结果也为true,那么result也将被设置为true。
假设该方法的执行结果返回是false,那么以上代码将继续进行,从而开始第二轮判断,于是onTouchEvent方法得以执行。

OK,对于这里的代码,我觉得有两点是值得一说的。第一是,我们首先可以明白如下的结论:

  • 假如我们为某个View设置了OnTouchListener,那么listener会先于onTouchEvent执行。同时:
  • 如果listener的onTouch方法返回true,那么onTouchEvent将不再执行。

第二点是,我们看到对于View来说:假设onTouchEvent的返回值为false,那么dispatchTouchEvent也会返回false。
这实际上就构成了整个“onTouchEvent返回值可以决定事件是否回传”的原因。这是因为:
ViewGroup在通过child.dispatchTouchEvent向下分发事件时,会通过该返回值来判断子视图是否具备处理事件的能力。
如果返回为false,就会继续遍历子视图,直至遍历到有一个具备事件处理能力的子视图为止。
如果没有一个具备处理能力的子视图,那么ViewGroup就会自己通过onTouchEvent来解决了。

到了这里,我们实际上已经对整个Android事件的分发机制有了不错的了解了。但我们肯定会想到一个问题:
那就是,以Button来说,我们通常是通过setOnClickListener的方式来对其设置点击事件监听的。
但很显然,我们目前为止研究过的代码中,还没有出现任何与之相关的东西。在View类中,我们剩下没有看过代码的,就是onTouchEvent了:

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

以上代码是我们关心的原因所在,我们看到经过一系列的判断后,会进入到一个名为performClick的方法调用,打开该方法的源码:

    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会在onTouchEvent方法中,ACTION为UP的时候得以执行。
  • 但注意前提条件是,控件是可点击(长按)的;并且mOnClickListener不为空。

最后的总结与比喻

最后,我们仍旧以“小明创业“的例子来总结我们所学到的关于Android的事件分发的相关知识。

  • 公司(Activity)接收了一项新的业务(触摸事件)。
  • 该业务会最先到达公司的最高层小明手上(最顶层View)。
  • 小明研究了以下此次业务,此时可以分为两种情况:
    1、小明认为此业务难度不大,于是决定将业务分配给部门经理小王(dispatch-event)。
    2、小明认为此次业务至关重要,决定自己处理(intercept-event & onTouchEvent)。
  • 假设业务分配给了经理小王,此时小王与小明一样,同样有两种选择:即自己拦截处理或者继续向下布置。
  • 业务最终终将被分配到某个员工的手中,它会负责处理(onTouchEvent)。
  • 但该员工接收到任务后,也可以判断自己是否能够完成,如果觉得不能完成,
    或者还需要上级审核,可以选择回报给上级(onTouchEvent返回false)
  • 员工处理该任务的方式也许有多种,例如A(OnTouchListener)、B(onTouchEvent)、C(OnClickListener) 等等。
    (它们的优先级顺序是 A > B > C 。。。。 )
  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值