当前位置:网站首页>根据热门面试题分析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!
边栏推荐
- 華為HCIP-DATACOM-Core_03day
- Jenkins task grouping
- SAP MM STO单据的外向交货单创建后新加ITEM?
- How to use clipboard JS library implements copy and cut function
- Netease Cloud Wechat applet
- **Grafana installation**
- 在EXCEL写VBA连接ORACLE并查询数据库中的内容
- Jenkins modifies the system time
- 二叉树高频题型
- Unity shader (learn more about vertex fragment shaders)
猜你喜欢

答案在哪里?action config/Interceptor/class/servlet

4、 Fundamentals of machine learning

Pytest installation (command line installation)
![[4G/5G/6G专题基础-146]: 6G总体愿景与潜在关键技术白皮书解读-1-总体愿景](/img/fd/5e8f74da25d9c5f7bd69dd1cfdcd61.png)
[4G/5G/6G专题基础-146]: 6G总体愿景与潜在关键技术白皮书解读-1-总体愿景

如何使用clipboard.js库实现复制剪切功能

其实特简单,教你轻松实现酷炫的数据可视化大屏

esp8266使用TF卡并读写数据(基于arduino)
![[4g/5g/6g topic foundation-146]: Interpretation of white paper on 6G overall vision and potential key technologies-1-overall vision](/img/fd/5e8f74da25d9c5f7bd69dd1cfdcd61.png)
[4g/5g/6g topic foundation-146]: Interpretation of white paper on 6G overall vision and potential key technologies-1-overall vision

How to use clipboard JS library implements copy and cut function

沙龙预告|GameFi 领域的瓶颈和解决方案
随机推荐
Network request process
其实特简单,教你轻松实现酷炫的数据可视化大屏
NETCORE 3.1 solves cross domain problems
【云原生】DevOps(一):DevOps介绍及Code工具使用
ViewPager2和VIewPager的区别以及ViewPager2实现轮播图
shake数据库中怎么使用Mongo-shake实现MongoDB的双向同步啊?
信息安全实验三 :PGP邮件加密软件的使用
ComputeShader
【BW16 应用篇】安信可BW16模组/开发板AT指令实现MQTT通讯
Elaborate on MySQL mvcc multi version control
Vs2013 generate solutions super slow solutions
JS reverse tutorial second issue - Ape anthropology first question
Jmeters use
How to become a senior digital IC Design Engineer (1-6) Verilog coding Grammar: Classic Digital IC Design
Regular matching starts with XXX and ends with XXX
First issue of JS reverse tutorial
Octopus future star won a reward of 250000 US dollars | Octopus accelerator 2022 summer entrepreneurship camp came to a successful conclusion
Jenkins modifies the system time
二叉树高频题型
Mysql database lock learning notes