Android触摸事件分发机制(一)

1. 简介

本文主要分享事件分发中的基本概念。

介绍负责参与分发事件的主要方法。

从这些方法的核心逻辑中,总结事件分发的规律。

2. 被分发的对象


被分发的对象是那些?被分发的对象是用户触摸屏幕而产生的点击事件,事件主要包括:按
下、滑动、抬起与取消。这些事件被封装成 MotionEvent 对象。该对象中的主要事件如下表
所示:

按下、滑动、抬起、取消这几种事件组成了一个事件流。事件流以按下为开始,中间可能有
若干次滑动,以抬起或取消作为结束。
在安卓对事件分发的处理过程中,主要是对按下事件作分发,进而找到能够处理按下事件的
组件。对于事件流中后续的事件(如滑动、抬起等),则直接分发给能够处理按下事件的组
件。故本文讨论的内容则是主要针对按下事件的。

 3. 分发事件的组件

分发事件的组件,也称为分发事件者,包括 Activity、View 和 ViewGroup。它们三者的一般
结构为:

事件分发者结构
从上图中可以看出,Activity 包括了 ViewGroup,ViewGroup 又可以包含多个 View。 

4. 分发的核心方法 

负责对事件进行分发的方法主要有三个,分别是:
dispatchTouchEvent(
onTouchEvent()
onInterceptTouchEvent()。
它们并不存在于所有负责分发的组件中,其具体情况总结于下面的表格中:

从 表 格 中 看 , dispatchTouchEvent,onTouchEvent 方 法 存 在 于 上 文 的 三 个 组 件 中 。 而
onInterceptTouchEvent 为 ViewGroup 独有。这些方法的具体作用在下文作介绍。
ViewGroup 类中,实际是没有 onTouchEvent 方法的,但是由于 ViewGroup 继承自 View,而
View 拥有 onTouchEvent 方法,故 ViewGroup 的对象也是可以调用 onTouchEvent 方法的。故在表格中表明 ViewGroup 中存在 onTouchEvent 方法的。 

5. 事件分发过程

这一小节是本文的核心内容,会从整体上对事件的分发过程作介绍。
对于事件分发过程从,笔者认为网上的一些教程中的观点是有误的。
网上部分教程认为事件是从内部(如 Button)开始分发的,这是有误的。
网上部分教程常使用’向上‘、’向下‘传播等描述,但又未对‘何为上’、‘何为下’作解释。
网上部分教程将 Java 的子类对象调用父类方法(向上转型)的过程也称为‘向上’传播,即将
事件在组件之间的传播与程序语言多态特性混为一谈,让初学者费解。
子类在覆写的方法中调用父类的同名方法,被称为’向上传播‘,这也是不对的。
为此在介绍分发过程之前,先对一些概念作定义:
向下传播:Activity 包括 Layout,事件从 Activity 向 Layout 传播被称作’向下传播‘。Layout 包
含若干 View,事件从 Layout 向其子 View 传播,也被称为’向下传播‘。
向上传播:与’向下传播‘相反。
’向上转型‘不能称为传播,即子类对象调用父类方法,或在覆写的方法中调用父类方法,都
不能称为传播。不能将面向对象程序语言中的概念与布局层次中的上下传播混为一谈。
分发方法 dispatchTouchEvent
从方法的名称中可以看出该方法主要是负责分发,是安卓事件分发过程中的核心。事件是如
何传递的,主要就是看该方法,理解了这个方法,也就理解了安卓事件分发机制。
在了解该方法的核心机制之前,需要知道一个结论:
如果某个组件的该方法返回 TRUE,则表示该组件已经对事件进行了处理,不用继续调用其余
组件的分发方法,即停止分发。
如果某个组件的该方法返回 FALSE,则表示该组件不能对该事件进行处理,需要按照规则继续
分发事件。在不复写该方法的情况下,除了一些特殊的组件,其余组件都是默认返回 False
的。后续有例子说明。
为何返回 TRUE 就不用继续分发,而返回 FALSE 就停止分发呢?为了解决这个疑问,需要看
一看该方法的具体分发逻辑。为了便于理解,下面对 dispatchTouchEvent 方法进行简化,只
保留最核心的逻辑。
Activity 的 dispatchTouchEvent 方法

// Activity 中该方法的核心部分伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
if (child.dispatchTouchEvent(ev)) {
    return true; //如果子 View 消费了该事件,则返回 TRUE,让调用者知道该事件已
被消费
} else {
    return onTouchEvent(ev); // 如 果 子 View 没 有 消 费 该 事 件 , 则 调 用 自 身 的
    onTouchEvent 尝试处理。
    }
}

首先,从核心逻辑中看出,当事件传递给 Activity 后,它先将事件分发给子 View 处理。
如果经过子 View 层层传递或处理后,该事件被消费了(即返回了 TRUE),则 Activity 的分
发方法也返回 TRUE,同样也表示该事件已经被消费了。
如果经过子 View 层层传递或处理后,该事件没有被消费(即返回了 FALSE),则 Activity 的
分发方法就不会返回 TRUE 了,而是调用 onTouchEvent()去处理,看其实际的处理情况。
如果 onTouchEvent 消费了事件,那依然能返回 TRUE(表示已消费事件),这个 TRUE 作为
dispatchTouchEvent 的返回值,让调用它的对象知道该 Activity 已经消费了事件。
如果 onTouchEvent 没有消费该事件,那就返回 FALSE(表示未消费事件),这个 FALSE 作为
dispatchTouchEvent 的返回值,让调用它的对象知道该 Activity 没有消费事件,需要继续处理。
ViewGroup 的 dispatchTouchEvent 方法

// ViewGroup 中该方法的核心部分伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (!onInterceptTouchEvent(ev)) {
        return child.dispatchTouchEvent(ev); //不拦截,则传给子 View 进行分发处理
    } else {
        return onTouchEvent(ev); //拦截事件,交由自身对象的 onTouchEvent 方法处理
    }
}

ViewGroup 的该方法与 Activity 的类似,只是新添了一个 onInterceptTouchEvent 方法。当事
件传入时,首先会调用 onInterceptTouchEvent。
如果该方法返回了 FALSE(表示不拦截),则交给子 View 去调用 dispatchTouchEvent()方

如果该方法返回了 TRUE(表示拦截),则直接交给该 ViewGroup 对象的 onTouchEvent(ev)
方法处理,具体是否能处理以 onTouchEvent()的实际情况为准。
实 际 上 , 在 onInterceptTouchEvent 返 回 TURE 表 示 拦 截 时 , 实 际 调 用 的 是
super.dispatchTouchEvent 方法,即 View 的该方法,进而由该方法调用 onTouchEvent.
View 的 dispatchTouchEvent 方法

// View 中该方法的核心部分伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
//如果该对象的监听成员变量不为空,则会调用其 onTouch 方法,
    if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
        return true; // 若 onTouch 方 法 返 回 TRUE , 则 表 示 消 费 了 该 事 件 , 则
        dispachtouTouchEvent 返回 TRUE,让其调用者知道该事件已被消费。
    }
    return onTouchEvent(ev); //若监听成员为空或 onTouch 没有消费该事件,则调用对象自身的        onTouchEvent 方法处理。
}

从该方法的核心逻辑中可以看到,事件传递进来后,首先会对 mOnTouchListener 判空,如
果之前 Set 了 Listener,则会调用其 onTouch 方法。
若 onTouch 方法返回 TRUE,则 dispatchTouchEvent 也会返回 TRUE,表示消费该事件。
若 onTouch 方法返回 FALSE,或者 mOnTouchListener 本来就是空,则调用自身的 onTouchEvent()
来处理,是否消费事件,可以由其返回值判断。
实际上,在 View 的 onTouchEvent 方法中,如果设置了 onClickListener 监听对象,则会调用
其 onClick 方法。
在 同 时 设 置 了 onTouchListener 与 onClickListener 对 象 的 情 况 下 , 正 是 由 于 View 的
dispacthTouchEvent 方法会先调用 mOnTouchListener 的 onTouch,才会调用 onTouchEvent 方法,
所以 onTouchListener 对象的 onTouch 方法是优先于 onClickListener 对象的 onClick 方法调用
的。这里只简单描述结论,具体源码请查看本文对应的高级篇内容。
小节:dispatchTouchEvent 方法
回顾上面 Activity、ViewGroup 和 View 中的 dispatchTouchEvent 方法,它们大体都可以分为
两部分,前一部分是交由子 View 的 dispatchTouchEvent 方法或 onTouch 方法进行处理,后一
部分是交给自身的 onTouchEvent 方法处理。这样理解的话,就非常便于记忆了。
为了便于记忆和理解,可以将各组件的 dispatchTouchEvent 方法分为两部分:
子 View 的 dispatchTouchEvent 或 onTouch 方法
自身的 onTouchEvent 方法
这个结构有点类似于递归的过程,就是组件的 dispatchTouchEvent 会自用子组件的同名方法,
子组件一样会调用子子组件的同名方法,直到递归到底,然后在从递归底部返回上层,直到
返回到最上层,整个过程结束。或者在这个过程中,事件传递到某个子 View,该子 View 决
定处理该事件,则事件交给其自身的 onTouchEvent 方法处理,如果 onTouchEvent 方法处理
不了,再交由父组件的同名方法处理,直到向上传递到顶层结束。
于是,就有了很多教程里的 U 型图。

安卓分发事件 U 型图
从 U 型图中可以发现,其实安卓事件分发的主体思路非常简单,即由父组件不断向子组件
分发,若子组件能够处理,则立刻返回。若子组件都不处理,那传递到底层的子组件,再返
回回来。这个过程类似上面说的递归的过程。
这 里 对 这 个 U 型 图 做 一 下 说 明 , 先 看 图 中 左 上 角 , 事 件 传 到 Activity , 首 先 调 用 其
dispatchTouchEvent 方法,其会传递给子 View 处理,该子 View(在图中是 ViewGroup)会调
用其 dispatchTouchEvent 方法,如果该方法被覆写直接返回 TRUE,则立即返回 Activity,表示
已 经 消 费 事 件 。 如 果 该 方 法 没 有 被 覆 写 或 调 用 了 super 的 同 名 方 法 , 则 会 调 用
onInterceptTouchEvent 方法,如果该方法返回 TRUE 拦截事件,则交给自身的 onTouchEvent
处 理 , 如 果 该 方 法 返 回 FALSE 不 拦 截 , 则 继 续 传 给 子 子 View ( 图 中 是 View ) 的
dispatchTouchEvent 方法处理。此时,再看看这个 U 型图,该递归调用已经到底了,若在该
方法中的 onTouchListener 方法不处理,则调用自身的 onTouchEvent 处理。若还是处理不了,
则从递归底部向上返回,依次调用 ViewGroup 的、Activity 的 onTouchEvent 方法。
实 际 上 , 用 这 个 U 型 图 来 描 述 安 卓 的 事 件 分 发 机 制 并 不 一 定 准 确 , 因 为 同 一 对 象 的
dispatchTouchEvent 方法实际是包含了另外几个方法的(Activity 与 View 只包含 onTouchEvent),
但是在这个图中,却是将几个方法分别画在不同的框中。所以通过该 U 型图来理解事件分
发机智是不准确的。但是对于部分读者可能会有所帮助。要准确理解事件调用机制,还是应
该回到上面,查看三个核心方法的核心逻辑,就能够准确理解。
强调说明,安卓事件分发的‘向上’与‘向下‘传播,不要与面向对象程序语言中基类与子类关系,
或子类向上调用父类方法等概念搞混淆。对于安卓事件分发的‘向上’与‘向下‘传播,这里的上
与下,是指在’递归‘调用过程中的上与下(也体现到 U 型图里的上与下)。这个概念,体现到布局中,就是外与内。即这里所说的事件’向下‘传播,等同于在布局上,由外向内传播,
而’向上’传播,等同于在布局上,由内向外传播。
在面向对象程序语言中,对于子类覆盖父类方法,或子类调用父类方法,这些‘上’与‘下’的关
系,在布局层面上并没有跨越布局层次,不要与事件传播的方向概念相混淆。
拦截方法 onInterceptTouchEvent
该方法是 ViewGroup 类对象所独有的,用于对事件进行提前拦截。在一般情况下,该方法是
默认返回 FALSE 的,即不拦截。
如果自定义的 ViewGroup 希望拦截事件,不希望事件继续往子 View 传播,可以覆写该方法,
返回 TRUE,即可阻止向下的传播过程。
实际上,从上面的核心逻辑的伪代码中可以看出,在 ViewGroup 调用 dispatchTouchEvent 后,
肯定会调用该方法,根据该方法的返回值来确定如何处理。若该方法返回 True,则会将事件
拦截掉,就给自身的 onTouchEvent 处理。如果返回 False,则继续传递给 child 执行分发流程。
处理方法 onTouchEvent
该方法主要对事件进行处理,若返回 True 表示已经处理了事件,若返回 False 则表示没有对
事件进行处理,需要继续传递事件。一般情况下,默认为 FALSE。在 View 的 onTouchEvent
方法中,如果设置了 onClickListener 监听对象,则会调用其 onClick 方法。

6. 总结

本文在介绍了事件分发基本概念的基础上,介绍了负责参与事件分发的核心方法,包括
dispatchTouchEvent()、onInterceptTouchEvent 与 onTouchEvent 方法。通过伪代码的形式介绍
了这些方法的核心逻辑,重点分析了在 Activity、ViewGroup 与 View 中的 dispatchTouchEvent
方法。它们三者中的该方法结构类似,都是先调用子 View 的同名方法或者 listener 方法,然
后再调用自身的 onTouchEvent 方法。
这些方法在调用关系中体现了一个类似‘递归’的调用过程,通过 dispatchTouchEvent 将事件
传 递 下 去 , 又 通 过 onTouchEvent 将 事 件 传 递 上 来 。 中 间 的 这 一 过 程 可 以 通 过 让
onInterceptTouchEvent 方法(对于 ViewGroup),或者另外的负责分发的方法返回 TRUE,均
可以提前终止这一类似’递归‘的调用过程,进而让事件的处理符合我们的预期。

 

猜你喜欢

转载自blog.csdn.net/loveseal518/article/details/131968638
今日推荐