「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
先赞后看,养成习惯~
对面的面试官喝了口水,不动声色地继续看我的简历,让自我感觉上个问题【面试官爸爸】来给我讲讲 View 绘制?回答的不错沾沾自喜的我慢慢收敛起来,正襟危坐,等待下一轮提问
准备好了没有啊?
准....准备好了
行,那我们开始吧
你来说说 View 的事件分发吧
我看你 View 绘制答的还可以,你来说说 View 的事件分发吧
好的。View 事件分发一般是指 UI 事件的分发。它本质是递归,递归函数是 dispatchTouchEvent
事件分发如何通过递归实现
递归?那你讲讲事件分发如何通过递归实现的
递归分为递过程和归过程。
当 UI 事件来临时,它首先被分发到 activity,activity 分发给 window,window 再分发给 view 树。分发过程 通过 dispatchTouchEvent 函数完成。它的返回值是 Boolean,如果为 true,表示事件被消费;为 false,表示事件未被消费。 dispatchTouchEvent 在不同的分发阶段有不同的作用
对于 View 而言,它没有子 View,View 自己的 dispatchTouchEvent 实际上会执行到 onTouchEvent 来进行事件的消费
对于 ViewGroup 而言,它重写了 dispatchTouchEvent, 先将事件分发给自己的子 View。
如果子 View 可以消费事件, dispatchTouchEvent 直接返回 true;如果子 View 都不消费事件,就调用自己的 onTouchEvent 来进行事件的消费。(ViewGroup 的 onTouchEvent 实际上就是 super.dispatchTouchEvent)
这样,Activity 通过调用 Window#superDispatchTouchEvent 方法将事件分发给 Window,Window 调用 DecorView#dispatchTouchEvent 将事件分发给 decorView。DecorView 是一个 ViewGroup,它又将事件分发给子 ViewGroup 和子 View。当有一个 View 消费掉事件时,他就会向上返回,通过递归链返回到 Activity#dispatchTouchEvent。最终完成事件的分发,消费和返回。
每一个事件的分发都需要递归吗?
那我问你,每一个事件的分发都需要递归吗?用户一次操作会产生大量的 UI 事件,频繁的递归遍历不会对性能有影响吗?
当然会有!所以 Android 为了避免每个事件都递归遍历,定义了一个 【事件序列】 的概念:将用户每一次触摸屏幕 --> 移动屏幕-->抬起手指称为一个事件序列。
一个事件序列必然包含 ACTION_DOWN,ACTION_MOVE,ACTION_UP 等多个事件。其中 ACTION_MOVE 数量不确定,ACTION_DOWN 和 ACTION_UP 数量则为 1
当接收到 ACTION_DOMN 事件时,意味着一次完成事件序列的开始。ViewGroup 会通过递归遍历找到 View 树中真正对事件进行消费的子 View,并将其保存。这之后接收到 ACTION_MOVE 和 ACTION_DOWN 事件时,则跳过递归遍历的过程,直接交给之前保存的消费者。当下一次 ACTION_DOWN 事件来临时重置整个过程
如何在事件被分发前拦截
嗯,了解的还挺仔细的嘛。那如果有一个 view 可以消费事件,但我想在事件分发给它之前进行拦截,该怎么做?
ViewGroup 提供了一个拦截事件的函数onInterceptTouchEvent,返回值为 Boolean。 表示是否拦截事件。如果拦截,则会调用 onTouchEvent 进行事件的进一步处理。
如何处理滑动冲突
可以,我看你理论掌握的差不多了,平常有遇到滑动冲突的问题吗?你是怎么处理滑动冲突的呢?
当父 View 和子 View 都有机会消费事件,但消费的时机不符合业务的需要(比如需要子 View 消费事件但父 View 先消费了),就会产生滑动冲突问题
解决办法一般分为内部拦截法和外部拦截法。
内部拦截法就是通过重写底层 View 的 dispatchTouchEvent 和 onTouchEvent 方法来决定是否消费事件。
外部拦截法就是重写 ViewGroup 的 dispatchTouchEvent 和 onInterceptTouchEvent 方法决定是否把事件分发给 View。
两种方法实际上就是对分发事件的 View 和被分发事件的 View 做不同的逻辑判断。
讲讲ACTION_CANCEL事件?
好的,看来平时没少解决滑动冲突的问题哈。刚才你提到了事件序列对吧,你说说 ACTION_CANCEL 事件是用来干嘛的?
前面我们提到在一个事件序列中,如果有 View 能够消费事件,那么该事件序列所有的后续事件都会交给这个 View 处理。但如果不希望它处理全部的后续事件怎么办?比如手指点击一个 Button 然后滑出它的边界。在这个事件序列中,我只希望 Button 处理它边界内的 move 事件。对于边界外的 move 事件,虽然它们都在一个事件序列中,但理论上不应该再传递给 Button 了。
可以看到,当我点击Button不动滑动到view之外时,Button实际上已经不响应事件了。这是怎么实现的呢?
ACTION_CANCEL 就是用来解决这个问题的。当 Button 判断 move 事件已经超出 view 的边界时,会将自己的 mPrivateFlags 置为 cancel 状态。等下次事件分发来临,Button 的父 ViewGroup 会检测 Button 的 mPrivateFlags,如果为 cancel 则将之前保存的 mFirstTouchTarget(也就是指向 Button 的引用) 设为 null,并向 Button 发送一个 ACTION_CANCEL 事件,表示以后不会再接受事件了。
不错不错,看来基础的 View 绘制和事件分发已经难不倒你了,下面我们来聊聊 Handler 相关知识吧~
上一篇【面试官爸爸】来给我讲讲 View 绘制? 反响不错!非常感谢大家的喜欢,同时也很感谢兄弟萌给我提出的意见
这个系列既是对基础知识的梳理,同时也结合了自己面试过程中遇到过的各种问题。其实很多问题是面试必考的,用最简单精炼的语言回答它们,向面试官展现自己的思考,而不单单只是背八股文,这样能极大提高获得 offer 的成功率
后续我还会继续更新 【面试官爸爸】 这个系列,包括最常考的消息机制,Handler,Activity 启动流程,编译打包优化,Context 等等面试常考的问题。如果不想错过,欢迎点赞,收藏,关注我!球球兄弟萌辣,这个对我真的很重要!!
点赞过 20,7 天之内更消息机制!!
我是方木
一个在互联网世界挣扎向上的打工人
努力生活,努力向前
微信搜公众号 方木 Rudy 第一时间获取我的更新!里面不仅有技术,还有故事和感悟~
搜索它,带走我 (#^.^#)