如果 不是重叠的 View,那就不用太关心“谁在上层、谁后绘制”。
因为 Android 分发事件时,核心逻辑是:
先看点击坐标落在哪个 View 的范围内,再把事件交给它。
1. 不重叠的 View,事件怎么分发?
比如一个父布局里有两个按钮:
+---------------------------+
| |
| +---------+ +---------+ |
| | ButtonA | | ButtonB | |
| +---------+ +---------+ |
| |
+---------------------------+
它们不重叠。
你点左边:
点击坐标在 ButtonA 范围内
那么事件会给 ButtonA。
你点右边:
点击坐标在 ButtonB 范围内
那么事件会给 ButtonB。
这种情况下,谁先绘制、谁后绘制,基本不重要。
2. Android 事件分发整体流程
Android 的触摸事件大概从上往下传:
Activity
↓
Window
↓
DecorView
↓
ViewGroup
↓
子 View
我们开发中最常关注的是这三个方法:
dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()
分别属于:
| 方法 | 所属对象 | 作用 |
|---|---|---|
dispatchTouchEvent() |
Activity / ViewGroup / View | 分发事件 |
onInterceptTouchEvent() |
ViewGroup | 是否拦截事件 |
onTouchEvent() |
View | 处理事件 |
3. ViewGroup 和 View 的区别
ViewGroup
比如:
LinearLayoutFrameLayoutConstraintLayoutRecyclerViewScrollView
它们可以包含子 View。
所以 ViewGroup 有两个选择:
1. 把事件交给子 View
2. 自己处理事件
因此 ViewGroup 有:
dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()
View
比如:
TextViewButtonImageView
普通 View 没有子 View。
所以它只能:
自己处理事件
View 没有 onInterceptTouchEvent()。
它主要有:
dispatchTouchEvent()
onTouchEvent()
4. 一个简单例子
布局:
<LinearLayout
android:id="@+id/parent"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/buttonA"
android:layout_width="200dp"
android:layout_height="100dp"
android:text="A" />
<Button
android:id="@+id/buttonB"
android:layout_width="200dp"
android:layout_height="100dp"
android:text="B" />
</LinearLayout>
显示效果:
+------------------+
| Button A |
+------------------+
| Button B |
+------------------+
你点击 ButtonA:
Activity.dispatchTouchEvent()
↓
LinearLayout.dispatchTouchEvent()
↓
LinearLayout 判断点击坐标在 ButtonA 范围内
↓
ButtonA.dispatchTouchEvent()
↓
ButtonA.onTouchEvent()
↓
触发 ButtonA 的点击
你点击 ButtonB:
Activity.dispatchTouchEvent()
↓
LinearLayout.dispatchTouchEvent()
↓
LinearLayout 判断点击坐标在 ButtonB 范围内
↓
ButtonB.dispatchTouchEvent()
↓
ButtonB.onTouchEvent()
↓
触发 ButtonB 的点击
5. 坐标判断才是关键
对于不重叠的 View,Android 会根据触摸点坐标判断事件属于谁。
比如:
ButtonA 范围:
x = 0 ~ 200
y = 0 ~ 100
ButtonB 范围:
x = 0 ~ 200
y = 100 ~ 200
如果你点击:
x = 50
y = 50
这个点在 ButtonA 里面,所以给 ButtonA。
如果你点击:
x = 50
y = 150
这个点在 ButtonB 里面,所以给 ButtonB。
如果你点击:
x = 250
y = 50
不在 ButtonA,也不在 ButtonB,那么事件可能由父布局自己处理。
6. dispatchTouchEvent 是什么?
dispatchTouchEvent() 的意思是:
这个事件来了,我要不要分发给别人,还是自己处理?
在 Activity 中:
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
return super.dispatchTouchEvent(ev)
}
在 ViewGroup 中:
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
return super.dispatchTouchEvent(ev)
}
在 View 中:
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
return super.dispatchTouchEvent(ev)
}
它的返回值是 Boolean:
true 表示事件被消费了
false 表示事件没有被消费
7. onInterceptTouchEvent 是什么?
onInterceptTouchEvent() 只有 ViewGroup 有。
它的作用是:
父布局要不要拦截这个事件,不让子 View 处理。
比如一个 ScrollView 里面有一个 Button。
ScrollView
↓
Button
当你手指滑动时,ScrollView 可能会拦截事件,因为它要滚动。
当你只是点击时,它可能不拦截,让 Button 处理点击。
伪代码理解:
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return false
}
表示:
我不拦截,交给子 View
如果返回:
return true
表示:
我拦截,子 View 不处理,我自己处理
8. onTouchEvent 是什么?
onTouchEvent() 是真正处理触摸事件的方法。
例如:
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
println("手指按下")
return true
}
MotionEvent.ACTION_MOVE -> {
println("手指移动")
return true
}
MotionEvent.ACTION_UP -> {
println("手指抬起")
return true
}
}
return super.onTouchEvent(event)
}
如果返回 true:
我处理了这个事件
如果返回 false:
我不处理这个事件
9. 一次完整点击事件包含什么?
一次点击不是只有一个事件。
它一般是:
ACTION_DOWN
ACTION_UP
如果有移动,就是:
ACTION_DOWN
ACTION_MOVE
ACTION_MOVE
ACTION_UP
比如你手指按下、移动、抬起:
手指按下:ACTION_DOWN
手指移动:ACTION_MOVE
手指抬起:ACTION_UP
Android 分发事件时,是一整组事件一起考虑的。
10. 一个非常重要的规则
谁消费了
ACTION_DOWN,后续的MOVE和UP通常就继续给谁。
比如你点击 ButtonA:
ACTION_DOWN 给了 ButtonA,并且 ButtonA 消费了
那么后面的:
ACTION_MOVE
ACTION_UP
也会继续给 ButtonA。
即使你的手指后来移出了 ButtonA 的范围,也通常还是由 ButtonA 接收后续事件。
11. 不重叠 View 的简单流程
假设布局是:
Parent
├── ButtonA
└── ButtonB
点击 ButtonA 时:
事件来到 Parent
↓
Parent 是否拦截?
↓
不拦截
↓
Parent 查找点击点在哪个子 View 范围内
↓
发现点在 ButtonA 里
↓
事件交给 ButtonA
↓
ButtonA 处理
点击 ButtonB 时:
事件来到 Parent
↓
Parent 是否拦截?
↓
不拦截
↓
Parent 查找点击点在哪个子 View 范围内
↓
发现点在 ButtonB 里
↓
事件交给 ButtonB
↓
ButtonB 处理
点击空白区域时:
事件来到 Parent
↓
Parent 是否拦截?
↓
不拦截
↓
没有子 View 命中
↓
Parent 自己的 onTouchEvent 处理
12. 用一句话理解事件分发
Android 事件分发可以理解成:
事件从外到内传:
Activity → 父 ViewGroup → 子 View
如果子 View 处理了,就结束。
如果子 View 不处理,事件可能返回给父 View 处理。
也可以理解成:
先找目标 View,再让目标 View 处理。
13. 和 Z 轴顺序的关系
不重叠时
Z 轴顺序影响不大。
因为点击点只会落在其中一个 View 里。
A 和 B 不重叠
点在 A 内,就给 A
点在 B 内,就给 B
重叠时
Z 轴顺序才重要。
因为点击点可能同时落在多个 View 里。
A 和 B 重叠
点击点既在 A 内,也在 B 内
那就优先给上层 View
所以:
不重叠:看坐标
重叠:先看坐标,再看层级
14. 总结
如果不是重叠的 View,事件分发主要看:
点击坐标落在哪个 View 的范围内
完整机制是:
Activity.dispatchTouchEvent()
↓
ViewGroup.dispatchTouchEvent()
↓
ViewGroup.onInterceptTouchEvent()
↓
子 View.dispatchTouchEvent()
↓
子 View.onTouchEvent()
最核心记住三句话:
1. 事件从 Activity 往子 View 传。
2. ViewGroup 可以选择拦截。
3. 不重叠时,谁的区域被点中,谁优先处理。
如果多个 View 重叠,才需要考虑:
谁在上层,谁优先接收事件。
1. 什么叫 View 的 Z 轴顺序?
你可以把手机屏幕想象成一张桌子,View 是一张张纸。
屏幕正面
↑
│ View B 后放上去的,盖在上面
│ View A 先放上去的,被盖在下面
↓
屏幕背面
所谓 Z 轴顺序,就是谁在上面,谁在下面。
- X 轴:左右
- Y 轴:上下
- Z 轴:前后层级
在 Android 里,如果两个 View 位置重叠:
+----------------+
| |
| View A |
| +---------+
| | View B |
| | |
| +---------+
| |
+----------------+
如果 View B 在上层,那么你看到的是 View B 盖住了 View A。
2. 什么叫“绘制顺序决定 Z 轴顺序”?
Android 绘制一个 ViewGroup 的子 View 时,通常是按照子 View 添加顺序来画的。
比如:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/viewA"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#FF0000" />
<View
android:id="@+id/viewB"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#0000FF" />
</FrameLayout>
这里 viewA 先写,viewB 后写。
绘制顺序一般是:
先画 viewA
再画 viewB
所以 viewB 会盖在 viewA 上面。
就像画画一样:
先画红色方块
再画蓝色方块
蓝色方块会盖住红色方块。
3. “后绘制的 View 会显示在上层”是什么意思?
看这个例子:
<FrameLayout>
<TextView
android:id="@+id/textA"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#FF0000"
android:text="A" />
<TextView
android:id="@+id/textB"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#0000FF"
android:text="B" />
</FrameLayout>
两个 TextView 大小一样,位置也一样。
因为 textB 写在后面,所以通常会后绘制。
最终你看到的可能是:
蓝色的 B
而不是红色的 A。
因为 B 把 A 盖住了。
4. 为什么这会影响事件传递?
假设 A 和 B 重叠:
+----------------------+
| |
| +---------+ |
| | B | |
| | | |
| +---------+ |
| |
| A 在 B 下面 |
+----------------------+
你手指点在 B 这个区域:
手指点击
↓
+---------+
| B |
+---------+
下面还有 A
那 Android 会优先问谁?
一般会先问上层的 View,也就是 B:
点击事件来了
↓
先问 B:你要不要处理?
↓
如果 B 处理了,A 就收不到
↓
如果 B 不处理,再考虑下面的 A
所以这句话说:
后绘制的 View 显示在上层,优先接收到事件。
意思就是:
谁盖在最上面,谁先有机会处理点击。
5. 举个具体例子
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/buttonA"
android:layout_width="200dp"
android:layout_height="200dp"
android:text="A"
android:backgroundTint="#FF0000" />
<Button
android:id="@+id/buttonB"
android:layout_width="150dp"
android:layout_height="150dp"
android:text="B"
android:backgroundTint="#0000FF" />
</FrameLayout>
buttonA 先写,buttonB 后写。
如果两个按钮重叠,那么 B 在 A 上面。
你点击重叠区域:
先触发 buttonB 的点击
如果 B 设置了点击事件:
buttonB.setOnClickListener {
Log.d("test", "点击了 B")
}
那么你点重叠区域,大概率输出:
点击了 B
而不是 A。
6. 可以理解成电梯楼层
把 View 想象成楼层:
第 3 层:View B
第 2 层:View A
第 1 层:背景
你点击屏幕时,Android 会从上往下找:
先看第 3 层有没有 View 接收
如果没有,再看第 2 层
如果还没有,再看第 1 层
所以越上层,事件优先级越高。
7. 但是注意:不是“上层 View 一定会消费事件”
上层 View 只是优先有机会接收事件,但不代表它一定会处理。
比如 B 不可点击:
buttonB.isClickable = false
或者 B 的 onTouchEvent() 返回 false,那么事件可能继续给下面的 A。
大概流程是:
点击事件
↓
ViewGroup 从上层子 View 开始找
↓
B 在点击范围内吗?
↓
是
↓
B 要处理吗?
↓
如果处理:事件给 B,结束
如果不处理:继续找下面的 A
8. 在 ViewGroup 里的大概事件分发顺序
比如有一个 FrameLayout,里面有 A、B、C:
<FrameLayout>
<View android:id="@+id/A" />
<View android:id="@+id/B" />
<View android:id="@+id/C" />
</FrameLayout>
绘制顺序通常是:
A -> B -> C
显示层级通常是:
C 在最上面
B 在中间
A 在最下面
事件查找顺序通常是反过来:
C -> B -> A
因为 Android 会优先找最上层的那个 View。
9. 为什么后添加的 View 在上面?
因为 Android 绘制子 View 时,通常是这样的:
for (i in 0 until childCount) {
drawChild(getChildAt(i))
}
也就是先画第一个子 View,再画第二个,再画第三个。
后画的东西会覆盖前面画的东西。
就像你在纸上先画一个红色圆,再画一个蓝色圆,蓝色圆会盖住红色圆。
10. 一个简单总结
你看到的那句话可以翻译成更好懂的话:
在同一个父布局里,如果几个 View 重叠,Android 一般会先画前面的 View,再画后面的 View。后画的 View 会盖在前面的 View 上面。所以当你点击重叠区域时,盖在最上面的那个 View 会优先收到点击事件。
更简单一点:
后画的,在上面;
在上面的,先接收点击。
11. 需要注意的特殊情况
这句话是一个常见规律,但不是绝对的。以下情况会影响顺序:
View.setZ()/translationZelevation- 自定义
ViewGroup的绘制顺序 bringToFront()- 父 View 的
onInterceptTouchEvent() - 子 View 是否
clickable - 子 View 是否
visible - 触摸点是否落在子 View 范围内
比如:
viewA.bringToFront()
可以把 viewA 提到最前面。
或者:
viewA.elevation = 10f
viewB.elevation = 1f
viewA 也可能显示在 viewB 上面。
12. 最核心的一句话
你只要记住这个就行:
如果多个 View 重叠,用户点击重叠区域时,
Android 通常会从最上层的 View 开始分发事件。
而最上层的 View,往往就是后绘制的那个 View。