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

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

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

写在最前

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;

}

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

相关推荐

最全的MySQL总结,助你向阿里“开炮”(面试题+笔记+思维图)

前言作为一名编程人员,对MySQL一定不会陌生,尤其是互联网行业,对MySQL的使用是比较多的。对于求职者来说,MySQL又是面试中一定会问到的重点,很多人拥有大厂梦,却因为MySQL败下阵来。实际上...

Redis数据库从入门到精通(redis数据库设计)

目录一、常见的非关系型数据库NOSQL分类二、了解Redis三、Redis的单节点安装教程四、Redis的常用命令1、Help帮助命令2、SET命令3、过期命令4、查找键命令5、操作键命令6、GET命...

netcore 急速接入第三方登录,不看后悔

新年新气象,趁着新年的喜庆,肝了十来天,终于发了第一版,希望大家喜欢。如果有不喜欢看文字的童鞋,可以直接看下面的地址体验一下:https://oauthlogin.net/前言此次带来得这个小项目是...

精选 30 个 C++ 面试题(含解析)(c++面试题和答案汇总)

大家好,我是柠檬哥,专注编程知识分享。欢迎关注@程序员柠檬橙,编程路上不迷路,私信发送以下关键字获取编程资源:发送1024打包下载10个G编程资源学习资料发送001获取阿里大神LeetCode...

Oracle 12c系列(一)|多租户容器数据库

作者杨禹航出品沃趣技术Oracle12.1发布至今已有多年,但国内Oracle12C的用户并不多,随着12.2在去年的发布,选择安装Oracle12c的客户量明显增加,在接下来的几年中,Or...

flutter系列之:UI layout简介(flutter-ui-nice)

简介对于一个前端框架来说,除了各个组件之外,最重要的就是将这些组件进行连接的布局了。布局的英文名叫做layout,就是用来描述如何将组件进行摆放的一个约束。在flutter中,基本上所有的对象都是wi...

Flutter 分页功能表格控件(flutter 列表)

老孟导读:前2天有读者问到是否有带分页功能的表格控件,今天分页功能的表格控件详细解析来来。PaginatedDataTablePaginatedDataTable是一个带分页功能的DataTable,...

Flutter | 使用BottomNavigationBar快速构建底部导航

平时我们在使用app时经常会看到底部导航栏,而在flutter中它的实现也较为简单.需要用到的组件:BottomNavigationBar导航栏的主体BottomNavigationBarI...

Android中的数据库和本地存储在Flutter中是怎样实现的

如何使用SharedPreferences?在Android中,你可以使用SharedPreferencesAPI来存储少量的键值对。在Flutter中,使用Shared_Pref...

Flet,一个Flutter应用的实用Python库!

▼Flet:用Python轻松构建跨平台应用!在纷繁复杂的Python框架中,Flet宛如一缕清风,为开发者带来极致的跨平台应用开发体验。它用最简单的Python代码,帮你实现移动端、桌面端...

flutter系列之:做一个图像滤镜(flutter photo)

简介很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起...

flutter软件开发笔记20-flutter web开发

flutterweb开发优势比较多,采用统一的语言,就能开发不同类型的软件,在web开发中,特别是后台式软件中,相比传统的html5开发,更高效,有点像c++编程的方式,把web设计出来了。一...

Flutter实战-请求封装(五)之设置抓包Proxy

用了两年的flutter,有了一些心得,不虚头巴脑,只求实战有用,以供学习或使用flutter的小伙伴参考,学习尚浅,如有不正确的地方还望各路大神指正,以免误人子弟,在此拜谢~(原创不易,转发请标注来...

为什么不在 Flutter 中使用全局变量来管理状态

我相信没有人用全局变量来管理Flutter应用程序的状态。毫无疑问,我们的Flutter应用程序需要状态管理包或Flutter的基本小部件(例如InheritedWidget或St...

Flutter 攻略(Dart基本数据类型,变量 整理 2)

代码运行从main方法开始voidmain(){print("hellodart");}变量与常量var声明变量未初始化变量为nullvarc;//未初始化print(c)...