百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

android事件分发,它其实是一个哲学问题

bigegpt 2024-10-12 05:14 5 浏览

写在最前

android事件分发可以说是面试中最为常见的问题,今天小盆友整理下资料,日后可用。

当一个事件被触发,事件也很郁闷啊,便开始思考“我是谁,我从哪里来,我到哪去”,程序猿此时便要作为圣人开始解开这一哲学问题。

我是谁?

我就是事件啊,还能是谁?此时若是这样回答,只是回答了表面问题,并非本质。当放荡不羁的用户在我们的APP点击一下,此时便产生了一个事件,而这个事件便会被包装成为MotionEvent,而这个类在我们的view层级间游走传递时,便有了事件分发这一说法。

我从哪里来?

事件此时更加疑惑了,我在用户点击了一下之后,怎么来到了程序猿的视图(通过setContentView设置的view)中,任其玩转。于是“圣人”便开始说到,当事件生成后,首先会到达我们最熟悉的Activity,此时会调用Activity的dispatchTouchEvent(),在Activity中可看到如下代码:

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

此时若getWindow().superDispatchTouchEvent(ev)返回false,即此时所有子View (ViewGroup也是View的子类) 都不进行拦截处理,则调用Activity的onTouchEvent(ev);返回true,即已有子View做处理。但getWindow()又是什么呢?查看Window的源码,在其类注解中可看见一行注释为:

The only existing implementation of this abstract class is android.view.PhoneWindow

即PhoneWindow为Window的唯一实现类。因此我们可以断定getWindow().superDispatchTouchEvent(ev)调用的是PhoneWindow中的superDispatchTouchEvent方法,于源码中可看到如下代码

public boolean superDispatchTouchEvent(MotionEvent event) {

return mDecor.superDispatchTouchEvent(event);

}

mDecor又是什么呢?此时通过类内搜索,搜寻锁定于installDecor方法,可看到方法中有如下代码:

if (mDecor == null) {

mDecor = generateDecor(-1); //1??

mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

mDecor.setIsRootNamespace(true);

if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {

mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);

}

} else {

mDecor.setWindow(this);

}

if (mContentParent == null) {

mContentParent = generateLayout(mDecor); //2??

...

在1??处的generateDecor方法中返回则为DecorView,DecorView即为我们视图的根View(敲黑板,划重点啦!!!);请看2??处,generateLayout方法中(代码较多,这里就不给出了,需要的话可以查看PhoneWindow类)其实主要是根据不同情况加在不同布局给layoutResource,而在其中需要特别注意的布局R.layout.screen_title,内含有“title”,"content"两个id,分别用来显示标题和内容。而大家一直通过setContentView所设置的view其实便显现在“content”(图中的contentView)中,给出一图助大家理解:

再次敲黑板!!!可以通过以下方法获取我们setContentView的视图(顺带说一句:这下大家知道为什么方法名叫setContentView,而不叫setView之类的名字了吧。)

((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);

小结:我从哪来?我从用户触发后经过Activity->Window(PhoneWindow)->DecorView

我到哪去

在“我从哪来”一节中,事件已经进入到我们的setContentView的View中;至此,事件的传递则由三个方法决定其去向:

public boolean dispatchTouchEvent(MotionEvent ev)

用于事件分发。只要事件能到达该view,则该方法一定被触发,表示是否消耗当前事件,返回结果受view的onTouchEvent和dispatchTouchEvent方法影响。

public boolean onInterceptTouchEvent(MotionEvent ev)

用于事件拦截,在dispatchTouchEvent方法中调用,view没有提供该方法,表示是否拦截当前事件,如果当前view拦截了某个事件,则同个事件序列中此方法不会被再次调用。

public boolean onTouchEvent(MotionEvent ev)

用于处理点击事件,在dispatchTouchEvent方法中调用,表示是否消耗当前事件,如果不消耗,则同个事件序列中,当前view无法再次接收到事件。

上述所说的“无再次调用”、无再次接收,稍后在“源码解析”一节会作解释,稍安勿躁。

三个方法的关系,伪代码表示为:

public boolean dispatchTouchEvent(EventMotion ev){

boolean consume = false;

if(onInterceptTouchEvent(ev)){

consume = onTouchEvent(ev);

}else{

consume = child.dispatchTouchEvent(ev);

}

return consume;

}

当事件进入ViewGroup后,最先触发dispatchTouchEvent方法,如果该ViewGroup的onInterceptTouchEvent方法返回true,则进行拦截后进入调用onTouchEvent方法;如果onInterceptTouchEvent返回false,则调用其子view的dispatchTouchEvent,进行下一轮调用,直至事件被处理。

当view进行事件处理时,并设置了onTouchListener和onClickListener,则onTouchListener中的onTouch最先被回调,如果其返回值为false,则view的onTouchEvent被调用;如果返回值为true,则onTouchEvent不会被调用,值得一提的是,设置的onClickListener会在onTouchEvent中被调用(稍后会在“源码解析”一节中解释),由此可知我们平常用的onClickListener优先级最为低,处于事件传递末端,以下给出一图,方便理解:

到此,事件分发流程就已走完,事件也知道自己要去哪了。现小结一下:事件产生后,经过顺序为Activity->Window->DecorView->我们的View,而后进入我们setContentView的View,进入dispatchTouchEvent方法(一般不会重写该方法),如果该层ViewGroup需要拦截,则在onInterceptTouchEvent中返回true,则会进入onTouchEvent方法,如果onTouchEvent方法返回true,则事件被消费,分发完毕,若返回false,则交由父级的onTouchEvent方法处理,如果其父级返回值为false,则继续向上调用,直至返回值为true或到达Activity;如果不需要拦截该事件则onInterceptTouchEvent的返回值为false,此时事件向子view传递(通过其dispatchTouchEvent),进行新的一轮调用。

为了理解,这里举一个例子,三国时期,蜀魏交战(故事虚构,只为方便理解),诸葛亮派关羽带领本部兵马出战,此时诸葛亮并没有亲自上阵(onInterceptTouchEvent返回false),关羽来到阵前,对面的阵营中出来了一个无名将领,关羽觉得不值得跟他一战(onInterceptTouchEvent返回了false),便派出了关平与之交战,此时关平手下无将领,只能亲自上阵,两回合后便将敌方将领斩于马下(onTouchEvent返回true);而后司马懿前来挑战,此时,关平自知智谋敌不过,则向关羽求救(onTouchEvent返回false),

关羽虽勇猛但智斗不过司马懿又向诸葛亮求救(onTouchEvent返回false),诸葛亮便用计将其打败(onTouchEvent返回ture)。

事件传递注意小点:

1、一个事件序列只能被一个View拦截且消耗,因为一旦拦截某个事件,则同个事件序列内的所有事件都由它处理。

2、如果一个view处理事件,但它的onTouchEvent方法返回了false,那么这一事件序列将不会再由该view来处理,而且会调用其父级view的onTouchEvent。

3、ViewGroup默认不拦截事件,即onInterceptTouchEvent返回false,

4、View(不含ViewGroup)没有onInterceptTouchEvent,事件只要传递至该View则其onTouchEvent会被调用。

5、View的onTouchEvent默认会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable默认为false,clickable则视情况而定,button则默认为true,textview默认为false。(onTouchEvent的返回值受clickable和longClickable影响,enable并不能决定其是否能消耗事件)

6、requestDisallowInterceptTouchEvent方法可干预父元素的分发过程

源码解析“我到哪去”

在ViewGroup中,我们可以在dispatchTouchEvent方法中看到如下一段代码:

// Handle an initial down.

if (actionMasked == MotionEvent.ACTION_DOWN) {

// Throw away all previous state when starting a new touch gesture.

// The framework may have dropped the up or cancel event for the previous gesture

// due to an app switch, ANR, or some other state change.

cancelAndClearTouchTargets(ev);

resetTouchState(); // 3??

}

// Check for interception.

final boolean intercepted;

if (actionMasked == MotionEvent.ACTION_DOWN

|| mFirstTouchTarget != null) { // 1??

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //2??

if (!disallowIntercept) {

intercepted = onInterceptTouchEvent(ev);

ev.setAction(action); // restore action in case it was changed

} else {

intercepted = false;

}

} else {

// There are no touch targets and this action is not an initial down

// so this view group continues to intercept touches.

intercepted = true;

}

可从1??处,当事件类型为ACTION_DOWN或者mFirstTouchTarget为非空时,则不进行拦截;当事件由ViewGroup子元素成功处理时,mFirstTouchTarget则被赋值,且指向子元素;也就是说,如果ViewGroup进行拦截,则mFirstTouchTarget为空,此条件mFirstTouchTarget != null不成立,若不进行拦截,mFirstTouchTarget被赋值为处理此事件的view,则mFirstTouchTarget != null成立。

当ViewGroup进行事件拦截时,ACTION_MOVE和ACTION_UP事件到达,由于条件 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)为false,将导致ViewGroup的onInterceptTouchEvent不会再次被调用,并且同事件序列都交由它来处理。

2??处中的FLAG_DISALLOW_INTERCEPT标志,是通过方法requestDisallowInterceptTouchEvent进行设置,当设置此标志后,ViewGroup将无法拦截除ACTION_DOWN以外的点击事件。为什么说除了ACTION_DOWN事件?因为ViewGroup在ACTION_DOWN注释3??处的方法中会对该标志进行重置,导致ViewGroup的ACTION_DOWN不受其影响。

如果ViewGroup不进行拦截,事件将会分发给其子View,在其dispatchTouchEvent中接下去的一部分代码为:

final View[] children = mChildren;

for (int i = childrenCount - 1; i >= 0; i--) { //1??

final int childIndex = getAndVerifyPreorderedIndex(

childrenCount, i, customOrder);

final View child = getAndVerifyPreorderedView(

preorderedList, children, childIndex);

// If there is a view that has accessibility focus we want it

// to get the event first and if not handled we will perform a

// normal dispatch. We may do a double iteration but this is

// safer given the timeframe.

if (childWithAccessibilityFocus != null) {

if (childWithAccessibilityFocus != child) {

continue;

}

childWithAccessibilityFocus = null;

i = childrenCount - 1;

}

if (!canViewReceivePointerEvents(child)

|| !isTransformedTouchPointInView(x, y, child, null)) {//2??

ev.setTargetAccessibilityFocus(false);

continue;

}

newTouchTarget = getTouchTarget(child);

if (newTouchTarget != null) {

// Child is already receiving touch within its bounds.

// Give it the new pointer in addition to the ones it is handling.

newTouchTarget.pointerIdBits |= idBitsToAssign;

break;

}

resetCancelNextUpFlag(child);

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//3??

// Child wants to receive touch within its bounds.

mLastTouchDownTime = ev.getDownTime();

if (preorderedList != null) {

// childIndex points into presorted list, find original index

for (int j = 0; j < childrenCount; j++) {

if (children[childIndex] == mChildren[j]) {

mLastTouchDownIndex = j;

break;

}

}

} else {

mLastTouchDownIndex = childIndex;

}

mLastTouchDownX = ev.getX();

mLastTouchDownY = ev.getY();

newTouchTarget = addTouchTarget(child, idBitsToAssign);//4??

alreadyDispatchedToNewTouchTarget = true;

break;

}

// The accessibility focus didn't handle the event, so clear

// the flag and do a normal dispatch to all children.

ev.setTargetAccessibilityFocus(false);

}

此段代码较长,但主体逻辑很清晰,首先遍历ViewGroup的所有自元素(注释1??),然后判断子元素是否能够接受到点击事件(注释2??),根据两个条件判断其是否能接收事件:(1)自元素是否在播放动画(2)点击事件坐标是否落在自元素区域内。如果满足以上两点,则传递给该子View。在注释3??处的dispatchTransformedTouchEvent方法(部分代码如下)内部其实调用的是dispatchTouchEvent方法,在注释3??的child为非null,则此处代码则调用了子view的dispatchTouchEvent,事件进入了子view,一轮事件分发则结束。

if (child == null) {

handled = super.dispatchTouchEvent(event);

} else {

handled = child.dispatchTouchEvent(event);

}

如果子view的dispatchTouchEvent返回了true(暂不考虑子view的内部事件分发),那么mFirstTouchTarget会在注释4??的addTouchTarget方法(如下代码)中被赋值后跳出for循环;但如果dispatchTouchEvent返回了false,则将事件分发给下一个子view。

/**

* Adds a touch target for specified child to the beginning of the list.

* Assumes the target child is not already present.

*/

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {

final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);

target.next = mFirstTouchTarget;

mFirstTouchTarget = target;

return target;

}

细心的猿会发现mFirstTouchTarget其实是一个单链表结构,而且其是否被赋值,直接影响ViewGroup对事件的拦截,为null时,则拦截(在本小节最开始时已有讲)

若遍历了所有子view后,事件并没有被消耗,ViewGroup会自己处理点击事件。没被消费的原因有两种:(1)ViewGroup没有子元素;(2)子元素dispatchTouchEvent返回了false,一般由onTouchEvent返回false导致。在ViewGroup的dispatchTouchEvent方法中,由以下一段代码,其中第三个参数,即View为null,则会导致上面的代码进入super.dispatchTouchEvent方法(ViewGroup继承自View),则此处会进入到View的dispatchTouchEvent,点击事件的处理便交由view来处理。

// Dispatch to touch targets.

if (mFirstTouchTarget == null) {

// No touch targets so treat this as an ordinary view.

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

}

view(不包含ViewGroup)的点击事件处理相对来说会简短些,下面的给出的是view中的dispatchTouchEvent方法的部分代码,因为view是单独的元素,没有子view一说,所以事件都由自己处理,这段代码便是对点击事件的处理过程,首先会判断是否有设置OnTouchListener(注释1??)如果OnTouchListener的onTouch返回了true,那么onTouchEvent便不会被调用(这便解释了“view处理事件流程图”中为何没调用onTouchEvent)

if (onFilterTouchEventForSecurity(event)) {

if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {

result = true;

}

//noinspection SimplifiableIfStatement

ListenerInfo li = mListenerInfo;

if (li != null && li.mOnTouchListener != null

&& (mViewFlags & ENABLED_MASK) == ENABLED

&& li.mOnTouchListener.onTouch(this, event)) { //1??

result = true;

}

if (!result && onTouchEvent(event)) {

result = true;

}

}

如果OnTouchListener的onTouch返回了false,则onTouchEvent会被调用。在该方法中,有以下一段代码可知,当view处于不可用状态时,仍然会消耗点击事件,尽管看不起来该view不能用。

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE

|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)

|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {

if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {

setPressed(false);

}

mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

// A disabled view that is clickable still consumes the touch

// events, it just doesn't respond to them.

return clickable;

}

如果view设置了代理,那么还会执行TouchDelegate的onTouchEvent方法

if (mTouchDelegate != null) {

if (mTouchDelegate.onTouchEvent(event)) {

return true;

}

}

接下来看onTouchEvent中针对点击事件的具体操作,看以下代码,只要clickable为true(只要LONG_CLICKABLE和CLICKABLE有一个为true)则会消耗该事件,并返回true,不管其是否为disable。

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { //

switch (action) {

case MotionEvent.ACTION_UP:

mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

if ((viewFlags & TOOLTIP) == TOOLTIP) {

handleTooltipUp();

}

if (!clickable) {

removeTapCallback();

removeLongPressCallback();

mInContextButtonPress = false;

mHasPerformedLongPress = false;

mIgnoreNextUpEvent = false;

break;

}

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(); //1??

}

}

}

...

break;

}

return true;

}

当ACTION_UP事件触发时,此时会调用performClick方法(注释1??),performClick的具体代码如下,如果view设置了OnClickListener(注释1??),那么performClick方法会在注释3??处调用onClick。

public boolean performClick() {

final boolean result;

final ListenerInfo li = mListenerInfo;

if (li != null && li.mOnClickListener != null) { //2??

playSoundEffect(SoundEffectConstants.CLICK);

li.mOnClickListener.onClick(this); //3??

result = true;

} else {

result = false;

}

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

notifyEnterOrExitForAutoFillIfNeeded(true);

return result;

}

值得一提的是,view的LONG_CLICKABLE属性默认为false,而CLICKABLE属性则有具体的view控件决定,但可以说,可点击的view默认都为true(例如button),不可点击的都为false(例如textview),但当我们通过setOnLongClickListener或setOnClickListener设置对应的listener时,LONG_CLICKABLE或CLICKABLE会被设置为true,从以下源码可知。

public void setOnLongClickListener(@Nullable OnLongClickListener l) {

if (!isLongClickable()) {

setLongClickable(true); //此处更改

}

getListenerInfo().mOnLongClickListener = l;

}

public void setOnClickListener(@Nullable OnClickListener l) {

if (!isClickable()) {

setClickable(true); //此处更改

}

getListenerInfo().mOnClickListener = l;

}

至此,安卓事件分发这一哲学问题圆满解决,撒花。

相关推荐

得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...

warm-flow新春版:网关直连和流程图重构

本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...

扣子空间体验报告

在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...

spider-flow:开源的可视化方式定义爬虫方案

spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...

solon-flow 你好世界!

solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...

新一代开源爬虫平台:SpiderFlow

SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...

通过 SQL 训练机器学习模型的引擎

关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...

鼠须管输入法rime for Mac

鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...

Go语言 1.20 版本正式发布:新版详细介绍

Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...

iOS 10平台SpriteKit新特性之Tile Maps(上)

简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...

程序员简历例句—范例Java、Python、C++模板

个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...

Telerik UI for iOS Q3 2015正式发布

近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...

ios使用ijkplayer+nginx进行视频直播

上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...

IOS技术分享|iOS快速生成开发文档(一)

前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...

macOS下配置VS Code C++开发环境

本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...