Android中的事件分发拦截机制

Android的UI交互是由事件驱动的,每一个App启动后,其Main Thread都会被Looper.loop()阻塞,它会不断检查MessageQueue中是否存在新的由Handler在不同Thread中抛出的事件。这些事件会被LooperMain Thread中调用对应HandlerhandleMessage()来处理,而处理这些事件的耗时过程则可以通过线程管理切换到工作线程中执行,然后再切回Main Thread更新UI。

正确使用这种机制可以保证事件被及时分发处理,并能在UI交互上随之作出响应。这篇文章谈论的事件指的是触控事件MotionEvent

事件序列

一次触控行为是由一系列MotionEvent组成的,以手指接触屏幕的ACTION_DOWN起始,手指离开屏幕的ACTION_UP结束,中间则是连续的ACTION_MOVE事件,它们被称为一次触控的事件序列

事件的分发轨迹

Activity -> PhoneWindow -> DecorView -> ViewGroup -> View

MotionEvent事件产生后,会首先由Activity传递到Window,然后通过DecorView进入View树,至此便会从整个View树中的根View开始,层层向下传递,并监听下层View对事件的处理结果。可以简单的理解为,在View的树形结构中,事件层层向下分发,而事件的处理结果层层向上报告。如果上层View发现事件已经下发,但子View没有处理这个事件,则它会自己去处理这个事件,并把处理结果上报,这种逻辑也是符合常理的。

需要定义一下事件处理这个词的含义,View从上级接收事件,该上级会通过这个View的某些方法返回的特定值来判断其对所接收事件的“态度”。比如onTouchEvent()返回true,即认为这个事件已经被处理了,如果返回false,则说明View收到了这个事件,也可能确实执行了某些操作,但它的返回值却向上报告称它没有处理这个事件,那么上级也会认为事件没有被处理。

分发、拦截、处理

ViewGroup有3个重要方法:

// ViewGroup

// 分发事件
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    val result = super.dispatchTouchEvent(ev)
    // 返回true表示事件下发后被子View处理,false表示事件下发后没有被子View处理
    return result
}
	
// 拦截事件
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    val result = super.onInterceptTouchEvent(ev)
    // 返回true表示拦截事件,即事件不会再向下分发,调用本层的onTouchEvent()处理
    return result
}
	
// 处理事件
override fun onTouchEvent(event: MotionEvent?): Boolean {
    val result = super.onTouchEvent(event)
    // 返回true表示事件被处理,false表示事件未被处理
    return result
}

对于View,因为已经没有可以向下传递的子View,也就没必要判断是否要拦截,所以没有onInterceptTouchEvent()方法:

// View

// 分发事件
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    val result = super.dispatchTouchEvent(ev)
    // 返回true表示事件被本View处理,false表示事件没有被本View处理
    return result
}
	
// 处理事件
override fun onTouchEvent(event: MotionEvent?): Boolean {
    val result = super.onTouchEvent(event)
    // 返回true表示事件被处理,false表示事件未被处理
    return result
}

事件在ViewGroup中的处理分为3步:

  • 判断是否拦截
  • 处理事件(下发或本层处理)
  • 向上报告处理结果

事件分发、拦截、处理的3个关键方法:

  • dispatchTouchEvent()
  • onInteceptTouchEvent()
  • onTouchEvent()

它们的执行逻辑可以用下面一段伪代码描述:

// 事件到达ViewGroup,首先会调用其dispatchTouchEvent()方法
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    var result = false
    // 判断是否在本层拦截事件
    if (onInteceptTouchEvent(ev)) {
        // 拦截事件,调用本层onTouchEvent()方法处理事件,并将其返回值作为事件处理结果
        result = onTouchEvent(ev)
    } else {
        // 不拦截事件,查找可以处理事件的子View
        val item = findItem()
        if(item == null) {
            // 没有找到可以处理事件的子View,调用本层的onTouchEvent()方法处理事件
            result = onTouchEvent(ev)
        } else {
            // 找到了可以处理事件的子View,调用其dispatchTouchEvent()方法传递事件,并将其返回值作为事件执行结果
            result = item.dispatchTouchEvent(ev)
        }
    }
    return result
}

真实的事件分发拦截逻辑十分复杂,涉及到对ACTION_DOWN这一特殊事件的判断和一些与拦截有关的标志位处理,比如FLAG_DISALLOW_INTERCEPT。但如果尝试阅读源码,参考一些技术文档,再实机打印一些Log,还是可以理清这些逻辑的,下面的内容就是我对它的描述:

首先深吸一口气。。

事件产生后,ActivitydispatchTouchEvent()首先被调用,它会调用PhoneWindowsuperDispatchTouchEvent(),如果返回false,说明PhoneWindow及该Window上的View都没有处理这个事件,将调用Activity自己的onTouchEvent()处理。

PhoneWindowsuperDispatchTouchEvent()被调用时,会调用View树根ViewDecorViewdispatchTouchEvent()

DecorView收到事件即说明事件已经正式进入View树

DecorViewdispatchTouchEvent()被调用时,会首先判断要不要拦截这个事件:

  • 如果是ACTION_DOWN,即它是一个事件序列的开始,则调用本ViewonInterceptTouchEvent()判断是否拦截。
  • 如果不是ACTION_DOWN,即它不是一个事件序列的开始,有两种情况:
    • 如果此事件序列之前的事件已经被处理了,则根据FLAG_DISALLOW_INTERCEPT标志位:
      • 如果标志位被设置为不允许拦截,则不拦截。
      • 如果标志位没有被设置,则调用本ViewonInterceptTouchEvent()判断是否拦截。
    • 如果此事件序列之前的事件没有被处理,则拦截事件,禁止向下分发。所以这会导致一个现象,如果一个事件序列的某个事件没有被View处理,那么此事件序列的所有后续事件都会被上层拦截,不会再分发给该View

如果拦截事件,则调用本ViewonTouchEvent()来处理事件,并将其返回值作为事件是否被处理的依据。

如果不拦截事件,则根据事件坐标判断它属于哪个子View,调用该ViewdispatchTouchEvent()把事件向下传递,并将其返回值作为判断这个事件是否被处理的依据:

  • 如果事件被处理,则将结果上报,作为本ViewdispatchTouchEvent()的返回。
  • 如果事件没有被处理,则调用本ViewonTouchEvent()来处理,并将结果上报。
  • 如果没有合适的下层View处理事件,则调用本ViewonTouchEvent()处理,并将结果上报。

OnTouchEventListener

View使用onTouchEvent()方法来处理事件,但如果从外界设置了OnTouchEventListener监听器,则要处理事件时首先调用OnTouchEventListeneronTouch()来处理事件,如果返回true则事件已被处理,不再调用ViewonTouchEvent()。如果返回false则调用ViewonTouchEvent()来处理事件,并将其返回值作为事件是否被处理的结果上报。

即从外部设置的OnTouchEventListener的优先级是高于ViewonTouchEvent()的。

一些结论

理解Android中事件的分发、拦截、处理机制非常重要,这也是自定义View和处理滑动冲突的知识基础,不过日常开发中只需要记住一些关键结论就足以应对大部分场景的需求了。

  • 事件的分发是层层向下的,事件的处理是层层向上的,当子View没有处理事件时,父ViewonTouchEvent()就会被调用,并把其返回值作为是否处理了事件的结果向更上层汇报。
  • 从一个层级分发下去的事件,如果没有被处理,则该事件序列的后续事件在这个层级就会被拦截,不会再向下分发了。
  • ACTION_DOWN是一个事件序列的开始,如果一个View接收了ACTION_DOWN事件,或一个ViewGroup拦截了ACTION_DOWN事件,而没有处理,则此事件序列的后续事件就会在上层被直接拦截,不会再下发到这里。所以,如果要监听整个事件序列,ACTION_DOWN必须被处理。
  • ViewGroup一旦决定拦截事件,无论是否处理,此事件序列的后续事件都会跳过此ViewGrouponInterceptTouchEvent()而直接执行onTouchEvent(),即后续事件会在本层被直接拦截,不会再向下分发。
  • 事件分发是层层向下传递的,下层的某个ViewViewGroup如果处理了一个事件,该事件序列的后续事件依然是在上面层层传递下来的,每一层都可以拦截,而一旦拦截,后续事件就不会向下再传递过来了。如果不想让父View拦截,可以设置它的FLAG_DISALLOW_INTERCEPT标志位,让父View在这种情况下不拦截事件。
  • 如果View处理了ACTION_DOWN事件,则此事件序列的后续事件都可以被发送到该View,除非在上层被拦截。对于此事件序列的后续事件,如果本View没有处理,则上层ViewGrouponTouchEvent()也不会被调用,当前View依然可以正常的收到后续事件,这些事件最终会被传递给Activity处理。
  • 如果事件到达一个ViewGroup,则onDispatchTouchEvent()是一定会被执行的,而onInterceptTouchEvent()则不一定会被执行,某些情况下,无需调用此方法就可以判断是否要拦截这个事件 。
    • ACTION_DOWN事件,一定调用onInterceptTouchEvent()
    • ACTION_DOWN事件,此序列前事件未被处理过,则直接拦截,不调用onInterceptTouchEvent()
    • ACTION_DOWN事件,此序列前事件被处理过,但ViewGroupFLAG_DISALLOW_INTERCEPT标志位被设置,即不允许拦截事件,则不拦截,也不调用onInterceptTouchEvent()
  • ViewGrouponTouchEvent()默认返回false,即不处理事件。
  • ViewonTouchEvent()的返回值与具体的View有关,只要clickablelongClickable属性有一个是true,则onTouchEvent()返回true,即处理事件。

其实工作一年来,我的多数精力都被集中在业务逻辑的实现上,已经很久没有认真复习过View了,所以最近一直在整理过去的学习笔记,把这些原生View的运行机制清晰的表达出来,然后才可以放心的去接触更多新玩具。

arrow_upward