Android 面试:事件分发8连问
Posted 涂程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 面试:事件分发8连问相关的知识,希望对你有一定的参考价值。
前言
android
事件分发是我们开发中的必备技能,但是关于事件分发的的知识点也有些繁杂。
如果我们从头开始阅读源码,往往千头万绪,抓不住要领。
我们可以从以下几个问题出发,提纲契领地掌握这个知识点。
读者最后也可以通过判断自己的问题是否得到解决,来判断自己是否真的掌握了这个知识点
首先我们可以思考下,从我们触摸屏幕到App
响应事件,都发生了什么,可以分为哪几个部分?
我们将整个Touch
事件可以分解为以下几个部分
- 1.
Touch
事件如何从屏幕到我们的App
- 2.
Touch
事件到达App
后怎么传递到对应页面 - 3.
Touch
事件到达对应页面后内部怎样分发
其中与上层软件开发息息相关的就是第3条,也是我们最关注的,它也可以拆解为以下几个问题
ViewGroup
是否拦截事件,拦截与不拦截后分别怎么处理?- 子
View
是否拦截事件,拦截与不拦截后分别怎么处理? ViewGroup
与子View
都不拦截,最终事件如何处理?- 如何处理事件冲突?
以上问题总结为思维导图如下:
接下来就具体分析分析
1.Touch
事件如何从屏幕到我们的App
1.1 硬件与内核部分
当我们触摸屏幕或者按键操作时,首先触发的是硬件驱动
驱动收到事件后,将相应事件写入到输入设备节点,这便产生了最原生态的内核事件
当屏幕被触摸,Linux
内核会将硬件产生的触摸事件包装为Event
存到/dev/input/event[x]
目录下
这样做的目的是将输入事件封装为通用的Event
,供后续处理
1.2 SystemServer
部分
我们知道,当系统启动时,在SystemServer
进程会启动一系列系统服务,如AMS
,WMS
等
其中还有一个就是我们管理事件输入的InputManagerService
这个服务就是用来负责与硬件通信,接受屏幕输入事件。
在其内部,会启动一个读线程,也就是InputReader
,它会从系统也就是/dev/input/
目录拿到任务,并且分发给InputDispatcher
线程,然后进行统一的事件分发调度。
1.3 跨进程通信传递给App
现在系统进程已经拿到输入事件了,但还需要传递给App
进程,这就涉及到跨进程通信的部分
我们的App
中的Window
与InputManagerService
之间的通信实际上使用的InputChannel
InputChannel
是一个pipe
,底层实际是通过socket
进行通信。
我们知道在Activity
启动时会调用ViewRootImpl.setView()
在ViewRootImpl.setView()
过程中,也会同时注册InputChannel
:
public final class ViewRootImpl {
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
requestLayout();
// ...
// 创建InputChannel
mInputChannel = new InputChannel();
// 通过Binder在SystemServer进程中完成InputChannel的注册
mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}
}
这里涉及到了WindowManagerService
和Binder
跨进程通信,读者不需要纠结于详细的细节
只需了解最终在SystemServer
进程中,WindowManagerService
根据当前的Window
创建了SocketPair
用于跨进程通信,同时并对App
进程中传过来的InputChannel
进行了注册
这之后,ViewRootImpl
里的InputChannel
就指向了正确的InputChannel
, 作为Client
端,其fd
与SystemServer
进程中Server
端的fd
组成SocketPair
, 它们就可以双向通信了。
然后我们App
进程的主线程就会监听socket
客户端,当收到消息(输入事件)后,回调NativeInputEventReceiver.handleEvent()
方法,最终会走到InputEventReceiver.dispachInputEvent
方法。
经过以上操作App
终于拿到输入事件了,接下来就是传递到对应页面
1.4小结
关于内核处理输入事件与跨进程通信的部分一般来说不是应用开发者最关注的部分,也不是本文的重点,所以只做了概述
想要了解细节的同学可参考:Input系统—事件处理全过程
2.Touch
事件到达App
后怎么传递到对应页面
现在我们已经在App
进程中拿到输入事件了,接下来看看事件如何分发到页面
我们接下来跟一下源码
2.1 事件回传到ViewRootImpl
//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
不仅负责界面的绘制,同时负责事件的传递
2.2 第一次责任链分发
接下来走到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;
....
//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) {
if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
forward(q);
} else if (shouldDropInputEvent(q)) {
finish(q, false);
} else {
traceEvent(q, Trace.TRACE_TAG_VIEW);
final int result;
try {
result = onProcess(q);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
apply(q, result);
}
}
}
如上所示:
1.QueuedInputEvent
是一种输入事件,链表结构,遍历传递给InputStage
2.InputStage
是处理输入的责任链,在调用deliver
时会遍历责任链传递事件
3.事件分发完成后会调用finishInputEvent
,告知SystemServer
进程的InputDispatcher
线程,最终将该事件移除,完成此次事件的分发消费。
那么问题来了,InputStage
的责任链是什么时候组件的呢?
2.3 组装责任链
我们得回到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
来处理事件。这个阶段其实是对事件进行一些简单的分类处理,比如视图输入事件,输入法事件,导航面板事件等等。
我们的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);
}
}
1.经过层层回调会调用到mView.dispatchPointerEvent
2.我们知道ViewRootImpl
中的mView
就是DecorView
现在事件已经传递到了DecorView
,也就是我们界面的根布局
接下来是事件在Activity
,Window
,DecorView
中的传递
2.4 事件在Activity
,Window
,DecorView
中的传递
//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
看起来是一个很奇怪的事件流转,事件从DecorView
出发,最后又回到了DecorView
,为什么这样做呢?
2.4.1 为什么ViewRootImpl
不直接把事件交给Activity
?
主要是为了解藕
ViewRootImpl
并不知道有Activity
这种东西存在!它只是持有了DecorView
。所以,不能直接把触摸事件送到Activity.dispatchTouchEvent()
2.4.2 交给Acitivity
后,为什么不直接交给DecorView
开始分发事件呢?
因为Activity
不知道有DecorView
!
但是,Activity
持有PhoneWindow
,而PhoneWindow
当然知道自己的窗口里有些什么了,所以能够把事件派发给DecorView
。
在Android
中,Activity
并不知道自己的Window
中有些什么,这样耦合性就很低了,Activity
不需要知道Window
中的具体内容
2.5 小结
经过上述过程,事件终于到了我们熟悉的ViewGroup.dispatchTouchEvent
流程图如下所示:
3.Touch
事件到达页面后内部怎样分发
下面就是我们最常用也是最常见的事件分发部分了
3.1 ViewGroup
是否拦截事件
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;
//只有ActionDown或者mFirstTouchTarget为空时才会判断是否拦截
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
}
}
if (!canceled && !intercepted) {
//事件传递给子view
....
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//如果子View消耗了则给mFirstTouchTarget赋值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
...
}
}
//mFirstTouchTarget不为空时会调用dispatchTransformendTouchEvent
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);
}
}
从以上可以看出
1.只有当Action_Down
或者mFirstTouchTarget
不为空时才判断是否拦截
2.mFirstTouchTarget
是个链表结构,代表某个子View
消费了事件,为null
则表示没有子View
消费事件
3.在判断是否拦截前有个disallowIntercept
字段,这个在后面事件冲突内部拦截法时会用到
4.接下来就到了onInterceptTouchEvent
,ViewGroup
是否拦截事件正是由这个方法控制的
3.1.2 ViewGroup
拦截后会发生什么?
1.拦截之后,事件自然就不会再下发给子View
2.接下来如果mFirstTouchTarget
为null
,则会调用到dispatchTransformedTouchEvent
,然后调用到super.dispatchTouchEvent
,最终到ViewGroup.onTouchEvent
3.为什么使用mFirstTouchTarget==null
来判断是否是ViewGroup
处理,是因为mFirstTouchTarget==null
有两种情况,一是ViewGroup
拦截,二是子View
没有处理事件,两种情况最后都回调到ViewGroup.onTouchEvent
通过上面的分析,我们可以得出ViewGroup拦截的伪代码:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean isConsume = false;
if (isViewGroup) {
if (onInterceptTouchEvent(event)) {
isConsume = super.dispatchTouchEvent(event);
}
}
return isConsume;
}
如果是ViewGroup
,会先执行到onInterceptTouchEvent
方法判断是否拦截,如果拦截,则执行父类View
的dispatchTouchEvent
方法。
3.1.3 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;
//遍历子View
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;
}
}
}
}
}
private boolean dispatchTransformedTouchEvent(View child) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
}
如果不拦截,ViewGroup
内主要做以下几件事
1.遍历当前ViewGroup
的所有子View
2.判断当前View
是否在当前子View
的坐标范围内,不在范围内不能接收事件,直接跳过
3.利用dispatchTransformedTouchEvent
,如果返回true
,则通过addTouchTarget
对mFirstTouchTarget
赋值
4.dispatchTransformedTouchEvent
做的主要就是两个事,如果child
不为null
,则事件分发到child
,否则调用super.dispatchTouchEvent
,并最终返回结果
5.mFirstTouchTarget
是单链表结构,记录消费链,但是在单点触控的时候这个特性没有用上,只是一个普通的TouchTarget
对象
3.2 子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;
}
子View
的diapatchTouchEvent
逻辑比较简单
1.如果设置了setOnTouchListener
并且返回为true
,那么onTouchEvent
就不再执行
2.否则执行onTouchEvent
,我们常用的OnClickListenr
就是在onTouchEvent
里触发的
所以默认情况下会直接执行onTouchEvent
,如果我们设置了setOnClickListener
或者setLongClickListener
,都会正常触发
3.2.1 如果子View
消费事件会怎么样?
上面说了,如果子View
消费事件,即dispatchTouchEvent
方法返回true
表示这个事件我处理了,那么事件从此结束,ViewGroup
的dispatchTouchEvent
也返回true
最后回到Activity
的dispatchTouchEvent
,也是直接返回true
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
小结:如果子View
消费事件的话,事件就此结束了
3.2.2 如果子View
不消费事件会怎么样?
子View
不拦截事件,那么mFirstTouchTarget
就为null
,退出循环后,调用了dispatchTransformedTouchEvent
方法。
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
小结一下:
1.子View
不拦截事件,就回调到了dispatchTransformedTouchEvent
2.然后就调到了super.dispatchTouchEvent
3.那么接下来ViewGroup
就跟子View
的逻辑一样了,默认执行onTouchEvent
,如果设置了setOnTouchLister
则执行onTouch
3.3 如果ViewGroup
与子View
都不拦截会怎么样
如果ViewGroup
与子View
都不拦截,即mFirstTouchTarget == null
,dispatchTouchEvent
也返回false
再看看Activity
的源码
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
答案很明显:会执行Activity
的onTouchEvent
方法
3.4 后续事件如何分发?
事件分发的处理者已经找到了,看起来任务已经完成了。
但其实事件分发是包括ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL
的一系列事件,我们上面分析的都是Action_DOWN
的过程
后续事件如何处理?
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
//1.遍历子View
//2.判断是否在坐标范围
//3.分发事件,给mFirstTouchTarget赋值
//4.如果分发成功,alreadyDispatchedToNewTouchTarget赋值为true
...
}
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;
}
}
}
从上可以看出
1.后续的事件不会走对子View
的循环判断的方法,因为已经找到了目标View
,直接通过mFirstTouchTarget
分发
2.如果某个View
开始处理拦截事件,后续事件序列只能由它处理
3.5 小结
- 事件分发的本质就是一个递归方法,通过往下传递,调用
dispatchTouchEvent
方法,找到事件的处理者,这也就是项目中常见的责任链模式。 - 在分发过程中,
ViewGroup
通过onInterceptTouchEvent
判断是否拦截事件 - 在分发过程中,
View
的默认通过onTouchEvent
处理事件 - 如果底层
View
不消费,则默认一步步往上执行父元素onTouchEvent
方法。 - 如果所有
View
的onTouchEvent
方法都返回false
,则最后会执行到Activity
的onTouchEvent
方法,事件分发也就结束了。
4 滑动冲突解决
我们在开发中经常会碰到滑动冲突的问题,比如一个页面同时有横向与竖向两个方向的滑动,这个时候就需要根据情况在Action_MOVE
时对事件进行判断和拦截
常见的滑动冲突解决方法有两种:
1.外部拦截法
2.内部拦截法
4.1 外部拦截法
外部拦截法的原理很简单,就是通过我们上面分析的onInterceptTouchEvent
进行
外部拦截法的模板代码如下:
//外部拦截法:父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;
}
但是这种方式会带来一个问题,如果ACTION_DOWN
交给了子View
处理,那么后续事件应该会直接被分发给这个view
呀,为什么还能被父View
拦截的?
我们再来看看dispatchTouchEvent
方法
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
//1.判断拦截
intercepted = onInterceptTouchEvent(ev);
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//4.后续事件就直接交给ViewGroup处理了
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
while (target != null) {
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//2.cancelChild为ture
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
//3.mFirstTouchTarget被置为null
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
}
}
}
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;
}
}
如上可知:
1.首先通过onInterceptTouchEvent
方法拦截事件
2.intercepted
为true
导致cancelChild
也为true
,dispatchTransformedTouchEvent
方法传递Action_CANCEL
给子View
3.cancelChild
后将mFirstTouchTarget
置为空
4.mFirstTouchTarget
为空后,后续的事件都由ViewGroup
处理了
综上就是外部拦截法能成功的原因
4.2 内部拦截法
接下来看下内部拦截法的模板代码
//父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:
getParent().requestDisallowInterceptTouchEvent(!parentCanIntercept);
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
内部拦截法是将主动权交给子View
,如果子View
需要事件就直接消耗,否则交给父容器处理
内部拦截法主要通过requestDisallowInterceptTouchEvent
方法控制
我们看下为什么调用这个方式可以实现内部拦截
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;
//只有ActionDown或者mFirstTouchTarget为空时才会判断是否拦截
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
}
}
}
如上所示,原理很简单
1.子View
通过requestDisallowInterceptTouchEvent
控制mGroupFlags
的值,从而控制disallowIntercept
的值
2.disallowIntercept
为true
时就不会走到onInterceptTouchEvent
,外部也就无法拦截了,当需要外部处理时,将disallowIntercept
置为false
即可
总结
本文详细总结了事件分发机制从屏幕到View
的详细过程,下面列出几个问题供读者参考,方便读者判断是否真正掌握了这个知识点
- 1.简单描述下事件是怎么从屏幕传递到
View
的 - 2.事件分发过程中有几次责任链分发?
- 3.为什么事件分发从
DecorView
->Activity
->PhoneWindow
->DecorView
- 4.滑动冲突有几种解决方法?分别介绍一下
- 5.如果只在
onInterceptTouchEvent
的ACTION_MOVE
中拦截事件,说一下从ViewGroup
到View
的各个Action
是如何传递的 - 6.点击
ViewGroup
中的一个View
,然后手指移动到其他地方然后抬起,事件是如何分发的 - 7.
View
的OnTouch
和OnTouchEvent
有什么关系?OnTouch
和OnClick
事件呢? - 8.手写一下长按事件的伪代码
最后
小编在网上收集了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接去我 GitHub地址:https://github.com/733gh/Android-T3 访问查阅。
以上是关于Android 面试:事件分发8连问的主要内容,如果未能解决你的问题,请参考以下文章