本以为精通Android事件分发机制,没想到被面试官问懵了

其实DecorView就是我们通过setContentView设置布局的父容器,我们可以通过getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)这个方式就能获取到setContentView中设置的布局。从DecorView的源码中可以发现DecorView是继承FrameLayout,而FrameLayout又是继承ViewGroup的,故DecorView的间接父类为ViewGroup,在DecorView中superDispatchTouchEvent方法是使用super来调用父类的dispatchTouchEvent,故等于调用ViewGroup的dispatchTouchEvent方法(从源码中我们可以得知FrameLayout并没有dispatchTouchEvent这个方法),于是DecorView将事件传递到了ViewGroup去处理。也可以这么说,事件已经传递到了顶级View也就是Activity中通过setContentView所设置的View(顶级View通常为ViewGroup)。

流程图如下:

到这里,我们也验证了前面提到的事件分发的顺序是:Activity->Window->DecorView->ViewGroup。那么ViewGroup又是如何将事件传递给View呢?让我们来继续分析!

2. ViewGroup事件的分发机制


从上面Activity事件的分发机制我们可以知道,ViewGroup事件分发机制是从dispatchTouchEvent()开始的,所以我们从这部分的源码开始分析,由于该方法代码量很多,下面将根据需要贴出相关代码:

源码:ViewGroup#dispatchTouchEvent

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

// Check for interception.

final boolean intercepted; //是否拦截

/*

  • 当事件由ViewGroup的子元素处理时,mFirstTouchTarget会被赋值并指向子元素

*/

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;

}

} else {

// There are no touch targets and this action is not an initial down

// so this view group continues to intercept touches.

intercepted = true;

}

}

从上面源码我们可以知道,ViewGroup判断是否要拦截只会是在ACTION_DOWN的时候,或者是mFirstTouchTarget != null。mFirstTouchTarget 从后面的代码才能知道其作用。它的作用就是:当事件被ViewGroup的某个子View处理时,mFirstTouchTarget 就会指向这个子View。所以当事件被这个ViewGroup拦截时,子类就不会处理这个事件,因此mFirstTouchTarget =null,那么这个时候ACTION_MOVE和ACTION_UP事件到来时,由于判断条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,然后intercepted被赋予true,所以同一事件序列的其它事件都会默认交给该ViewGroup来处理。在上面源码中我们还可以发现这么一句语句:

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

FLAG_DISALLOW_INTERCEPT是个标记位,这个标记位是通过 requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法来设置的,一般用在子View中。如果FLAG_DISALLOW_INTERCEPT被设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其它点击事件,这是因为ViewGroup在分发事件中,如果是ACTION_DOWN事件,将会重置FLAG_DISALLOW_INTERCEPT这个标记位。让我们来看看源码。

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(); //对FLAG_DISALLOW_INTERCWPT进行重置

}

上面源码是在判断是否拦截的前面的,所以能够重置标记位,从这里我们也可以发现,当点击事件为ACTION_DOWN时,ViewGroup总是会调用自己的onInterceptTouchEvent来询问自己是否要拦截事件。

requestDisallowInterceptTouchEvent方法针对的是ACTION_DOWN以外的其他事件,并且是在不拦截ACTION_DOWN事件的情况下才会起作用。

接下来让我们瞧瞧ViewGroup不再拦截事件的时候,事件的分发情况,源码如下:

final View[] children = mChildren;

for (int i = childrenCount - 1; i >= 0; i–) { //遍历ViewGroup的所有子元素

final int childIndex = getAndVerifyPreorderedIndex(

childrenCount, i, customOrder);

final View child = getAndVerifyPreorderedView(

preorderedList, children, childIndex);

// If there is a view that has accessibility focus we want it

// to get the event first and if not handled we will perform a

// normal dispatch. We may do a double iteration but this is

// safer given the timeframe.

if (childWithAccessibilityFocus != null) {

if (childWithAccessibilityFocus != child) {

continue;

}

childWithAccessibilityFocus = null;

i = childrenCount - 1;

}

/**

** 判断子元素是否能够接受到点击事件:

** 子元素是否在播动画和点击事件的坐标是否落在子元素的区域内

** 如果某个元素满足这两个条件,则事件交给它来处理

**/

if (!canViewReceivePointerEvents(child)

|| !isTransformedTouchPointInView(x, y, child, null)) {

ev.setTargetAccessibilityFocus(false);

continue;

}

newTouchTarget = getTouchTarget(child);

if (newTouchTarget != null) {

// Child is already receiving touch within its bounds.

// Give it the new pointer in addition to the ones it is handling.

newTouchTarget.pointerIdBits |= idBitsToAssign;

break;

}

resetCancelNextUpFlag(child);

//dispatchTransformedTouchEvent实际调用的是子元素的dispatchTouchEvent方法

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);

//记录ACTION_DOWN事件已经被处理了

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);

}

从上面代码中我们可以知道,不拦截事件时,首先会遍历ViewGroup的所有子元素,然后判断子元素是否能够接受到点击事件。判断的依据是:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果找到一个目标子View来处理事件时,则调用dispatchTransformedTouchEvent()方法。来看看这个方法重要实现逻辑:

if (child == null) {

handled = super.dispatchTouchEvent(event);

} else {

handled = child.dispatchTouchEvent(event);

}

可以发现由于在上面中的child并不等于null,所以将直接调用子元素的dispatchTouchEvent方法,使得事件传递到子View上,然后继续分发。

你以为这样就结束了?答案肯定是没有,从上面源码中我们可以发现当子元素的dispatchTouchEvent返回true后,还有相应操作:

newTouchTarget = addTouchTarget(child, idBitsToAssign);

//记录ACTION_DOWN事件已经被处理了

alreadyDispatchedToNewTouchTarget = true;

break;

这几行代码完成了mFirstTouchTarget的赋值并终止了对子元素的遍历。如果子元素的dispatchTouchEvent返回false,则ViewGroup就会把事件分发给下一个元素(如果还有子元素的话),看到这你也许又纳闷了,mFirstTouchTarget的赋值?怎么没看见mFirstTouchTarget的影子呢,答案其实在addTouchTarget这个方法中:

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {

final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);

target.next = mFirstTouchTarget;

mFirstTouchTarget = target;

return target;

}

这个方法可以看出其实mFirstTouchTarget是一种单链表结构,首先根据坐标点找到了目标子View,然后将子View放在链表头上,从而实现了mFirstTouchTarget!=null。

到这里就完成了ViewGroup一轮的事件分发了,然而还没有结束,如果遍历了所有子元素后事件都没有被合适处理呢?

没有合适处理包括了两种情况:

  • ViewGroup没有子元素

  • 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false(默认是返回true,只有重写View的这个方法或者在onTouchEvent中返回了false)

那么这时候ViewGroup将会自己处理点击事件(当ViewGroup拦截了事件时也是做同样的处理)。

if (mFirstTouchTarget == null) {

// No touch targets so treat this as an ordinary view.

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

}

可以看出这时候还是调用了dispatchTransformedTouchEvent方法,不过这时候第三个参数不是child而是null,所以会调用下面这句代码:

//dispatchTransformedTouchEvent

handled = super.dispatchTouchEvent(event);

super其实就是View中的dispatchTouchEvent方法,所以点击事件开始交由View来处理。

ViewGroup并没有调用onTouchEvent,ViewGroup也没有去重写onTouchEvent

流程图如下:

3. View事件的分发机制


从上面对ViewGroup事件分发机制可知,View事件分发机制是从dispatchTouchEvent开始的。

源码:View#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {

boolean result = false;

//判断窗口是否被遮挡,如果被遮挡则返回false,比如有时候两个View是会重叠的,导致其中一个被遮挡了。

if (onFilterTouchEventForSecurity(event)) {

if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {

result = true;

}

//noinspection SimplifiableIfStatement

ListenerInfo li = mListenerInfo;

//判断是否设置了mOnTouchListener,如果设置了onTouchListener,且onTouch方法返回了ture,

//则result = true

if (li != null && li.mOnTouchListener != null

&& (mViewFlags & ENABLED_MASK) == ENABLED

&& li.mOnTouchListener.onTouch(this, event)) {

result = true;

}

//在result = ture情况下,就不会调用onTouchEvent,可见onTouchListener的优先级高于onTouchEvent

if (!result && onTouchEvent(event)) {

result = true;

}

}

return result;

}

由于View是一个单独元素,没有子元素可以继续向下传递事件,只能自己处理事件,所以代码也会明显减少。从上面的源码中我们可以看到View对点击事件的处理过程,result代表是否消耗该事件,然后进行onTouchListener的判断,如果onTouchListenter中的onTouch方法返回了true,那么就不会再调用onTouchEvent方法,由此可见onTouchListener的优先级高于onTouchEvent。

然后来看看onTouchEvent的实现。

public boolean onTouchEvent(MotionEvent event) {

final float x = event.getX();

final float y = event.getY();

final int viewFlags = mViewFlags;

final int action = event.getAction();

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE

|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)

|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

//不可用状态下点击事件的处理,依然会消耗点击事件

if ((viewFlags & ENABLED_MASK) == DISABLED) {

if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {

setPressed(false);

}

mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

// A disabled view that is clickable still consumes the touch

// events, it just doesn’t respond to them.

return clickable;

}

//如果VIew设置了代理,将会执行代理的onTouchEvent方法

if (mTouchDelegate != null) {

if (mTouchDelegate.onTouchEvent(event)) {

return true;

}

}

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {

switch (action) {

case MotionEvent.ACTION_UP:

boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;

//经过种种判断

performClickInternal();

break;

case MotionEvent.ACTION_DOWN:

break;

case MotionEvent.ACTION_CANCEL:

break;

case MotionEvent.ACTION_MOVE:

break;

}

//若该控件可点击,就一定返回true

return true;

}

//若该控件不可点击,就一定返回false

return false;

}

从上面代码可以知道只要View的CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE有一个为true,那么它就会消耗该事件,不管它是不是DISABLE状态。然后假如控件可点击,就对四种事件类型进行相对应的处理,这里值得一说的是ACTION_UP事件,从源码中可以发现在ACTION_UP事件发生时,会触发performClickInternal方法。这个方法内部实现又是怎样的呢?如下:

private boolean performClickInternal() {

// Must notify autofill manager before performing the click actions to avoid scenarios where

// the app has a click listener that changes the state of views the autofill service might

// be interested on.

notifyAutofillManagerOnClick();

return performClick();

}

我们可以发现最后还是会调用performClick,而在performClick内部中:

public boolean performClick() {

// We still need to call this method to handle the cases where performClick() was called

// externally, instead of through performClickInternal()

notifyAutofillManagerOnClick();

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后

最后这里放上我这段时间复习的资料,这个资料也是偶然一位朋友分享给我的,里面包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

还有 高级架构技术进阶脑图、高级进阶架构资料 帮助大家学习提升进阶,也可以分享给身边好友一起学习。

一起互勉~

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
/13f2cb2e05a14868a3f0fd6ac81d625c.png)

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-etUgP9pJ-1712996959801)]

最后

最后这里放上我这段时间复习的资料,这个资料也是偶然一位朋友分享给我的,里面包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

还有 高级架构技术进阶脑图、高级进阶架构资料 帮助大家学习提升进阶,也可以分享给身边好友一起学习。

[外链图片转存中…(img-ikugFXZV-1712996959802)]

[外链图片转存中…(img-HWOOEQzz-1712996959802)]

一起互勉~

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-98BiV3tD-1712996959802)]

  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值