Android Touch Event 分发和处理

复习一下 Android 应用中 TouchEvent 的处理流程。

让人迷糊的 onInterceptTouchEvent

很多年前刚开始入门 Android 应用开发时,一直也没完全弄搞明白应用中是如何处理 TouchEvent 的。大部分时候这并不影响我们开发常规功能,但遇到嵌套滚动等稍复杂一些的场景时,较为深入地理解 TouchEvent 的处理流程是必要的。

不能完全搞懂这块知识,是可以理解的。因为相关的概念看起来有些多,比如 WindowActivityViewViewGroupdipatchTouchEvent()onInterceptTouchEvent() 等等,官方文档其实也没有解释得特别明白。所以我们总是写出一堆相关的 bug 后才能逐渐加深理解。

不过 StackOverflow 上总能找到一些神奇的帖子和神奇的图片,简简单单就把问题说明白。请看下图:

图片来自 How are Android touch events delivered? - Stack Overflow。注意,这个图特别清晰地展示了原理,但是它有些小瑕疵

先看三条最基本的:

  • 有两个流,分别是 Notify 流Handle 流,TouchEvent 通常会沿着这两个流垂直流动
  • Notify 流中 TouchEvent 从上往下流动, Activity 最早获得 TouchEvent ,它一直流动到 View,View 最晚获得这个事件
  • Handle 流中 TouchEvent 从下往上流动, View 拥有最高的优先权来处理 TouchEvent ,它一直流动到优先权最低的 Activity

再看另外两种情况:

  • 水平方向上还一有类流,可以称之为 Intercept 流。特殊情况下,TouchEvent 会沿着 Intercept 流水平流动。比如:
    • ViewGroup onInterceptTouchEvent() 返回 true
    • 或者 View 上设置了 OnTouchListener
  • 最后,如果 onTouch() 返回 true,则认为 TouchEvent 被消费掉了,它不再沿着任意一个流来流动

这样来理解是不是容易多了。

我们什么时候会去覆盖 ViewGroup 的 onInterceptTouchEvent() 呢?最常见的一种情况是让 ViewGroup 处理某一类型的 TouchEvent,而其他类型的则交由它的子 View 来处理。比方说 ScrollView 中有一个 Button,那么 ScrollView 需要处理滚动事件,而 Button 需要处理点击事件,这个场景就要覆盖 onInterceptTouchEvent() 了。

示例代码

对着上面的图写了个 Demo,这里的 TeView 对应于图中的 View

TeView 是一个 TextView 时,日志如下:

  • Notify 流中 TouchEvent 从上往下流动, TeActivity 最早获得 TouchEvent ,TeView 最晚获得
  • TeView 没有消费掉这个 TouchEvent,所以它又沿着 Handle 流回到了 TeActivity

但是当 TeView 是一个 Button 时,情况就有所不同了:

  • Notify 流中 TouchEvent 从上往下流动, TeActivity 最早获得 TouchEvent ,TeView 最晚获得。这一条没变
  • TeView 是个 Button,它消费掉 TouchEvent,之后事件不再沿着任意一个流流动

以上只验证了两种简单的情况。我们也可以调整代码验证更多的情况,比如让 TeViewGroupB.onInterceptTouchEvent() 返回 true。这里不再展开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// touchevent.kt
package com.example.myapplication.touchevent

var sEventTime = 0L
var sEventHandlerSeq = 0

private fun trackEvent(e: MotionEvent?): String {
if (sEventTime == 0L || sEventTime != e?.eventTime) {
sEventTime = e?.eventTime ?: 0
sEventHandlerSeq = 0
} else {
sEventHandlerSeq++
}
return "#$sEventHandlerSeq"
}

object TeViewTouchListener : OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
Timber.i("%s eventHandlerSeq = %s", object {}.javaClass.enclosingMethod?.name, trackEvent(event))
return false
}

}

//class TeView(context: Context, attributeSet: AttributeSet? = null) : androidx.appcompat.widget.AppCompatButton(context, attributeSet) {
class TeView(context: Context, attributeSet: AttributeSet? = null) : androidx.appcompat.widget.AppCompatTextView(context, attributeSet) {

val isBtn = Button::class.java.isInstance(this)

override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
val me = if (isBtn) {
"I'm a button"
} else {
"I'm NOT a button"
}
Timber.i(
"%s %s eventHandlerSeq = %s",
me,
object {}.javaClass.enclosingMethod?.name,
trackEvent(event)
)
return super.dispatchTouchEvent(event)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
val me = if (isBtn) {
"I'm a button"
} else {
"I'm NOT a button"
}
Timber.i(
"%s %s eventHandlerSeq = %s",
me,
object {}.javaClass.enclosingMethod?.name,
trackEvent(event)
)
return super.onTouchEvent(event)
}

}

class TeViewGroupB(context: Context, attributeSet: AttributeSet? = null) : FrameLayout(context, attributeSet) {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Timber.i("%s eventHandlerSeq = %s", object {}.javaClass.enclosingMethod?.name, trackEvent(ev))
return super.dispatchTouchEvent(ev)
}

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
Timber.i("%s eventHandlerSeq = %s", object {}.javaClass.enclosingMethod?.name, trackEvent(ev))
return super.onInterceptTouchEvent(ev)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
Timber.i("%s eventHandlerSeq = %s", object {}.javaClass.enclosingMethod?.name, trackEvent(event))
return super.onTouchEvent(event)
}
}

class TeViewGroupA(context: Context, attributeSet: AttributeSet? = null) : FrameLayout(context, attributeSet) {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Timber.i("%s eventHandlerSeq = %s", object {}.javaClass.enclosingMethod?.name, trackEvent(ev))
return super.dispatchTouchEvent(ev)
}

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
Timber.i("%s eventHandlerSeq = %s", object {}.javaClass.enclosingMethod?.name, trackEvent(ev))
return super.onInterceptTouchEvent(ev)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
Timber.i("%s eventHandlerSeq = %s", object {}.javaClass.enclosingMethod?.name, trackEvent(event))
return super.onTouchEvent(event)
}
}

class TeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vga = TeViewGroupA(this)
val vgb = TeViewGroupB(this)
val v = TeView(this).apply {
text = "click me"
}

vgb.addView(v)
vga.addView(vgb)
setContentView(vga)

}

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Timber.i("%s eventHandlerSeq = %s", object {}.javaClass.enclosingMethod?.name, trackEvent(ev))
return super.dispatchTouchEvent(ev)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
Timber.i("%s eventHandlerSeq = %s", object {}.javaClass.enclosingMethod?.name, trackEvent(event))
return super.onTouchEvent(event)
}

companion object {
@JvmStatic
fun start(context: Context) {
val starter = Intent(context, TeActivity::class.java)
context.startActivity(starter)
}
}
}

另一种理解

另外有张图,也特别有助于理解。图片来源于 Android UI Internal : Pipeline of View’s Touch Event Handling
,可惜原文已经打不开了。

  • Notify 流:1 -> 2 -> 3 -> 4 -> 5
  • Handle 流:6 -> 7 -> 8 -> 9 -> 10