最近实验室做的一个东西会向 Android 应用快速注入一系列触屏事件,模拟用户的点击。但是我们发现当按下和弹起的 MotionEvent 之间时间间隔过小(例如小于 100ms)时,会导致该事件被忽略。看了代码后发现 Android 中按下和弹起之间时间间隔要在 115ms 以上才会被认为是一个点击事件。这里结合 Android 的源码分析一下点击事件的产生过程。

当用户触摸屏幕后,底层驱动会将这一事件包装成一个 MotionEvent 对象,传递给 WindowsManagerService。后者维护了一个事件队列,会将事件分发给正获得焦点的控件,其中,dispatchPointer 方法负责将触屏事件发送给当前活动窗口(Window)。每一个 Window 都有一个 ViewRoot 实例接收来自 WindowManagerService 的消息。活动窗口的 ViewRoot 在 handleMessage 方法中接收了这一触屏事件后,就会调用最上层的 View 对象的 dispatchTouchEvent 方法。和大多数图形系统一样,Android 中的 view 也是以树状结构组织的,事件会从最上层依次向下层传递,直到某一结点能处理这一事件为止。

View 对象的 onTouchEvent 负责处理触屏事件,这里也是点击事件产生的地方,下面这段代码创建了一个点击事件,并把它添加到事件链中,在触屏事件处理完成后再执行。

// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
    // Use a Runnable and post this rather than calling
    // performClick directly. This lets other visual state
    // of the view update before click actions start.
    if (mPerformClick == null) {
        mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) {
        performClick();
    }
}

而在前面的判断条件可以看到,这一点击事件创建的前提是 mPrivateFlags 的 PRESSED 位和 PREPRESSED 位同时被置上 (View.java):

boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
    ...
}

接着看触屏按下事件的处理 (View.java):

case MotionEvent.ACTION_DOWN:
    if (mPendingCheckForTap == null) {
        mPendingCheckForTap = new CheckForTap();
    }
    mPrivateFlags |= PREPRESSED;
    mHasPerformedLongPress = false;
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
    break;

mPrivateFlags 的 PREPRESSED 位会被立即置上,另外 postDelayed 方法会在经过 ViewConfiguration.getTapTimeout() 时间后,执行一个 CheckForTap 的实例,看来这就是我们要找的东西了。CheckForTap 是一个很简单的 Runnable 对象,它所做的就是把 mPrivateFlags 中除了 PREPRESSED 的位都清零,再把 PRESSED 位置上,并注册一个长按事件的检测对象。而 ViewConfiguration.getTapTimeout() 的默认值便是 115ms。注释中还说明了这个延时的用意在于区分滑动操作和点击操作,如果触屏区域在这一时间内没有移动,这个操作才会被识别成点击。