前言

聊到事件分发,很多朋友就会想到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)

然后任务处理部门需要把任务交给 专业处理任务的项目组了,这就涉及到跨部门沟通了(跨进程通信)。

大家都知道跨部门沟通是个比较麻烦的事情,谁来完成这个事情呢?InputChannel

让我们回到ViewRootImplsetView方法:

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.java
private void dispatchInputEvent(int seq, InputEvent event) {
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event);
} //ViewRootImpl.java ::WindowInputEventReceiver
final class WindowInputEventReceiver extends InputEventReceiver {
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
} //ViewRootImpl.java
void 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)

确定了任务的分类,接下来就开始组内任务讨论整理了,这个阶段发生在几个大佬之间的谈话,这几个大佬分别是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。

  • 其次,交给Acitivity后,为什么不直接交给顶层View——DecorView开始分发事件呢?

因为ActivityDecorView之间并没有直接关系。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或者子ViewGroup处理?

接着说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都不处理事件,那么:

  • 默认执行ViewGrouponTouchEvent方法。
  • 如果设置ViewGroupsetOnTouchListener,就会执行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开发艺术探索》

每日一问 | 事件到底是先到DecorView还是先到Window的?

Input系统—事件处理全过程

反思|Android 事件分发机制的设计与实现

View·InputEvent事件投递源码分析

彻底掌握 Android touch 事件分发时序

拜拜

感谢大家的阅读,有一起学习的小伙伴可以关注下我的公众号——码上积木️️

每日一个知识点,积少成多,建立知识体系架构。

这里有一群很好的Android小伙伴,欢迎大家加入~

拇指记者深入Android公司,打探事件分发机制背后的秘密的更多相关文章

  1. Android中的事件分发机制

    Android中的事件分发机制 作者:丁明祥 邮箱:2780087178@qq.com 这篇文章这周之内尽量写完 参考资料: Android事件分发机制完全解析,带你从源码的角度彻底理解(上) And ...

  2. Android view 的事件分发机制

    1 事件的传递顺序是 Activity -> Window -> 顶层View touch 事件产生后,最先由 activity 的 dispatchTouchEvent 处理 /** * ...

  3. Android中的事件分发机制总结

    Android 的事件分发机制 一.View的事件分发总结: View的onTouchEvent和OnTouch区别  还是以自定义的TestButton为例. 我们可以通过重写onTouchEven ...

  4. Android的Touch事件分发机制简单探析

    前言 Android中关于触摸事件的分发传递是一个很值得研究的东西.曾不见你引入了一个ListView的滑动功能,ListView就不听你手指的指唤来滚动了:也不知道为啥Button设置了onClic ...

  5. Android View的事件分发机制

    准备了一阵子,一直想写一篇事件分发的文章总结一下.这个知识点实在是太重要了. 一个应用的布局是丰富的,有TextView,ImageView,Button等.这些子View的外层还有ViewGroup ...

  6. Android View的事件分发机制和滑动冲突解决方案

    这篇文章会先讲Android中View的事件分发机制,然后再介绍Android滑动冲突的形成原因并给出解决方案.因水平有限,讲的不会太过深入,只希望各位看了之后对事件分发机制的流程有个大概的概念,并且 ...

  7. Android程序员事件分发机制学习笔记

    通过问题来学习一个东西是很好的方法.学习Android中View的事件体系,我也通过给自己提问题,在解决问题的同时也就知道了其中原理. 首先来几个问题起步: 什么是事件?什么是事件分发机制? 在我们通 ...

  8. Android View的事件分发机制探索

    概述 Android事件传递机制也是Android系统中比较重要的一块,事件类型有很多种,这里主要讨论TouchEvent的事件在framework层的传递处理机制.因为对于App开发人员来说,理解f ...

  9. 浅谈Android中的事件分发机制

    View事件分发机制的本质就是就是MotionEvent事件的分发过程,即MotionEvent产生后是怎样在View之间传递及处理的. 首先介绍一下什么是MotionEvent.所谓MotionEv ...

随机推荐

  1. c++ 获取和设置 窗口标题

    有些窗口标题中含有中文,可能识别不出来,可以改一下 SetWindowTextW GetWindowTextW 设置: SetWindowTextW( (HWND)0x00370868/*窗口句柄*/ ...

  2. Vulhun-y0usef靶机通关

    Vulhub-y0sef靶机通关 安装靶机环境,下载地址:https://www.vulnhub.com/entry/y0usef-1,624/ 网络模式:桥接 目标:user.txt和root.tx ...

  3. 【死磕JVM】JVM快速入门之前戏篇

    简介 Java是一门可以跨平台的语言,但是Java本身是不可以实现跨平台的,需要JVM实现跨平台.javac编译好后的class文件,在Windows.Linux.Mac等系统上,只要该系统安装对应的 ...

  4. 搭建SSH框架

    以下为链接地址:https://www.2cto.com/kf/201606/518341.html

  5. selectors版socket

    一.作业需求: 使用SELECT或SELECTORS模块实现并发简单版FTP 允许多用户并发上传下载文件 二.readme 一.作业需求: 使用SELECT或SELECTORS模块实现并发简单版FTP ...

  6. ”capslock+“ 一款超级文本编辑外挂

    "capslock+" 一款超级文本编辑外挂 彻底释放大写锁定键"caps lock" 潜力的强大工具 一.基础功能 CapsLock按键与以下按键的组合 实现 ...

  7. ClickHouse源码笔记4:FilterBlockInputStream, 探寻where,having的实现

    书接上文,本篇继续分享ClickHouse源码中一个重要的流,FilterBlockInputStream的实现,重点在于分析Clickhouse是如何在执行引擎实现向量化的Filter操作符,而利用 ...

  8. c++中深层复制(浅层复制运行错误)成功运行-----sample

    下面随笔给出c++中深层复制(浅层复制运行错误)成功运行------sample. 浅层复制与深层复制 浅层复制 实现对象间数据元素的一一对应复制. 深层复制 当被复制的对象数据成员是指针类型时,不是 ...

  9. 理解ASP.NET Core 中的WebSocket

    在本文中,我们将详细介绍RFC 6455 WebSocket规范,并配置一个通用的.NET 5应用程序通过WebSocket连接与SignalR通信. 我们将深入底层的概念,以理解底层发生了什么. 关 ...

  10. POJ-2240(floyd算法简单应用)

    Arbitrage poj-2240 #include<iostream> #include<cstdio> #include<cstring> #include& ...