Android 事件传递机制详解(事件的分发、拦截、处理)

1、事件在Android中的传递顺序

事件在Android的传递顺序:

Activity--> Window-->DecorView --> 布局View

在这里插入图片描述

或者说以上顺序是事件在应用层的传递顺序。如果要说整个事件的传递机制,是Android底层收到触摸屏的事件后,使用socket跨进程通信,用InputDispatcher将事件发送给APP进程,由主线程的Looper去取出消息进行处理。

本文主要分析Android应用层的一个传递过程。

2、事件的传递规则

一个点击事件,或者说触摸事件,被封装为了一个MotionEvent。事件的分发主要由三个重要的方法来完成:

  • 1、分发:dispatchTouchEvent;
  • 2、拦截:onInterceptTouchEvent;
  • 3、处理:onTouchEvent;

如果是ViewGroup容器类view,则以上三个方法都会用到。但是如果是View类型的,不能包含子view,那就没有第二个拦截方法,因为没有子view的话,拦截方法的就是多余的,只有ViewGroup才会有拦截。

2.1 public boolean dispatchTouchEvent(MotionEvent ev)

此方法用来处理事件的分发,当事件传递给当前view时,首先就是通过调用此方法来进行传递的,如果当前view锁包含的子view的dispatchTouchEvent方法或者当前view的onTouchEvent处理了事件, 通常返回true, 表示事件已消费。如果没有处理则返回false。

2.2 public boolean onInterceptTouchEvent(MotionEvent ev)

此方法用来判断某个事件是否需要拦截,如果某个view拦截了此事件,那么同时个事件序列中,此方法不会被再次调用,因为会把当前view赋值给mFirstTouchTarget对象(原本为null),后续父必问判断mFirstTouchTarget != null时,就会去调用它的onTouchEvent方法,交给mFirstTouchTarget处理事件。

2.3 public boolean onTouchEvent(MotionEvent ev)

用来处理事件,如果事件被消耗了,通常就返回true, 如果不做处理,则发挥false,并且在同一个时间序列中,当前view不会再接受到事件。

以上三个方法的关系可以用以下伪代码来表示:

public boolean dispatchTouchEvent(MotionEvent ev) {
	boolean consume = false;
	if(onInterceptTouchEvent(ev)) {
		consume = onTouchEvent(ev);
	} else {
		consume = child.dispatchTouchEvent(ev);
	}
	
	return consume;
}

在这里插入图片描述

根据以上伪代码和图片展示的流程图,我们梳理一下从根ViewGroup(也就是DecorView)往下传递事件的过程:首先,事件产生后,通过调用根ViewGroup的dispatchTouchEvent方法,然后,如果这个ViewGroup要拦截事件, 则它的onInterceptTouchEvent返回true,然后事件交给它的onTouchEvent处理,不再进行传递。如果不拦截,则继续调用子view的dispatchTouchEvent方法,继续往下传递,往下递归,直到最终被处理。如果没有任何一个view处理事件, 则最终又会回调给Activity的onTouchEvent方法,如果Activity也不处理,则此事件结束,且没做任何处理。

3、事件传递的源码分析

3.1 Activity对事件的传递

前面已经讲到,APP层的事件传递是从Activity看是的,首先就是调用Activity的dispatchTouchEvent, 源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();//注释1
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;//注释2
    }
    return onTouchEvent(ev);//注释3
}

注释1 是Activity的方法,如果当事件开始传递前,我们需要额外处理一些操作,可以咋onUserInteraction()中进行处理,这本身是一个空方法;

注释2 getWindow返回的就是DecorView对象,相当于最顶层的ViewGroup,然后就开始往下层的view传递, 如果事件在view的传递中被处理,则返回true,否则就调用注释3的代码;

注释3:如果事件在view的传递中未被处理,则调用Activity自己的onTouchEvent方法。

3.2 View对事件的传递

在上面3.1小节讲到, 通过getWindow().superDispatchTouchEvent(ev),把事件传递给了DecorView。其实在我们做APP开发时,有时就会用到(ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0) 这种方式来获取我们给Activity的布局view。

DecorView以前是PhoneWindow的一个内部类,不过现在已经独立成单独的一个类了,查看它的superDispatchTouchEvent方法代码

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

发现直接使用的父类,也就是ViewGroup的dispatchTouchEvent方法,这和我们前面的分析就一一对上了,我们进入ViewGroup的dispatchTouchEvent方法,代码很长,先看这一段代码:

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;
    }
} else {
    intercepted = true;
}

这一段代码是ViewGroup用来判断是否需要拦截事件,如果为ACTION_DOWN或者mFirstTouchTarget != null,则进入判断。mFirstTouchTarget是个什么东东呢? 从后面代码可以看出,它是ViewGroup的子view,即当事件序列已经由子view进行处理的话,则父view不会再进行拦截。

另外就是FLAG_DISALLOW_INTERCEPT这个标记位,一般用于子view中,如果子view拦截了事件,则这个标记位就会设置为1,以便当前时间序列所有事件都将交给子view,当时当再次传来ACTION_DOWN事件时(也就是一个新的时间序列),这个标记位就会被重置为0。

也就是在上一段代码的前一个位置,有这么一段代码:

if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();//如果为ACTION_DOWN,则重置标记位
}

所以以上两段代码可以得出:

  • 1、在一个完整的时间序列中,当某个子View拦截ACTION_DOWN后,后续所有事件都会交给它处理,而父view也不会再去调用onInterceptTouchEvent方法;
  • 2、当新来到一个事件序列时(也就是以ACTION_DOWN开头),会重置mFirstTouchTarget以及标记位FLAG_DISALLOW_INTERCEPT等;

接着往下看dispatchTouchEvent的其他代码段,当ViewGroup不拦截事件时的处理,那么事件就会分发给它的子view:

final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;
    }

    if (!canViewReceivePointerEvents(child) //注释3.1
            || !isTransformedTouchPointInView(x, y, child, null)) {//注释3.2
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//注释3.3
        // 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);//注释3.4
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }

上述代码中:

  • 注释3.1: 用来判断view是否可见,是否在播放动画, 因为不可见或播动画都无法获得焦点;
  • 注释3.2: 用来判断子view是否在点击区域中,没在点击区域,自然不用接收事件;
  • 注释3.3: 实际上dispatchTransformedTouchEvent方法就是调用子view的dispatchTouchEvent方法,继续让子view分发事件;
  • 注释3.4: 完成对mFirstTouchTarget的赋值;

这样,就完成了整个View树对事件的传递。

再往后看:

if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);//注释3.5
} else {
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
       ....
    }
}

如果mFirstTouchTarget == null, 则没有子view处理事件,则交给父view处理, 否则就去子view处理事件。

到此,应用层的整个事件传递机制就分析完了。

如果您觉得内容还可以,麻烦点一个赞吧~~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值