深入理解Android之Touch事件的分发

144
作者 absfree
2016.10.30 22:43* 字数 1108

概述

当时输入设备(如触摸屏,键盘等)可用时,Linux Kernel会在/dev/input/下创建名为event0~eventN的设备节点; 当输入设备不可用时,会将相应的设备节点删除。

当用户操作输入设备时,Linux Kernel会收到相应的硬件中断,然后会将中断加工成原始输入事件(raw input event),并写入到设备节点中。而后在用户空间就可以通过read()函数读取事件数据了。

Android输入系统会监控/dev/input/下的所有设备节点,当某个结点有数据可读时,将数据读出并进行一系列处理,然后在当前系统中的所有窗口(Window)中寻找合适的接收者,并把事件派发给它。

具体来说,Linux Kernel将raw input event写入到设备节点后,InputReader会通过EventHub将原始事件读取出来并翻译加工为Android输入事件,而后把它交给InputDispatcher。InputDispatcher根据WMS(WindowManagerService)提供的窗口信息将事件传递给合适的窗口,若窗口为壁纸/SurfaceView等,则到了终点;否则会由该Window的ViewRoot继续分发到合适的View。

使用adb shell的getevent命令可从设备节点读出事件信息,而sendevent命令可向设备节点写入事件。

Android输入事件的派发流程

当原始事件进入InputDispatcher时,已被加工为KeyEvent、MotionEvent(或SwitchEvent)。而后InputDispatcher会对事件进行进一步分发。

将事件放入派发队列

将输入事件加入到派发队列后,会依次执行以下步骤:

  • 派发线程开始派发事件;
  • 锁定目标窗口,然后向目标Window发送事件。
  • InputDispatcher通过InputChannel将event发给Window(InputDispatcher运行于system_server进程中,Window运行于应用进程,两者通过InputChannel通信。);
  • Window端的Looper被唤醒,从InputChannel中读取一个InputEvent,而后调用onInputEvent(ev)。具体来说是调用ViewRootImpl的mInputEventReceiver的成员的onInputEvent()方法。
  • 调用doProcessInputEvents()
  • 调用deliverInputEvent()

deliverInputEvent()方法

这个方法内部会调用processPointerEvent()方法对输入事件进行处理,而processPointerEvent内部会通过mView.dispatchPointerEvent()方法来进一步对输入事件分发。这里的mView即为Window的decorView,decorView的概念如下图所示:



这里我们只分析Touch事件的情况,对于Touch事件,会进一步调用mView.dispatchTouchEvent()方法来对其进行分发。

Touch事件的分发

mView.dispatchTouchEvent()

我们可以把decorView看作是Window的Callback的实现者(即Activity)在控件树(ViewTree)中安插的一个间谍,decorView所接收到的事件,Activity也都会接收到。实际上,Activity的dispatchTouchEvent()方法会先于控件树中的任一个子View被调用。

Activity.dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {
  if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    onUserInteraction();
  } 
  if (getWindow().superDispatchTouchEvent(ev)) {
    return true; 
  } 
  return onTouchEvent(ev);
}

我们可以看到,这个方法中调用了Window的superDispatchTouchEvent()方法,这个方法会把Touch事件传递给decorView,从而开始在窗口的控件树(ViewTree)中进行分发。

ViewGroup.dispatchTouchEvent()

decorView本质上是一个ViewGroup,因此我们来看一下ViewGroup的dispatchTouchEvent()方法。

public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;
    ......
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    // 1. 处理初始的ACTION_DOWN
    if (actionMasked == MotionEvent.ACTION_DOWN) {

       // 把ACTION_DOWN作为一个Touch手势(Touch事件序列)的始点,清除之前的手势状态;
       // 将mFirstTouchTarget重置为null(mFirstTouchTarget为最近一次对处理Touch事件的View)
        cancelAndClearTouchTargets(ev); 
       // 重置FLAG_DISALLOW_INTERCEPT,该标记为true则表示禁止拦截本次事件
        resetTouchState();
    }

    // 2. 判断是否拦截此次的Touch事件
    final boolean intercepted;

    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        // 本次事件是ACTION_DOWN,或者mFirstTouchTarget不为null(已经找到能够接收该Touch事件的目标组件)
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

        // 判断禁止拦截的FLAG,因为子View可以通过requestDisallowInterceptTouchEvent()
        // 方法可以禁止父View执行“是否需要拦截”的判断
        if (!disallowIntercept) {   
            // 禁止拦截的FLAG为false,说明可以执行拦截判断,则执行此ViewGroup的onInterceptTouchEvent方法
            intercepted = onInterceptTouchEvent(ev);
            // 此方法默认返回false,如果想修改默认的行为,需要override此方法,修改返回值。
            ev.setAction(action);

        } else {

            // 禁止拦截的FLAG为ture,设置intercepted为false
            intercepted = false;

        }

    } else {
       // 当不是ACTION_DOWN事件并且mFirstTouchTarget为null时,
       // 这个ViewGroup应该继续执行拦截的操作。
        intercepted = true;
    }


    // 通过前面的逻辑处理,得到了是否需要进行拦截的变量值
    final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;

    final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;

    TouchTarget newTouchTarget = null;

    boolean alreadyDispatchedToNewTouchTarget = false;

    if (!canceled && !intercepted) {
        // 不是ACTION_CANCEL且不拦截
        if (actionMasked == MotionEvent.ACTION_DOWN) {

           // 在ACTION_DOWN时去寻找这次DOWN事件新出现的TouchTarget
            final int actionIndex = ev.getActionIndex(); // always 0 for down
            .....
            final int childrenCount = mChildrenCount;

            if (newTouchTarget == null && childrenCount != 0) {
                // 根据触摸的坐标寻找能够接收这个事件的子组件
                final float x = ev.getX(actionIndex);
                final float y = ev.getY(actionIndex);
                final View[] children = mChildren;

                // 逆序遍历所有子组
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = i;
                    final View child = children[childIndex];

                    // 寻找可接收这个事件(可见性为VISIBLE或包含动画)并且组件区域内包含点击坐标的子View
                    if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    }
                    // 找到了符合条件的子View,赋值给newTouchTarget
                    newTouchTarget = getTouchTarget(child);

                    ......

                    // 把ACTION_DOWN事件传递给子View进行处理
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

                        // 如果此子View消费了这个touch事件
                        . . .

                       // 则把mFirstTouchTarget赋值为newTouchTarget,此子View成为新的touch事件的起点
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }
                }
            }

            ......

        }
    }

    // 经过前面对ACTION_DOWN的处理后,有两种情况。
    if (mFirstTouchTarget == null) {
        // 情况1: 没有找到能够消费Touch事件的子View或者是touch事件被拦截了,此时mFirstTouchTarget为null。
        // 会调用ViewGroup的dispatchTransformedTouchEvent方法对Touch事件做处理,
        // 这里传入该方法的第三个参数为null,表示没有子View能消费此事件,
        // 则会通过调用super.dispatchOnTouchEvent()把事件回递给父ViewGroup进行处理,
        // 实际上会调用父View的onTouchEvent()方法来处理
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
    } else {

        // 情况2:找到了能够消费touch事件的子组件,那么后续的touch事件都可以传递到子View
        TouchTarget target = mFirstTouchTarget;

       // 简单起见,省略了Target List的概念,具体可参考源码
        while (target != null) {
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {

                // 如果前面找到了能消费ACTION_DOWN事件的View并消费掉了ACTION_DOWN事件,这里直接返回true
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;

                // 对于非ACTION_DOWN事件,则继续传递给目标子View进行处理,无需再判断是否进行拦截
                if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                    handled = true;
                }
                // 如果ACTION_DOWN时没有被拦截,而后面的Touch事件被拦截,则需要发送ACTION_CANCEL给之前处理ACTION_DOWN的View
                ......
            }
        }

    }

    if (canceled || actionMasked == MotionEvent.ACTION_UP) {
        // 如果是ACTION_CANCEL或者ACTION_UP,重置FLAG_DISALLOW_INTERCEPT,mFirstTouchTarget赋为null
        resetTouchState();
    }
    ......
    return handled;
}

View对Touch事件的处理

在以上ViewGroup对Touch事件分发的源码中,我们多次看到了ViewGroup的dispatchTransformedEvent()方法,这个方法中调用了View的dispatchTouchEvent()方法来对Touch事件进行处理:

View.dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    . . .


    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        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;
}

若设置了onTouchListener,则会先调用onTouch()方法,此方法返回false才会调用onTouchEvent()方法。

View.onTouchEvent()

只要View的CLICKABLE属性和LONG_CLICKABLE属性有一个为true(View的CLICKABLE属性和具体View有关,LONG_CLICKABLE属性默认为false,setOnClikListener和setOnLongClickListener会分别自动将以上俩属性设为true),那么这个View就会消耗这个touch事件,即使这个View处于DISABLED状态。若当前事件是ACTION_UP,还会调用performClick()方法,该View若设置了OnClickListener,则performClick方法会在其内部调用onClick()方法.

至此,Touch事件的分发流程就分析完了。

参考资料

  1. 《深入理解Android(卷三)》
  2. Touch事件分发响应机制

长按或扫描二维码关注我们,让您利用每天等地铁的时间就能学会怎样写出优质app。


Android通俗说