当前位置:网站首页>根据热门面试题分析Android事件分发机制(一)
根据热门面试题分析Android事件分发机制(一)
2022-07-07 07:03:00 【光阴剑客】
(一)事件分发机制概述
面试题:你了解过Android的事件分发机制吗?请大致介绍一下
点击事件产生后,首先传递给Activity的dispatchTouchEvent方法,这时会调用getWindow().superDispatchTouchEvent(ev),由于PhoneWindow是Android Window的唯一实现类,所以会通过PhoneWindow中的mDecor.superDispatchTouchEvent(event)去调用父类的dispatchTouchEvent方法,mDecor 是DecorView,它继承自FrameLayout,FrameLayout继承自ViewGroup,因为FrameLayout没有重写dispatchTouchEvent方法,所以在DecorView中:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
就会调用ViewGroup的dispatchTouchEvent方法,在该方法中会执行onInterecptTouchEvent方法判断是否拦截,在不拦截的情况下,会遍历ViewGroup的子元素,进入子view的dispatchTouchEvent方法,如果子view设置了onTouchListener,就执行onTouch方法,并根据onTouch方法的返回值为true还是false决定是否执行onTouchEvent方法,如果是false,则继续执行onTouchEvent方法,在onTouchEvent的ACTION_UP事件中判断,如果设置了onClickListener,就执行onClick方法。
(二)源码解析面试中的高频考点
面试题1:如果父view中不拦截down事件,拦截move,up事件,在子view中设置了requestDisallowInterceptTouchEvent(true);(请求父view不拦截事件)这个标志后,子view能收到move,up事件吗?
解析: 视情况而定:(1)假如子view消费down事件,并且设置了requestDisallowInterceptTouchEvent(true),子view可以收到move,up事件(2)子view不消费down事件,则只会收到down事件,不会收到move,up事件。原因如下:
(1)子view消费down事件:在我们父view不重写dispatchTouchEvent的情况下,会使用ViewGroup的dispatchTouchEvent方法分发事件。下面先通过源码了解这个问题的相关部分:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...//省略部分代码
// 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();//如果是down事件来的时候,重置Touch的状态
}
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//这个状态可以通过
//requestDisallowInterceptTouchEvent(true)这个方法设置,
//这里会重置它
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
上面的代码的意思就是当down事件来时会做一些状态重置,因为down事件是事件处理的开始,所以每次down事件来,都相当于一次事件处理的开始,需要将相关的状态重置。
然后是判断是否拦截事件部分的源码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......//省略部分代码
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//如果事件是down事件的话,disallowIntercept
//会一直是false,因为down的状态在刚开始时都被重置过了
//mFirstTouchTarget是一个链表,用来保存用户第一次点击的信息,如果子view消费了事件,就会给这个变量赋值,反之则不会
if (!disallowIntercept) {
//所以down事件时,这个判断一定会进
intercepted = onInterceptTouchEvent(ev);
//最后是否拦截down事件就靠这里了,如果拦截了down事件
// onInterceptTouchEvent(ev)为true,那么
//子view就收不到事件了,设置啥标记也没有用,不拦截的话,down
//事件可以通过,子view有机会处理事件
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;
}
上面的代码执行完会得到一个intercepted值,这个值直接会影响到事件的分发,同样在viewGroup的dispatchTouchEvent方法中:
if (!canceled && !intercepted) {
//在不拦截的情况下会进入下面的代码块
....//省略部分代码
final ArrayList<View> preorderedList = buildTouchDispatchChildList();//遍历子view
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();//检查是否设置了子view的绘制顺序
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//获取到子view的索引值
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
//如果子view不能收到事件或者是没有在点击的范围内,也就是没点击到view上,结束本次循环,进行下一次循环遍历
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
//newTouchTarget不为空,表示当前的view已经处理了事件,直接结束循环。
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//dispatchTransformedTouchEvent这个方法是询问子view是否处理事件,处理的话返回true,不处理返回false
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 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);//如果view处理事件的话就会通过addTouchTarget()为mFirstTouchTarget赋值,后续move和up事件不会被拦截。
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);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
所以情况一根据上面的源码可以总结为:
(1)down事件到来后,首先会对其相关的状态进行重置,比如mFirstTouchTarget,FLAG_DISALLOW_INTERCEPT等
(2)接着就会判断是否拦截事件,如果是down事件,会直接进入是否进行拦截的代码块,这时候会判断disallowIntercept这个标志位,但是由于是down事件,这个标志位都被重置了,所以down事件的情况下,这个标志位一直是false,所以down事件会进入到判断的代码块中,由onInterceptTouchEvent决定是否拦截,如果拦截了,子view就收不到事件了,这里讨论的是不拦截的情况下:
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
}
(3)不拦截down事件的情况下,mFirstTouchTarget会被赋值,所以当move和up事件来临时,会进入到下面的代码块中
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//mFirstTouchTarget不为空,会走入到是否拦截判断代码块中
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//disallowIntercept设置为true,则不会进入下面的判断代码块,所以就不会执行父view的onInterceptTouchEvent(ev)方法,所以在父view中设置啥拦截都没有用,反正也执行不了。若是设置为false,即子view设置了requestDisallowInterceptTouchEvent(false),那么会进入到下面的代码块中,具体是否拦截由父view的onInterceptTouchEvent(ev)方法决定
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
通过上面的源码分析我们可以知道,在父view不拦截down事件的时候,子view设置了requestDisallowInterceptTouchEvent(true),那么就算父view拦截了move和up事件也没有用,因为根本就执行不到父view的onInterceptTouchEvent(ev)方法。如果子view设置了requestDisallowInterceptTouchEvent(false)后,那么子view是否能收到事件得看父view的onInterceptTouchEvent方法处理逻辑,本题中是拦截move和up事件,所以子view中只会收到一个父view没有拦截的down事件,以及一个cancel事件。会收到cancel事件是因为move和up事件都被拦截了,不会进入到事件分发到逻辑里面,也就是父view的:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
if (!canceled && !intercepted) {
......//省略
}
在子view消费事件的情况下(子view的dispatchTouchEvent返回true)
//子view消费事件,则mFirstTouchTarget不为空,如果子view不消费事件,则父view会自己去处理事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;//由于拦截了move和up事件,所以这cancelChild会为true,
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
......
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
// cancel就是上面传的cancelChild,在这里会设置一个ACTION_CANCEL事件,所以子view中消费事件的情况下会收到这个事件
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
所以在自view消费事件的情况下,父view中若是move和up事件被拦截,则子view会收到一个cancel事件。
面试题2:如果view设置了onTouchListener,onClickListener,onTouchEvent,那么会先执行哪一个?
解析: 在这个地方,回答的时候很多人可能都会回答,先执行onTouchListener的onTouch方法。然后就没了,这个回答表面上没有错,但是容易把天聊死。面试官也不知道该咋问了,只会吐出三个字,为什么?根据我的面试经验,这样是很难获得面试官的认可的,就算你答对了先执行哪一个函数,也只会让面试官觉得你在背面试题,虽然我们很多时候都是在背,但是我们要把原理弄清楚,争取不让面试官问太多的问题。争取一个回答将他想问的问题都回答完。
下面咱们看下应该怎么回答这种题,面试官问你这个问题,就是想考察你对事件分发机制的了解程度。我们先看源码;当子view不重写dispatchTouchEvent的时候,事件分发到子view后,会执行view的dispatchTouchEvent()方法。
public boolean dispatchTouchEvent(MotionEvent event) {
...//省略掉不相关的代码
//从下面的代码中不难看出,当用户设置了onTouchListener监听时,会去执行onTouch方法,根据onTouch()方法的返回值判断是否执行onTouchEvent.
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果onTouch()返回为true,下面的代码就不执行,因为java条件是短路的,也就是说如果//(!result)条件是false,与另外一个条件用“&&”连接的话,那么后面的一个条件就不会执行,整个判断返回false
if (!result && onTouchEvent(event)) {
result = true;
}
由上面的代码可以知道,如果用户设置了onTouchListener,onTouchevent和onClickListener方法,那么会首先执行onTouchListener中的onTouch方法,然后根据onTouch的返回结果判断是否执行onTouchEvent,假设onTouch()返回false,继续执行onTouchEvent(),我们继续看源码:
public boolean onTouchEvent(MotionEvent event) {
...省略部分代码
case MotionEvent.ACTION_UP:
//......省略部分代码
//在事件的 ACTION_UP 中会去执行clickListener的onClick()方法。
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
//如果用户设置了onClickListener的话。就会执行onClick()方法
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
综上:咱们的回答应该是,如果用户设置了onTouchListener,onTouchEvent,onClickListener时,首先会执行onTouchEvent的onTouch方法,根据onTouch的返回值决定是否执行onTouchEvent方法,如果onTouch方法返回false,则继续执行onTouchEvent,如果onTouchEvent中没有直接返回true或者false(这样会导致事件到不了view的OnTouchEvent方法,从而导致不执行咱们设置的onClickListener中的onClick方法),那么会执行到view的onTouchEvent方法,然后就会接着执行ClickListener的onClick方法
篇幅太长,所以事件分发机制的滑动冲突问题在下一个博客中总结,也是问题加源码分析的方式,希望各大朋友提出宝贵意见,大家一起进步~~~~,一起read the fucking sorce code!
边栏推荐
- Unity shader (learn more about vertex fragment shaders)
- liunx命令
- How to become a senior digital IC Design Engineer (5-3) theory: ULP low power design technology (Part 2)
- Kubernetes cluster capacity expansion to add node nodes
- Create an int type array with a length of 6. The values of the array elements are required to be between 1-30 and are assigned randomly. At the same time, the values of the required elements are diffe
- 战略合作|SubQuery 成为章鱼网络浏览器的秘密武器
- JS reverse tutorial second issue - Ape anthropology first question
- Arthas simple instructions
- Pycharm importing third-party libraries
- nlohmann json
猜你喜欢
In fact, it's very simple. It teaches you to easily realize the cool data visualization big screen
Where is the answer? action config/Interceptor/class/servlet
Unity shader (basic concept)
What development models did you know during the interview? Just read this one
esp8266使用TF卡并读写数据(基于arduino)
Lesson 1: finding the minimum of a matrix
Information Security Experiment 2: using x-scanner scanning tool
第一讲:寻找矩阵的极小值
Mysql database transaction learning notes
Jmeters use
随机推荐
Octopus future star won a reward of 250000 US dollars | Octopus accelerator 2022 summer entrepreneurship camp came to a successful conclusion
[SVN] what is SVN? How do you use it?
【frida实战】“一行”代码教你获取WeGame平台中所有的lua脚本
印象笔记终于支持默认markdown预览模式
JS inheritance prototype
Unity uses mesh to realize real-time point cloud (II)
Integer or int? How to select data types for entity classes in ORM
JS逆向教程第一发
How to become a senior digital IC Design Engineer (5-2) theory: ULP low power design technology (Part 1)
csdn涨薪技术-浅学Jmeter的几个常用的逻辑控制器使用
Network request process
asp. How to call vb DLL function in net project
Mysql database lock learning notes
Strategic cooperation subquery becomes the secret weapon of Octopus web browser
信息安全实验三 :PGP邮件加密软件的使用
如何使用clipboard.js库实现复制剪切功能
(3/8)枚举的不当用法 之 方法参数(二)
Dynamics 365online applicationuser creation method change
二叉树高频题型
**grafana安装**