聊到事件分发,很多朋友就会想到vIEw的dispatchtouchEvent
,其实在此之前,AndroID还做了很多工作。
比如跨进程获取输入事件的方式?在dispatchtouchEvent
责任链之前还有一条inputStage
责任链?DecorVIEw,PhoneWindow
之间的传递顺序?
另外还包括事件分发过程中事件序列的处理方式?VIEwGroup和VIEw之间的协调?mFirsttouchTarget
真假链表?等等。
这一切,都要从你可爱的小拇指
说起...
当你的拇指触碰手机的那一刹那,手机就被你深深的影响了,没错,手机会收到你给他布置的任务。
这个任务可以是:
滑动界面任务点击按钮任务长按任务等等,总之,你向手机传递了这个任务信息,接下来就是手机的处理任务时间。
我们可以假设手机系统就是一个大的公司(AndroID公司)
,而我们触摸手机的任务就是一个完整的项目需求,今天就和大家一起深入AndroID公司内部,打探事件分发的那些秘密。
在此之前,我也列出了问题和大纲:
硬件部门和内核部门首先,我的拇指找到了AndroID
公司,说出了自己的需求,比如:点击某个VIEw并滑动到另外的位置。
AndroID
公司会派出硬件部门,和我的小拇指进行会谈,接收到我的需求之后,硬件部门生成简单的终端,并传递给内核部门。
内核部门将任务进行加工,生成了内部事件——event,并添加到公司内部的一个管理系统/dev/input/
目录下。
这样做的目的是把外来的需求转化成内部通用,都能看懂的任务。
任务处理部门(SystemServer进程)当任务记录在公司管理系统上,就会有专门的任务处理部门对这些任务进行处理,他们做的事情就是一直监听/dev/input/
目录,当发现有新的事件就会进行处理。
那这个任务处理部门到底是何方神圣呢?
不知道大家还记不记得在SystemServer
进程中启动了一系列系统有关的服务,比如AMS,PMS等等,其中还有一个不是很起眼的角色,叫做inputManagerService
。
这个服务就是用来负责与硬件通信,接受屏幕输入事件。
在其内部,会启动一个读线程,也就是inputReader
,它会从这个管理系统也就是/dev/input/
目录拿到任务,并且分发给inputdispatcher
线程,然后进行统一的事件分发调度。
然后任务处理部门需要把任务交给 专业处理任务的项目组了,这就涉及到跨部门沟通了(跨进程通信)。
大家都知道跨部门沟通是个比较麻烦的事情,谁来完成这个事情呢?inputChannel
。
让我们回到VIEwRootImpl
的setVIEw
方法:
public voID setVIEw(VIEw vIEw, WindowManager.LayoutParams attrs, VIEw panelParentVIEw) { synchronized (this) { //创建inputChannel minputChannel = new inputChannel(); //通过Binder进入systemserver进程 res = mwindowsession.addTodisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mdisplay.getdisplayID(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, minputChannel); }}
在该方法中,创建了一个inputChannel
对象,并且通过Binder进入systemserver进程,最终形成socket的客户端。
这里涉及到socket通信的知识,比较重要的就是c层的socketpair
方法。
socketpair()函数用于创建一对无名的、相互连接的套接子。如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。
通过这个方法,就生成了socket通信的客户端和服务端:
socket服务端
保存到system_server中的windowstate的minputChannel;socket客户端
通过binder传回到远程进程的UI主线程VIEwRootImpl的minputChannel;感兴趣的可以看看gityuan
对于input分析的博客,文末有链接。
所以小结一下就是,在App进程创建了一个对象inputChannel
,通过Binder机制传入了SystemServer
进程,也就是WindowManagerService
中。然后在WindowManagerService
中创建了一对套接字用于进程间通信,而传过来的inputChannel
就指向了socket
的客户端。
然后App进程的主线程就会监听这个socket客户端,当收到消息(输出事件)后,回调NativeinputEventReceiver.handleEvent()
方法,最终会走到inputEventReceiver.dispachinputEvent
方法。
dispachinputEvent
,处理输入事件,感觉离我们熟知的事件分发比较近了。
没错,到此,任务已经分配到了具体的项目组,也就是我们所使用的具体APP中了。
小组中任务第一次分发(inputStage)当任务到达了项目组,首先组内会对这个任务进行分发,这里会涉及到第一次责任链分发模式
。
为什么强调是第一次呢?因为还没有到达我们熟知的vIEw事件分发阶段,在此之前,还会有一次事件分类的责任链分发工作,也就是inputStage
处理事件分发。
//inputEventReceiver.javaprivate voID dispatchinputEvent(int seq, inputEvent event) { mSeqMap.put(event.getSequenceNumber(), seq); oninputEvent(event); }//VIEwRootImpl.java ::WindowinputEventReceiverfinal class WindowinputEventReceiver extends inputEventReceiver { public voID oninputEvent(inputEvent event) { enqueueinputEvent(event, this, 0, true); }}//VIEwRootImpl.javavoID enqueueinputEvent(inputEvent event, inputEventReceiver receiver, int flags, boolean processImmediately) { adjustinputEventForCompatibility(event); QueuedinputEvent q = obtainQueuedinputEvent(event, receiver, flags); QueuedinputEvent last = mPendinginputEventTail; if (last == null) { mPendinginputEventhead = q; mPendinginputEventTail = q; } else { last.mNext = q; mPendinginputEventTail = q; } mPendinginputEventCount += 1; if (processImmediately) { doProcessinputEvents(); } else { scheduleProcessinputEvents(); }}
兜兜转转,没想到还是到了VIEwRootImpl这里,所以VIEwRootImpl
不仅负责了界面的绘制,也负责了事件分发的部分处理工作。
这里的enqueueinputEvent
方法中,有涉及到一个QueuedinputEvent
类,这个类就是一个封装了inputEvent的事件类,然后经过赋值调用到doProcessinputEvents
方法:
voID doProcessinputEvents() { // Deliver all pending input events in the queue. while (mPendinginputEventhead != null) { QueuedinputEvent q = mPendinginputEventhead; mPendinginputEventhead = q.mNext; deliverinputEvent(q); } } private voID deliverinputEvent(QueuedinputEvent q) { inputStage stage; if (stage != null) { stage.deliver(q); } else { finishinputEvent(q); } } abstract class inputStage { private final inputStage mNext; public inputStage(inputStage next) { mNext = next; } public final voID deliver(QueuedinputEvent q) { apply(q, onProcess(q)); }
到这里逻辑好像慢慢清晰了,QueuedinputEvent
是一种输入事件,inputStage
是处理输入事件的责任链,next字段则表示责任链的下一个inputStage
。
那inputStage
到底干了哪些事情呢?返回到VIEwRootImpl
的setVIEw方法再看看:
public voID setVIEw(VIEw vIEw, WindowManager.LayoutParams attrs, VIEw panelParentVIEw) { synchronized (this) { // Set up the input pipeline. mSyntheticinputStage = new SyntheticinputStage(); inputStage vIEwPostImeStage = new VIEwPostImeinputStage(mSyntheticinputStage); inputStage nativePostImeStage = new NativePostImeinputStage(vIEwPostImeStage, "aq:native-post-ime:" + counterSuffix); inputStage earlyPostImeStage = new EarlyPostImeinputStage(nativePostImeStage); inputStage imeStage = new ImeinputStage(earlyPostImeStage, "aq:ime:" + counterSuffix); inputStage vIEwPreImeStage = new VIEwPreImeinputStage(imeStage); inputStage nativePreImeStage = new NativePreImeinputStage(vIEwPreImeStage, "aq:native-pre-ime:" + counterSuffix); mFirstinputStage = nativePreImeStage; mFirstPostImeinputStage = earlyPostImeStage; } }
可以看到在setVIEw方法中,就把这条输入事件处理的责任链拼接完成了,不同的inputStage子类,通过构造方法一个个串联起来了,那这些inputStage到底干了啥呢?
SyntheticinputStage
。综合处理事件阶段,比如处理导航面板、 *** 作杆等事件。VIEwPostImeinputStage
。视图输入处理阶段,比如按键、手指触摸等运动事件,我们熟知的vIEw事件分发就发生在这个阶段。NativePostImeinputStage
。本地方法处理阶段,主要构建了可延迟的队列。EarlyPostImeinputStage
。输入法早期处理阶段。ImeinputStage
。输入法事件处理阶段,处理输入法字符。VIEwPreImeinputStage
。视图预处理输入法事件阶段,调用视图vIEw的dispatchKeyEventPreIme方法。NativePreImeinputStage
。本地方法预处理输入法事件阶段。小结一下,事件到达应用端的主线程,会通过VIEwRootImpl进行一系列inputStage来处理事件。这个阶段其实是对事件进行一些简单的分类处理,比如视图输入事件,输入法事件,导航面板事件等等。
事件分发完成后,会告知SystemServer
进程的inputdispatcher
线程,最终将该事件移除,完成此次事件的分发消费。
我们的vIEw手指触摸事件就是发生在VIEwPostImeinputStage
阶段了,具体来看看:
final class VIEwPostImeinputStage extends inputStage { @OverrIDe protected int onProcess(QueuedinputEvent q) { if (q.mEvent instanceof KeyEvent) { return processKeyEvent(q); } else { final int source = q.mEvent.getSource(); if ((source & inputDevice.soURCE_CLASS_POINTER) != 0) { return processpointerEvent(q); } } } private int processpointerEvent(QueuedinputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; boolean handled = mVIEw.dispatchPointerEvent(event) return handled ? FINISH_HANDLED : FORWARD; }//VIEw.java public final boolean dispatchPointerEvent(MotionEvent event) { if (event.istouchEvent()) { return dispatchtouchEvent(event); } else { return dispatchGenericMotionEvent(event); } }
经过一系列分发,最终会执行到mVIEw的dispatchtouchEvent
方法,而这个mVIEw就是DecorVIEw,同样是在setVIEw中进行赋值的,就不细说了。
至此,终于到了我们熟悉的环节,dispatchtouchEvent
方法。
确定了任务的分类,接下来就开始组内任务讨论整理了,这个阶段发生在几个大佬之间的谈话,这几个大佬分别是DecorVIEw、PhoneWindow、Activity/Dialog
:
//DecorVIEw.java @OverrIDe public boolean dispatchtouchEvent(MotionEvent ev) { //cb其实就是对应的Activity/Dialog final Window.Callback cb = mWindow.getCallback(); return cb != null && !mWindow.isDestroyed() && mFeatureID < 0 ? cb.dispatchtouchEvent(ev) : super.dispatchtouchEvent(ev); }//Activity.java public boolean dispatchtouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getwindow().superdispatchtouchEvent(ev)) { return true; } return ontouchEvent(ev); }//PhoneWindow.java @OverrIDe public boolean superdispatchtouchEvent(MotionEvent event) { return mDecor.superdispatchtouchEvent(event); }//DecorVIEw.java public boolean superdispatchtouchEvent(MotionEvent event) { return super.dispatchtouchEvent(event); }
可以看到,从DecorVIEw开始,事件依次经过了Activity、PhoneWindow、DecorVIEw
。
有点奇怪哈,为啥是这样一个顺序呢?而不是直接VIEwRootImpl交给Activity,再交给顶层VIEw——DecorVIEw?而是转来转去,缘起和从呢?
首先,为什么VIEwRootImpl不直接把事件交给Activity?因为界面上不止Activity
一种形态呀,如果界面上存在Dialog
,而Dialog的Window属于子Window,是可以覆盖应用级Window的,所以总不能把事件直接交给Activity吧?都被覆盖了,所以这时候应该把事件交给Dialog。
为了方便,我们用到了DecorVIEw
这个角色来充当分发的第一元素,由他来找到当前界面window的所持着,所以代码中也是找到mWindow.getCallback()
,其实也就是对应的Activity或者Dialog。
因为Activity
和DecorVIEw
之间并没有直接关系。DecorVIEw怎么来的?通过setContentVIEw被创建出来的,所以在Activity中是看不到DecorVIEw身影的,DecorVIEw的实例保存在PhoneWindow中,由Window所管理。
所以Activity
的事件肯定是交给Window来管理,之前也说过PhoneWindow的指责就是帮助Activity管理VIEw,所以事件分发交给它也是它的职责所在。而PhoneWindow
的处理方式,就是交给顶层的DecorVIEw
来处理了。
这样,一个事件分发的链条就形成了:
DecorVIEw——>Activity——>PhoneWindow——>DecorVIEw——>VIEwGroup
交给做任务具体的人(VIEwGroup)接下来就开始分派任务了,也就是VIEwGroup
的事件分发时间,这部分内容是老生常谈了,最重要的就是这个dispatchtouchEvent
方法。
假设我们没有看过源码,那么事件来了,会产生多种传递拦截的可能,我画了个脑图:
其中产生的疑问就包括:
VIEwGroup
是否拦截事件,拦截后怎么处理?不拦截后交给子VIEw
或者子VIEwGroup
怎么处理?子VIEw
怎么决定是否拦截?子VIEw
拦截后怎么处理事件?子VIEw
不拦截事件后父元素VIEwGroup
怎么处理事件?VIEwGroup
不拦截,子VIEw
也不拦截,最终事件怎么处理?接下来就具体分析分析。
VIEwGroup是否拦截事件,拦截后怎么处理? @OverrIDe public boolean dispatchtouchEvent(MotionEvent ev) { //1 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN|| mFirsttouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_disALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onIntercepttouchEvent(ev); } } //2 if (!canceled && !intercepted) { //事件传递给子vIEw } //3 if (mFirsttouchTarget == null) { handled = dispatchtransformedtouchEvent(ev, canceled, null, touchTarget.ALL_POINTER_IDS); } } private boolean dispatchtransformedtouchEvent(VIEw child) { if (child == null) { handled = super.dispatchtouchEvent(event); } else { handled = child.dispatchtouchEvent(event); } }
上述代码分成了三部分,分为VIEwGroup
是否拦截、拦截后则不再传递下去,VIEwGroup
拦截后的处理。
1、VIEwGroup是否拦截
可以看到,初始化了一个变量intercepted
,代表vIEwGroup
是否拦截。
如果满足两个条件任意一个,才去讨论VIEwGroup是否拦截:
事件为ACTION_DOWN
,也就是按下事件。mFirsttouchTarget
不为null其中mFirsttouchTarget
是个链表结构,代表某个子元素成功消费了该事件,所以mFirsttouchTarget为null就代表没有子vIEw消费事件,这个待会再细谈。
当第一次进入这个方法,事件肯定就是ACTION_DOWN
,所以就进入了if语句,这时候获取了一个叫做disallowIntercept(不允许拦截)的变量,暂且按下不表,接着看。
然后给这个intercepted赋值为onIntercepttouchEvent
方法的结果,我们可以理解为 vIEwGroup是否拦截取决于onIntercepttouchEvent方法。
2、拦截后则不再传递
如果vIEwGroup拦截了,也就是intercepted
为true,自然也就不需要再往子vIEw或者子VIEwGroup进行传递了。
3、VIEwGroup拦截后的处理
如果mFirsttouchTarget
为null,则表示没有子VIEw进行拦截,然后就转向执行dispatchtransformedtouchEvent
方法,代表VIEwGroup要自己再进行一次分发处理。
这里有个问题就是为什么不直接判断intercepted
呢?非要去判断这个mFirsttouchTarget
?
mFirsttouchTarget==null
不仅代表VIEwGroup要自己消费事件,也代表了VIEwGroup
没消费并且子VIEw
也没有去消费事件,两种情况都会执行到这里。也就是VIEwGroup
拦截或子VIEw没有拦截,都会调用到dispatchtransformedtouchEvent
方法,在该方法中,最后会调用super.dispatchtouchEvent
。
super代表VIEwGroup的父类VIEw,也就是VIEwGroup会作为一个普通VIEw执行VIEw.dispatchtouchEvent
方法,至于这个方法具体做了什么,待会和VIEw的事件处理再一起看。
通过上面的分析,我们可以得出VIEwGroup
拦截的伪代码:
public boolean dispatchtouchEvent(MotionEvent event) { boolean isConsume = false; if (isVIEwGroup) { if (onIntercepttouchEvent(event)) { isConsume = super.dispatchtouchEvent(event); } } return isConsume;}
如果是VIEwGroup,会先执行到onIntercepttouchEvent
方法判断是否拦截,如果拦截,则执行父类VIEw的dispatchtouchEvent
方法。
接着说VIEwGroup
不拦截的情况,也就会传到子VIEw的情况:
if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int childrenCount = mChildrenCount; //1 if (newtouchTarget == null && childrenCount != 0) { for (int i = childrenCount - 1; i >= 0; i--) { final int childindex = getAndVerifyPreorderedindex( childrenCount, i, customOrder); final VIEw child = getAndVerifyPreorderedVIEw( preorderedList, children, childindex); //2 if (!child.canReceivePointerEvents() || !istransformedtouchPointInVIEw(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //3 if (dispatchtransformedtouchEvent(ev, false, child, IDBitsToAssign)) { newtouchTarget = addtouchTarget(child, IDBitsToAssign); alreadydispatchedToNewtouchTarget = true; break; } } } } }
VIEwGroup
不拦截,则intercepted
为false,那么就会进入上述的if语句中。
同样分为三部分来说,分别是遍历子VIEw,判断事件坐标,传递事件
1、遍历子VIEw
第一部分就是遍历当前VIEwGroup所有的子VIEw。
2、判断事件坐标
然后会判断这个事件是否在当前子VIEw的坐标内,如果用户触摸的地方都不是当前的VIEw自然不需要对这个vIEw在进行分发处理,还有个条件就是当前VIEw没有在动画状态。
3、传递事件
如果事件坐标在这个VIEw内,就开始传递事件,调用dispatchtransformedtouchEvent方法,如果为true,就调用addtouchTarget方法记录事件消费链。
dispatchtransformedtouchEvent方法是不是有点熟悉?没错,刚才也出现过,再看一遍:
private boolean dispatchtransformedtouchEvent(VIEw child) { if (child == null) { handled = super.dispatchtouchEvent(event); } else { handled = child.dispatchtouchEvent(event); } }
这里对传进来的 child
进行了判断,这个child
就是子VIEw
,如果子VIEw不为null,就调用这个子VIEw的dispatchtouchEvent
方法,继续分发事件。如果为null,就是刚才的情况,调用父类的dispatchtouchEvent
方法,默认为自己来消费事件。
当然,这个child有可能为vIEwGroup有可能为VIEw,总之就是继续分发调用子VIEw
或者子VIEwGroup
的方法。
到此,一个关于dispatchtouchEvent
的递归就显现出来了:
如果某个VIEwGroup无法消费事件,那么就会传递给子vIEw/子VIEwGroup的dispatchtouchEvent方法,如果是VIEwGroup,那么又会重复这个 *** 作,直到某个VIEw/VIEwGroup消费事件。
最后,如果dispatchtransformedtouchEvent
方法返回true,就代表有子vIEw消费了事件,然后会调用到addtouchTarget
方法:
在该方法中,会对mFirsttouchTarget
这个单链表进行了赋值,记录消费链(但是在单点触控的情况下,其单链表的结构并没有用上,只是作为一个普通的touchTarget对象,待会会说到),然后就break退出了循环。
接下来就看看关于VIEw内部具体处理事件的逻辑。
子VIEw怎么处理事件,是否拦截?public boolean dispatchtouchEvent(MotionEvent event) { if (onFiltertouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; if (li != null && li.mOntouchListener != null && (mVIEwFlags & ENABLED_MASK) == ENABLED && li.mOntouchListener.ontouch(this, event)) { result = true; } if (!result && ontouchEvent(event)) { result = true; } } return result; }
其实就是两个逻辑:
1、如果VIEw设置了setontouchListener
并且ontouch
方法返回true,那么ontouchEvent
就不会被执行。2、否则,执行ontouchEvent
方法。所以默认情况下是直接会执行ontouchEvent
方法。
关于VIEw的事件分发我们也可以写一段伪代码,并且增加了setonClickListener
方法的调用:
public voID consumeEvent(MotionEvent event) { if (!setontouchListener || !ontouch) { ontouchEvent(event); } if (setonClickListener) { onClick(); }}
子VIEw拦截后怎么处理事件?子VIEw拦截后,就会给单链表mFirsttouchTarget
赋值。
这个刚才已经说过了。逻辑就在addtouchTarget方法中,我们来具体看看:
private touchTarget addtouchTarget(@NonNull VIEw child, int pointerIDBits) { final touchTarget target = touchTarget.obtain(child, pointerIDBits); target.next = mFirsttouchTarget; mFirsttouchTarget = target; return target; } public static touchTarget obtain(@NonNull VIEw child, int pointerIDBits) { final touchTarget target; target.child = child; return target; }
这个单链表到底怎么连的呢?之前我们说过dispatchtouchEvent
是一个递归的过程,当某个子VIEw消费了事件,那么通过addtouchTarget
方法,就会让mFirsttouchTarget
的child值指向那个子VIEw,依此向上,最后就会拼接成一个类似单链表结构,尾节点就是消费的那个VIEw。
为什么说类似呢?因为mFirsttouchTarget
并没有真正连起来,而是通过每个VIEwGroup的mFirsttouchTarget
间接连起来。
打个比方,我们假设一个VIEw树关系:
A / \ B C / \ D E
A、B、C为VIEwGroup,D、E为VIEw。
当我们触摸的点在VIEwD中,事件分发的顺序就是A-C—D
。
在C遍历D的时候,VIEwD消费了事件,所以走到了addtouchTarget方法中,包装了一个包含VIEwD的touchTarget
,我们叫它TargetD。
然后设置C的mFirsttouchTarget
为TargetD,也就是其child值为VIEwD。
再返回上一层,也就是A层,因为D消费了事件,所以C的dispatchtouchEvent
方法也返回了true,同样调用了addtouchTarget
方法,包装了一个TargetC。
然后会设置A的mFirsttouchTarget
为TargetC,也就是其child值为VIEwC。
最终的分发结构就是:
A.mFirsttouchTarget.child -> C
C.mFirsttouchTarget.child -> D
所以说mFirsttouchTarget
通过child找到了消费链的下一层VIEw,然后下一层又继续通过child找到下下层VIEw,依次往下,就记录了消费的完整路径。
那mFirsttouchTarget
的链表结构用到哪了呢?多点触控。
对于多点触控且点击目标不同的情况,mFirsttouchTarget
才会作为链表结构存在,next指向上一个手指按下时创建的touchTarget对象。
而在单点触控情况下,mFirsttouchTarget
链表会蜕变成单个touchTarget
对象:
mFirsttouchTarget.next
始终为null。mFirsttouchTarget.child
赋值为这条消费链的下一层VIEw,一层层递归调用每一层的mFirsttouchTarget.child,直到消费的那个vIEw。最后再补充一点,每次ACTION_DOWN事件来到的时候,mFirsttouchTarget就会被重置,迎接新的一轮事件序列。
子VIEw不拦截事件后VIEwGroup怎么处理事件?子VIEw不拦截事件,那么mFirsttouchTarget
就为null,退出循环后,调用了dispatchtransformedtouchEvent
方法。
//3 if (mFirsttouchTarget == null) { handled = dispatchtransformedtouchEvent(ev, canceled, null, touchTarget.ALL_POINTER_IDS); }
最终调用了super.dispatchtouchEvent
,也就是VIEw.dispatchtouchEvent
方法。
可以看到子VIEw
不拦截事件和VIEwGroup
拦截事件的处理是一样的都会走到这个方法中。
那么这个方法到底干了什么呢?上面说到VIEw的处理方法dispatchtouchEvent
已经说过了,还是那段伪代码,只不过在这里VIEw是作为VIEwGroup的父类。
所以,小结一下,如果所有子VIEw
都不处理事件,那么:
VIEwGroup
的ontouchEvent
方法。如果设置VIEwGroup
的setontouchListener
,就会执行ontouch
方法。VIEwGroup不拦截,子VIEw也不拦截,最终事件怎么处理?最后一点,如果VIEwGroup
不拦截,子VIEw
也不拦截,这个意思就是mFirsttouchTarget == null
的同时,dispatchtransformedtouchEvent
方法也返回false。
总之,就是所有VIEwGroup的dispatchtouchEvent
方法都返回false,这时候该怎么处理呢?返回到一开始大佬会谈的时候:
//Activity.java public boolean dispatchtouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getwindow().superdispatchtouchEvent(ev)) { return true; } return ontouchEvent(ev); }
没错,如果superdispatchtouchEvent
方法返回false,那么就会执行Activity的ontouchEvent
方法。
小结一下:
事件分发的本质就是一个递归方法,通过往下传递,调用dispatchtouchEvent
方法,找到事件的处理者,这也就是项目中常见的责任链模式
。
在消费过程中,VIEwGroup的处理方法就是onIntercepttouchEvent
在消费过程中,VIEw的处理方法就是ontouchEvent
方法。
如果底层VIEw不消费,则一步步往上执行父元素的ontouchEvent
方法。
如果所有VIEw的ontouchEvent
方法都返回false,则最后会执行到Activity的ontouchEvent
方法,事件分发也就结束了。
完整事件消费伪代码:
public boolean dispatchtouchEvent(MotionEvent event) { boolean isConsume = false; if (isVIEwGroup) { //VIEwGroup if (onIntercepttouchEvent(event)) { isConsume = consumeEvent(event); } else { isConsume = child.dispatchtouchEvent(event); } } else { //VIEw isConsume = consumeEvent(event); } if (!isConsume) { //如果自己没拦截,子VIEw没有消费,自己也要调用消费方法 isConsume = consumeEvent(event); } return isConsume;}public voID consumeEvent(MotionEvent event) { //自己消费事件的逻辑,默认会调用到ontouchEvent if (!setontouchListener || !ontouch) { ontouchEvent(event); } }
dispatchtouchEvent() + onIntercepttouchEvent() + ontouchEvent()
,大家也可以把这三个方法作为理解记忆事件分发的重点。
终于,任务找到了它的主人,看似流程也结束了,但是还存在一个问题就是,这个任务之后的后续任务该怎么处理呢?比如要增加某某模块功能。
不可能再走一遍公司流程吧?如果按照正常逻辑,是应该找到当初负责我们任务的那个人来继续处理,看看AndroID公司
是不是这么做的。
一个MotionEvent
事件序列一般包括:
ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL
刚才我们都说的是ACTION_DOWN
,也就是手机按下的事件处理,那么后续的移动手机,离开屏幕事件该怎么处理呢?
假设之前已经有一个ACTION_DOWN
并且被某个子VIEw消费了,所以mFirsttouchTarget
会有一条完整的指向,这时候来了第二个事件——ACTION_MOVE
。
if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { }
然后就会发现,ACTION_MOVE
事件根本进不去对子VIEw
的循环方法,而是直接到了最后面的逻辑:
if (mFirsttouchTarget == null) { handled = dispatchtransformedtouchEvent(ev, canceled, null, touchTarget.ALL_POINTER_IDS); } else { touchTarget target = mFirsttouchTarget; while (target != null) { final touchTarget next = target.next; if (alreadydispatchedToNewtouchTarget && target == newtouchTarget) { handled = true; } else { if (dispatchtransformedtouchEvent(ev, cancelChild, target.child, target.pointerIDBits)) { handled = true; } } predecessor = target; target = next; } }
如果mFirsttouchTarget
为null,就是之前说过的转到VIEwGroup自身的ontouchEvent
方法。
这里很明显不为null,所以走到else中,又开始遍历mFirsttouchTarget
,之前说过单点触控的时候,target.next
为null,target.child
为消费链的下一层VIEw,所以其实就是将事件交给了下一层VIEw。
这里有个点很多朋友可能之前没注意到,就是当ACTION_DOWN
的时候,走到这里,会通过mFirsttouchTarget找到那个消费的VIEw执行dispatchtransformedtouchEvent
。
但是这之前,遍历VIEw
的时候已经执行了一次dispatchtransformedtouchEvent
方法,难道这里还要执行一次dispatchtransformedtouchEvent
方法吗?
这不就重复了?
alreadydispatchedToNewtouchTarget
。这个变量代表之前是否已经执行过一次VIEw消费事件,当事件为ACTION_DOWN
,就会遍历VIEw,如果vIEw消费了事件,那么alreadydispatchedToNewtouchTarget
就被赋值为true,所以到这里也就不会再次执行了,直接handled = true
。所以后续任务
的处理逻辑也基本明白了:
只要某个VIEw开始处理拦截事件,那么这一整个事件序列都只能交给它来处理。
优化任务派发流程(解决滑动冲突)到此,任务终于是分发完成了,任务完成后,小组开了一个总结会议
:
其实任务分发过程还是有可以优化的过程,比如有些任务是不一定就只交给一个人做,比如交给两个人做,把A擅长的任务给A做,B擅长的任务给B做,最大化利用好每个人。
但是我们之前的逻辑默认
是按下任务交给了A,后续都会交给A。所以这时候就需要设计一种机制对某些任务进行拦截。
其实这就涉及到滑动冲突
的问题了,举例一个场景:
外面的VIEwGroup
是横向移动,而内部的VIEwGroup
是需要纵向移动的,所以需要在ACTION_MOVE
的时候对事件进行判断和拦截。(类似VIEwGroup+Fragment+RecyclervIEw)
直接说AndroID公司的解决方案,两种方案:
外部拦截法。内部拦截法。外部拦截法外部拦截法比较简单,因为不管子VIEw是否拦截,每次都会执行onIntercepttouchEvnet
方法,所以我们就可以在这个方法中,根据自己的业务条件选择是否拦截事件。
//外部拦截法:父vIEw.java @OverrIDe public boolean onIntercepttouchEvent(MotionEvent ev) { boolean intercepted = false; //父vIEw拦截条件 boolean parentCanIntercept; switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if (parentCanIntercept) { intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; } return intercepted; }
逻辑很简单,就是根据业务条件,在onIntercepttouchEvent
中决定是否拦截,因为这种方法是在父VIEw中控制是否拦截,所以这种方法叫做外部拦截法。
但是这和我们之前的认知又冲突了,如果ACTION_DOWN
交给了子VIEw处理,那么后续事件应该会直接被分发给这个vIEw呀,为什么还能被父VIEw拦截的?
我们再来看看dispatchtouchEvent
方法:
public boolean dispatchtouchEvent(MotionEvent ev) { final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN|| mFirsttouchTarget != null) { intercepted = onIntercepttouchEvent(ev); } // dispatch to touch targets. if (mFirsttouchTarget == null) { handled = dispatchtransformedtouchEvent(ev, canceled, null, touchTarget.ALL_POINTER_IDS); } else { while (target != null) { if (alreadydispatchedToNewtouchTarget && target == newtouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchtransformedtouchEvent(ev, cancelChild, target.child, target.pointerIDBits)) { handled = true; } } } } }
当事件为ACTION_MOVE
的时候,并且在onIntercepttouchEvent
方法返回了true,所以这里的intercepted=true
,再到下面的逻辑,cancelChild
的值也为true,然后被传到了dispatchtransformedtouchEvent
方法,没错,又是这个方法,不同的是cancelChild
子段为true。
看这个字段的名字肯定是和取消子vIEw事件有关的,继续看看:
private boolean dispatchtransformedtouchEvent(MotionEvent event, boolean cancel, VIEw child, int desiredPointerIDBits) { final boolean handled; if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchtouchEvent(event); } else { handled = child.dispatchtouchEvent(event); } event.setAction(oldAction); return handled; } }
看出来了么,当第二个字段cancel为true的时候,事件会被修改成ACTION_CANCEL
!!,然后才会被继续传下去。
所以就算某个VIEw消费了ACTION_DOWN
,但是当后续事件来的同时,在父元素的onIntercepttouchEvent()
中返回true,那么这个事件就会被修改为ACTION_CACLE
事件再传给子VIEw。
所以子VIEw
再次交出了对该事件序列的控制权,这也就是外部拦截法能实现的原因。
继续看看内部拦截法:
//父vIEw.java @OverrIDe public boolean onIntercepttouchEvent(MotionEvent ev) { if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { return false; } else { return true; } } //子vIEw.java @OverrIDe public boolean dispatchtouchEvent(MotionEvent event) { //父vIEw拦截条件 boolean parentCanIntercept; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: getParent().requestdisallowIntercepttouchEvent(true); break; case MotionEvent.ACTION_MOVE: if (parentCanIntercept) { getParent().requestdisallowIntercepttouchEvent(false); } break; case MotionEvent.ACTION_UP: break; } return super.dispatchtouchEvent(event); }
内部拦截法是将主动权交给子VIEw,如果子VIEw需要事件就直接消耗,否则交给父容器处理。我们列举下DOWN和MOVE两种情况:
ACTION_DOWN
的时候,子VIEw必须能消费,所以父VIEw的onIntercepttouchEvent
要返回false,否则就被父VIEw拦截了,而且后续事件也不会传到子VIEw这里了。ACTION_MOVE
的时候,父VIEw的onIntercepttouchEvent
方法要返回true,表示当子VIEw不想消费的时候,父VIEw能及时消费,那么子VIEw怎么控制呢?可以看到代码设置了一个requestdisallowIntercepttouchEvent
方法,这个是干嘛呢? protected static final int FLAG_disALLOW_INTERCEPT = 0x80000; @OverrIDe public voID requestdisallowIntercepttouchEvent(boolean disallowIntercept) { if (disallowIntercept) { mGroupFlags |= FLAG_disALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_disALLOW_INTERCEPT; } }
这种通过|=
和 &= ~
运算符修改参数是源码中常见的设置标识的方法:
|=
将标志位设置为1&= ~
将标识位设置为0所以在需要父元素拦截的时候就设置了requestdisallowIntercepttouchEvent(false)
方法,让标志位设置为0,这样父元素就能执行到onIntercepttouchEvent方法。
具体生效代码就在dispatchtouchEvent
方法中:
if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndCleartouchTargets(ev); resettouchState(); } final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirsttouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_disALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onIntercepttouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } }
可以看到,如果disallowIntercept
为false,就代表父VIEw要拦截,然后就会执行到onIntercepttouchEvent
方法,在onIntercepttouchEvent
方法中返回ture,父VIEw成功拦截。
经过拇指记者的探访,终于把AndroID公司对于事件任务处理摸清楚了,希望对于屏幕前的你能有些帮助,下期再见啦。
手表去专柜买还是网上买
怎么调手表日期比较好
手表如何戴在手腕上舒服
总结以上是内存溢出为你收集整理的拇指记者深入Android公司,打探事件分发机制背后的秘密全部内容,希望文章能够帮你解决拇指记者深入Android公司,打探事件分发机制背后的秘密所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)