Android事件分发机制抽象--钓钩模型,这些面试官常问的开发面试题你都掌握好了吗

接下来的问题只需要围绕 FrameLayout 和 TextView 两个控件的顺序说出事件分发相关方法调用即可。因为场景固定,不存在如果,即答案对应的是唯一路径,不存在如果…就…

为了便于理解,在回答上述问题前,我先介绍一下事件分发机制的核心方法以及对应的功能:

dispatchTouchEvent:控件事件分发主体逻辑,View 中的该方法用于调用 OnTouchListener.onTouch 和 onTouchEvent,ViewGroup 中该方法用于判断是否拦截,不拦截则遍历子控件分发。

onInterceptTouchEvent:是否拦截事件。若拦截事件,则事件不会分发给子控件,而是直接给自己消费。

onTouchEvent:消费事件主体逻辑,用于处理按键状态、OnClickListener.onClick 和 OnLongClickListener.onLongClick。玉刚哥的几行伪代码,已将上面三大核心方法融会贯通。拿来帮助大家重温经典。中间夹带了自己的一点思考,详见第 12、13 行。

基于上述我夹带的伪代码画了一幅流程图,如下图所示:▼

《Android 开发艺术探索》第 3 章 142 页中使用了一个通俗易懂的例子一语道破了事件分发机制的“天机”。

假如点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定(onTouchEvent 返回了 false),现在该怎么办呢?

难题必须要解决,那只能交给水平更高的上级解决(上级的 onTouchEvent 被调用),如果上级再搞不定,那只能交给上级的上级去解决,就这样将难题一层层地向上抛,这是公司内部一种很常见的处理问题的过程。

不 设 按 键 监 听 点 击 分 发 不设按键监听点击分发 不设按键监听点击分发

▼ ▼ ▼

1. 不设置按键监听,在红色区域点击一下,顺序说出调用了哪个控件的哪个事件分发相关方法?

这个问题比较简单,无需赘述,答案如下(首行缩进关系表示当前方法在上一步方法内部调用):

① 调用 FrameLayout 的 dispatchTouchEvent,即对应 ViewGroup 中的 dispatchTouchEvent 方法。

② 调用 FrameLayout 的 onInterceptTouchEvent。因为没有重写事件拦截,所以返回默认 false。

③ 调用 TextView 的 dispatchTouchEvent,即对应 View 中的 dispatchTouchEvent 方法。

④ 调用 TextView 的 onTouchEvent。因为 onInterceptTouchEvent 只有 ViewGroup 有,TextView 不是 ViewGroup,也就不存在事件拦截方法。因为未设置相关按键监听消费事件,所以返回默认 false。

⑤ 调用 FrameLayout 的 super.dispatchTouchEvent,即对应 View 中的 dispatchTouchEvent 方法。因为子控件 TextView 没有消费事件,转由 FrameLayout 尝试消费事件。

⑥ 调用 FrameLayout 的 onTouchEvent。因为未设置相关按键监听消费事件,所以返回默认 false。

相信这个问题难不倒大部分同学。但是,问题结束了吗?

众所周知,普通点击事件包含 DOWN 事件和 UP 事件,上面说的只是 DOWN 事件,UP 事件呢?

1.1 因为 DOWN 事件无人消费,那么 UP 事件是否还能分发到 FrameLayout?

如果不能,那 UP 事件去哪了?

这个问题其实我刚开始自问自答时,也没有回答上来。

在回答这个问题前,有必要科普一下 Android 开发者文档中描述的事件流一致性保证(Consistency Guarantees):

按下开始,中间可能伴随着移动,直到松开或者取消结束。

DOWN → MOVE(*) → UP/CANCEL。

简单来说,一条事件流就像一辆火车,车头和车尾是必须要有的,中间的车厢可有可无,有的话可以是任意节。DOWN 事件相当于火车头,UP 或 CANCEL 相当于火车尾,MOVE 事件相当于火车厢。我们所熟悉的 onClick 按键监听就是由完整事件流共同决定是否触发响应。 事件流火车模型如下图所示:▼

如果控件及其子孙控件都没有消费 DOWN 事件,则该控件不会收到接下来的事件流。

《Android 开发艺术探索》中的比喻十分生动形象:领导给你安排一件事,如果你中间掉链子,那就没有然后了,因为机会只有一次。

按此逻辑,DOWN 事件没有消费,那应该是不会收到 UP 事件了。如果是这样,那么问题来了,UP 事件去哪了?毕竟没有控件消费 UP 事件。

凭直觉,可能是给 Activity 消费了,通过自定义重写 Activity 的 dispatchTouchEvent 和 onTouchEvent,FrameLayout 的 dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent,FrameLayout 的 dispatchTouchEvent 和 onTouchEvent,加上日志,点击一下。

答案一目了然:UP 事件会继续调用 Activity 的 dispatchTouchEvent 和 onTouchEvent,但不会再调用 FrameLayout 和 TextView。

阅读过源码的同学大概知道,Activity 并没有事件分发逻辑,兜兜转转最终调用的还是 DecorView 的事件分发,而 DecorView 是继承自 ViewGroup,也就是事件分发主体逻辑还是由 ViewGroup 和 View 完成的。

按键响应调用如下图所示:▼

所以,事件大概率被 DecorView 消费了。如果继续靠猜,那效率就有点低了。最直接最有效的方式就是 Debug 源码。

在 build.gradle 中将 compileSdkVersion 和 targetSdkVersion 指定成和 Android 模拟器一样的版本,并且在 Debug 调试时下载对应源码。

接下来,只是时间问题。

多说一句,千万别在 ViewGroup 或 View 中直接断点,这么做会很容易让你内心崩溃…

因为所有控件都会继承 View(包括 ViewGroup),而你在 Activity 中的 setContentView 并不是 View 树的全部,像状态栏、导航栏等都属于页面内容的一部分,而这些,系统帮你做了。

页面布局如下图所示:▼

科学的操作是先通过日志摸清情况,找到规律,然后控制局面,有的放矢,通过自定义控件重写相关方法,在自定义控件中打断点,断住后单点跟进,精准查看逻辑。

细节建议读者实操一遍,我直接说结果了:

DOWN 事件:TextView 和 FrameLayout 未消费DOWN事件,会继续向上回传到 DecorView,调用 DecorView 的 onTouchEvent。

但 DecorView 也不消费,继续传给 Activity,调用 Activity 的 onTouchEvent,Activity 返回 false。

简而言之,DOWN 事件会陆续调用到 DecorView 和 Activity,始终没有被消费。

UP 事件:Activity 的 dispatchTouchEvent 先调用到,接着调用 DecorView 的 dispatchTouchEvent。

因为 mFirstTouchTarget 为 null,不会调用 onInterceptTouchEvent,但会设置 intercepted 状态位为 true。逻辑见下述 ViewGroup 中 dispatchTouchEvent 源码片段,执行逻辑为第 4 行和 16 行。

接着调用 DecorView 的 onTouchEvent,显然,DecorView 也不消费,继续传给 Activity,调用 Activity 的 onTouchEvent,Activity 返回 false。

简而言之,UP 事件也不会被消费,而且只会调用 DecorView 和 Activity 的事件分发相关方法,其他控件将无法收到事件分发调用。

ViewGroup 中 dispatchTouchEvent 逻辑源码片段如下图所示:▼

这个问题看似简单,但实际能回答上来的才是真的高手。

画一幅时序图总结一下:▼

但可能有同学会问,不设置按键监听情况下,没啥实际意义,大部分人不会关心这种情况,换一题。

设 按 键 监 听 a n d 拦 截 点 击 分 发 设按键监听 and 拦截点击分发 设按键监听and拦截点击分发

▼ ▼ ▼

2. FrameLayout 和 TextView 均设置按键监听,要求在红色和蓝色区域任意位置点击,只由 FrameLayout 的按键监听响应,怎么做?

这个简单,我来!

重写 FrameLayout 的 onInterceptTouchEvent 方法返回 true。

答案没毛病。但小问题接踵而至,DOWN 事件和 UP 事件可能都会触发调用 onInterceptTouchEvent,上面的答案不区分 DOWN 还是 UP,简单粗暴的返回了 true。DOWN 事件一定要返回 true 吗?返回 false 行不行?UP 事件呢,需不需要返回 true?

2.1 只能拦截 DOWN 事件吗?拦截 DWON 事件后,UP 事件需不需要返回 true?

这里有必要先科普一下按键监听 OnClickListener 的小知识点:

onClick 是由UP 事件的 onTouchEvent 触发调用的,但是触发的前提条件是已标记 PFLAG_PRESSED 按下状态位,而标记操作恰恰是在 DOWN 事件中做的。这也就解释了事件流的连续性。MOVE 事件呢?这是第三题,这里先按下不表。基于上述理论,DOWN 事件是一定要拦截的。但是 UP 事件,想必有部分同学开始模棱两可了,返回 true 肯定对,返回 false 好像也对…

从常识判断,如果一个返回布尔值的纯函数,调用后返回 ture 和返回 false 效果一样,那这个调用肯定是冗余的。onInterceptTouchEvent 基本可以看成是这种纯函数。

基于对 Android Framework 工程师的基本尊重,犯这种低级错误没有道理。

那么结论只能是:onInterceptTouchEvent 在 DOWN 事件返回 true,那么后续 UP 事件根本不会再调用 onInterceptTouchEvent。

换个角度看,如果 onInterceptTouchEvent 在 DOWN 事件返回 true,意味着本控件将拦截处理后续的事件流,后续事件调用自然也就用不着再问要不要拦截。

事实也是如此。

① onInterceptTouchEvent 只能拦截 DOWN 事件,否则 FrameLayout 的按键监听不会响应。

② 无需处理 UP 事件,因为 DOWN 事件拦截后,后续事件流根本不会再调用 onInterceptTouchEvent。因为 FrameLayout 直接在 DOWN 事件就拦截了,TextView 没有机会消费事件,不会有什么问题。但如果我们继续向前走一步,进一步窥探事件分发机制。

2.2 不拦截 DOWN 事件只拦截 UP 事件,UP 事件到底由谁消费?

按理说,DOWN 事件由 TextView 消费,UP 事件被 FrameLayout 拦截,当然是 FrameLayout消费。但如果是这样,TextView 怎么办,考虑过被拦截的子控件的感受了吗?

好比领导给了机会,我也兢兢业业的投入工作,然后就戛然而止…让不让干好歹给个痛快话,我还在干杵着呢…

显然,拦截的控件满意了,但被拦截的控件也不能不管,成熟的事件分发机制必须能妥善解决这些 “民事纠纷”。

这就涉及到了一个高级知识点了-- CANCEL 事件。

这年头,不知道 CANCEL 事件的都不好意思说自己精通事件分发(反正我不敢说精通)。

当 ViewGroup 的子类重写 onInterceptTouchEvent 返回 true 拦截事件后,如果存在被拦截的子控件(该事件流的头部事件已被子控件消费),子控件将会收到一个 CANCEL 事件被告知事件流到此为止。

以上是事件拦截的大致逻辑,但是细心的同学会发现,上面只回答了 CANCEL 事件到哪去,那它是从哪来的呢?被拦截的那个事件,又是谁消费的?

相信这个问题难不倒深入阅读分析事件分发源码的同学,答案如下:

① 被拦截的事件会被转换为 CANCEL 事件,即event.setAction(MotionEvent.ACTION_CANCEL),会传递给被拦截的子控件告知事件流取消,View 中的 onTouchEvent 会消费 CANCEL 事件返回 true。 ② 此后的事件流,将调用拦截控件的 dispatchTouchEvent 和 onTouchEvent。

其实这里面还有一个问题…

2.3 如果拦截了 FrameLayout 的 DOWN 事件,但是不消费,又会怎么样?再这么推演下去,没完没了了,换一题。

磨刀不耽误砍柴,画两幅时序图总结一下:▼

设 按 键 监 听 a n d 按 键 移 动 分 发 设按键监听 and 按键移动分发 设按键监听and按键移动分发

▼ ▼ ▼

(https://i-blog.csdnimg.cn/blog_migrate/e3a75b0768660ebb81077c7006a0a69c.jpeg)

磨刀不耽误砍柴,画两幅时序图总结一下:▼

设 按 键 监 听 a n d 按 键 移 动 分 发 设按键监听 and 按键移动分发 设按键监听and按键移动分发

▼ ▼ ▼

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值