Android 中的view事件分发机制

如果 不是重叠的 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

比如:

  • LinearLayout
  • FrameLayout
  • ConstraintLayout
  • RecyclerView
  • ScrollView

它们可以包含子 View。

所以 ViewGroup 有两个选择:

1. 把事件交给子 View
2. 自己处理事件

因此 ViewGroup 有:

dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()

View

比如:

  • TextView
  • Button
  • ImageView

普通 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,后续的 MOVEUP 通常就继续给谁。

比如你点击 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. 需要注意的特殊情况

这句话是一个常见规律,但不是绝对的。以下情况会影响顺序:

  1. View.setZ() / translationZ
  2. elevation
  3. 自定义 ViewGroup 的绘制顺序
  4. bringToFront()
  5. 父 View 的 onInterceptTouchEvent()
  6. 子 View 是否 clickable
  7. 子 View 是否 visible
  8. 触摸点是否落在子 View 范围内

比如:

viewA.bringToFront()

可以把 viewA 提到最前面。

或者:

viewA.elevation = 10f
viewB.elevation = 1f

viewA 也可能显示在 viewB 上面。


12. 最核心的一句话

你只要记住这个就行:

如果多个 View 重叠,用户点击重叠区域时,
Android 通常会从最上层的 View 开始分发事件。
而最上层的 View,往往就是后绘制的那个 View。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇