- 教程
- 技术博客
- 文档
【免费下载链接】YCBlogs
技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!
本文是 YCBlogs 仓库「事件分发」系列的开篇之作,以 android/05.事件分发/01.View事件基础介绍.md 为骨架,结合同目录下源码分析、滑动冲突、返回键分发与面试题等姊妹篇,完整梳理 Android 触摸事件分发的概念、核心方法、调用顺序与经典场景。读完本文你将掌握:事件序列(DOWN/MOVE/UP/CANCEL)的本质、
dispatchTouchEvent()/onInterceptTouchEvent()/onTouchEvent()三者分工与返回语义、默认 / 处理 / 拦截 / 半路拦截四种典型场景下事件的流向,以及由这些规则衍生出的滑动冲突与拖动业务解决方案,可直接用于自定义 View 开发和滑动冲突排查。
01. 事件分发概念
1.1 事件分发的对象是谁
事件分发的对象是事件,注意:事件分发是向下传递的,也就是父到子的顺序。
当用户触摸屏幕时(View 或 ViewGroup 派生的控件),将产生点击事件(Touch 事件)。Touch 事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象。
主要发生的 Touch 事件有如下四种:
| 事件类型 | 说明 |
|---|---|
MotionEvent.ACTION_DOWN | 按下 View,所有事件的开始 |
MotionEvent.ACTION_MOVE | 滑动 View |
MotionEvent.ACTION_CANCEL | 非人为原因结束本次事件 |
MotionEvent.ACTION_UP | 抬起 View,与 DOWN 对应 |
a.1 ACTION_DOWN
用户手指的按下操作,一次按下操作标志着一次触摸事件的开始。
a.2 ACTION_UP
用户手指离开屏幕的操作,一次抬起操作标志着一次触摸事件的结束。
a.3 ACTION_MOVE
用户手指按压屏幕后,在松开之前,如果距离超过一定阈值,那么会被判定为 ACTION_MOVE。一般情况下,手指的轻微移动都会触发一系列移动事件。
a.4 注意
- 在一次屏幕触摸事件中,
ACTION_DOWN和ACTION_UP这两个事件是必须的,而ACTION_MOVE视情况而定——如果用户仅是点击了一下屏幕,那么可能只会监测到按下和抬起的动作。 - 通过
MotionEvent可以获得事件发生的 x 和 y 坐标:getX()/getY()返回的是相对于当前 View 左上角的 X 和 Y 坐标;getRawX()/getRawY()返回的是相对于手机屏幕左上角的 X 和 Y 坐标。
a.5 其他
Touch触摸事件 ♦ 在Android中Touch触摸事件主要包括: 点击(onClick) 长按(onLongClick) 拖拽(onDrag) 滑动(onScroll) ♦ 在Android中Touch操作状态主要包括: 按下(ACTION_DOWN) 移动(ACTION_MOVE) 抬起(ACTION_UP) 取消手势(ACTION_CANCEL) 划出屏幕(ACTION_OUTSIDE)事件列(事件序列):从手指接触屏幕至手指离开屏幕,这个过程产生的一系列事件。即当一个 MotionEvent 产生后,系统需要把这个事件传递给一个具体的 View 去处理。任何事件列都是以 DOWN 事件开始、UP 事件结束,中间有无数个 MOVE 事件。
补充:ACTION_OUTSIDE 表示触摸区域划出了控件边界,而 ACTION_CANCEL 通常是系统层面(如父容器中途拦截)主动结束事件序列时派发的,是触摸事件处理中非常关键的“补偿机制”,后文 3.2.4 会详细展开。
1.2 事件分发的本质
将点击事件(MotionEvent)向某个 View 进行传递并最终得到处理——即当一个点击事件发生后,系统需要将这个事件传递给一个具体的 View 去处理。这个事件传递的过程就是分发过程。
Android 事件分发机制的本质是要解决:点击事件由哪个对象发出,经过哪些对象,最终达到哪个对象并最终得到处理。
1.3 事件在哪些对象间进行传递
Activity、ViewGroup、View是参与事件分发的三大角色。
一个点击事件产生后,传递顺序是:Activity(Window) -> ViewGroup -> View。Android 的 UI 界面正是由 Activity、ViewGroup、View 及其派生类组合而成的。
- View 是所有 UI 组件的基类:一般 Button、ImageView、TextView 等控件都是继承父类 View。
- ViewGroup 是容纳 UI 组件的容器,即一组 View 的集合(包含很多子 View 和子 ViewGroup),其本身也是从 View 派生的,即 ViewGroup 是 View 的子类:
- 是 Android 所有布局的父类或间接父类:项目用到的布局(LinearLayout、RelativeLayout 等)都继承自 ViewGroup,即属于 ViewGroup 子类;
- 与普通 View 的区别:ViewGroup 实际上也是一个 View,只不过比起 View,它多了可以包含子 View 和定义布局参数的功能。
从更完整的链路看,触摸事件从系统 Native 层进入应用后,会依次经过Native 层 -> ViewRootImpl 层 -> DecorView 层 -> Activity 层 -> ViewGroup 层 -> View 层(详见 返回键事件分发流程 中的输入链路分析)。应用内日常讨论的分发,聚焦于 Activity/ViewGroup/View 三层。
1.4 事件分发过程涉及方法
事件分发过程由这几个方法协作完成:
dispatchTouchEvent():分发事件onInterceptTouchEvent():判断是否拦截事件(仅 ViewGroup 有)onTouchEvent():处理事件
1.5 Android 触摸事件流程总结
- 一个事件序列从手指触摸屏幕开始,到触摸结束。同一事件序列是以
ACTION_DOWN开始,中间有数量不定的ACTION_MOVE事件,最终以ACTION_UP结束。 - 事件传递顺序是:
Activity ——> Window ——> View;最后顶级 View 接收到事件后,就会按照事件分发机制去分发事件。 - 事件传递过程是由外向内的,即事件总是由父元素分发给子元素。
02. 事件分发机制方法
事件分发过程由dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()三个方法协助完成。
2.1 dispatchTouchEvent()
| 属性 | 介绍 |
|---|---|
| 使用对象 | Activity、ViewGroup、View |
| 作用 | 分发点击事件 |
| 调用时刻 | 当点击事件能够传递给当前 View 时,该方法就会被调用 |
| 返回结果 | 是否消费当前事件 |
1. 默认情况:根据当前对象的不同而返回方法不同
| 对象 | 返回方法 | 备注 |
|---|---|---|
| Activity | super.dispatchTouchEvent() | 即调用父类(ViewGroup)的dispatchTouchEvent() |
| ViewGroup | onInterceptTouchEvent() | 即调用自身的onInterceptTouchEvent() |
| View | onTouchEvent() | 即调用自身的onTouchEvent() |
流程解析
- 返回 true(消费事件)
- 事件不会往下传递;
- 后续事件(Move、Up)会继续分发到该 View。
- 返回 false(不消费事件)
- 事件往下传递;
- 将事件回传给父控件的
onTouchEvent()处理(Activity 例外:返回 false = 消费事件); - 后续事件(Move、Up)会继续分发到该 View(与
onTouchEvent()的区别)。
源码佐证:Activity 的
dispatchTouchEvent()中,事件先经过onUserInteraction()(空实现,多用于屏保逻辑),随后调用getWindow().superDispatchTouchEvent(ev)把事件交给 Window;只有 Window 层返回 false(即所有 View 都没有处理)时,才回落到onTouchEvent(ev)自己处理(见 View事件机制源码分析)。
2.2 onTouchEvent()
| 属性 | 介绍 |
|---|---|
| 使用对象 | Activity、ViewGroup、View |
| 作用 | 处理点击事件 |
| 调用时刻 | 在dispatchTouchEvent()内部调用 |
| 返回结果 | 是否消费(处理)当前事件 |
流程解析
- 返回 true
- 自己处理(消费)该事件;
- 事件停止传递;
- 该事件序列的后续事件(Move、Up)交由它处理。
- 返回 false(同默认实现:调用父类 onTouchEvent())
- 不处理(消费)该事件;
- 事件往上传递给父控件的
onTouchEvent()处理; - 当前 View 不再接受此事件列的其他事件(Move、Up)。
注意一点
这里要强调 View 的OnTouchListener:如果 View 设置了该监听,那么onTouch()将会回调。如果返回为 true,那么该 View 的onTouchEvent()将不会执行——这是因为设置的OnTouchListener执行时的优先级要比onTouchEvent高。
优先级:OnTouchListener > onTouchEvent > onClickListener
源码佐证:View 的
dispatchTouchEvent()源码中,只有当mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)三个条件全部成立才直接返回 true,否则继续走onTouchEvent(event);而onClick()是在onTouchEvent()的 ACTION_UP 分支中经由performClick()回调的,因此优先级最低(详见 View事件机制源码分析 与 View事件总结案例)。
2.3 onInterceptTouchEvent()
| 属性 | 介绍 |
|---|---|
| 使用对象 | ViewGroup(注:Activity、View 都没有该方法) |
| 作用 | 拦截事件,即自己处理该事件 |
| 调用时刻 | 在 ViewGroup 的dispatchTouchEvent()内部调用 |
| 返回结果 | 是否拦截当前事件 |
流程解析
- true:当前 ViewGroup(因为 View 中没有该方法,而没有 child 的 View 也不需要有拦截机制)希望该事件不再传递给其 child,而是希望自己处理。
- false:当前 ViewGroup 不准备拦截该事件,事件正常向下分发给其 child。
2.4 三个方法执行顺序
以下代码在自定义 View 中同时重写三个方法并打印日志:
@Override public boolean onInterceptTouchEvent(MotionEvent e) { LogUtils.e("yc----------事件拦截----------"); return super.onInterceptTouchEvent(e); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { LogUtils.e("yc----------事件分发----------"); return super.dispatchTouchEvent(ev); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent e) { LogUtils.e("yc----------事件触摸----------"); return super.onTouchEvent(e); }执行结果如下:
yc----------事件分发---------- yc----------事件拦截---------- yc----------事件触摸----------可以看到:事件先进入dispatchTouchEvent()(分发入口)——> 再进入onInterceptTouchEvent()(判断是否拦截)——> 最后进入onTouchEvent()(处理事件),这与源码中的调用关系完全一致。
2.5 三者之间关系
下面将用一段伪代码来阐述上述三个方法的关系和点击事件传递规则:
// 点击事件产生后,会直接调用dispatchTouchEvent分发方法 public boolean dispatchTouchEvent(MotionEvent ev) { // 代表是否消耗事件 boolean consume = false; if (onInterceptTouchEvent(ev)) { // 如果onInterceptTouchEvent()返回true则代表当前View拦截了点击事件 // 则该点击事件则会交给当前View进行处理 // 即调用onTouchEvent()方法去处理点击事件 consume = onTouchEvent(ev); } else { // 如果onInterceptTouchEvent()返回false则代表当前View不拦截点击事件 // 则该点击事件则会继续传递给它的子元素 // 子元素的dispatchTouchEvent()就会被调用,重复上述过程 // 直到点击事件被最终处理为止 consume = child.dispatchTouchEvent(ev); } return consume; }对照真实源码,ViewGroup 的
dispatchTouchEvent()中判断是否调用拦截器的核心逻辑如下(节选自 View事件机制源码分析):// 发生 ACTION_DOWN 事件或者已经发生过 ACTION_DOWN 且 mFirstTouchTarget 已被赋值,才进入此区域 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // disallowIntercept:是否禁用事件拦截的功能(默认是 false) final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); // 默认情况下会调用拦截方法 ev.setAction(action); } else { intercepted = false; } } else { // 当没有触摸 targets,且不是 down 事件时,开始持续拦截触摸 intercepted = true; }这也解释了:如果子 View 的
onTouchEvent()返回 false,那么接下来的一系列事件都不会再交给它处理;如果 ViewGroup 的onInterceptTouchEvent()第一次执行为 true,则mFirstTouchTarget = null,后续事件将不再调用onInterceptTouchEvent(),直接视为拦截。
03. 事件分发背景描述
3.1 先看一个案例
讨论的布局层次如下:
- 最外层:Activity A,包含两个子 View:ViewGroup B、View C;
- 中间层:ViewGroup B,包含一个子 View:View C;
- 最内层:View C。
触摸情况:假设用户首先触摸到屏幕上 View C 上的某个点,那么ACTION_DOWN事件就在该点产生,然后用户移动手指并最后离开屏幕。
3.2 该案例事件传递情况
一般的事件传递场景有四种:默认情况、处理事件、拦截 DOWN 事件、拦截后续事件(MOVE、UP)。
3.2.1 默认情况
即不对控件里的方法(dispatchTouchEvent()、onTouchEvent()、onInterceptTouchEvent())进行重写或更改返回值,那么调用的是这 3 个方法的默认实现——调用父类的方法。
事件传递情况:
- 从
Activity A --> ViewGroup B --> View C,从上往下调用dispatchTouchEvent(); - 再由
View C --> ViewGroup B --> Activity A,从下往上调用onTouchEvent()。
注:虽然 ViewGroup B 的onInterceptTouchEvent()方法对 DOWN 事件返回了 false,后续的事件(MOVE、UP)依然会传递给它的onInterceptTouchEvent()。这一点与onTouchEvent()的行为是不一样的(因为此时mFirstTouchTarget != null,仍会进入拦截判断分支,见 2.5 节源码)。
3.2.2 处理事件
假设 View C 希望处理这个点击事件,即 C 被设置成可点击的(Clickable)或者覆写了 C 的onTouchEvent()方法返回 true(最常见的:设置 Button 按钮来响应点击事件)。
事件传递情况:
- DOWN 事件被传递给 C 的
onTouchEvent()方法,该方法返回 true,表示处理这个事件; - 因为 C 正在处理这个事件,那么 DOWN 事件将不再往上传递给 B 和 A 的
onTouchEvent(); - 该事件列的其他事件(Move、Up)也将传递给 C 的
onTouchEvent()。
3.2.3 拦截 DOWN 事件
假设 ViewGroup B 希望处理这个点击事件,即 B 覆写了onInterceptTouchEvent()返回 true、onTouchEvent()返回 true。
事件传递情况:
- DOWN 事件被传递给 B 的
onInterceptTouchEvent()方法,该方法返回 true,表示拦截这个事件,即自己处理这个事件(不再往下传递); - 调用
onTouchEvent()处理事件(DOWN 事件将不再往上传递给 A 的onTouchEvent()); - 该事件列的其他事件(Move、Up)将直接传递给 B 的
onTouchEvent()。
注意:该事件列的其他事件(Move、Up)将不会再传递给 B 的onInterceptTouchEvent()方法,该方法一旦返回一次 true,就再也不会被调用了。
3.2.4 拦截 DOWN 的后续事件
假设 ViewGroup B 没有拦截 DOWN 事件(还是 View C 来处理 DOWN 事件),但它拦截了接下来的 MOVE 事件。
事件传递情况:
- DOWN 事件传递到 C 的
onTouchEvent()方法,返回了 true; - 在后续到来的 MOVE 事件中,B 的
onInterceptTouchEvent()方法返回 true 拦截该 MOVE 事件,但该事件并没有传递给 B;这个 MOVE 事件将会被系统变成一个 CANCEL 事件传递给 C 的onTouchEvent()方法; - 后续又来了一个 MOVE 事件,该 MOVE 事件才会直接传递给 B 的
onTouchEvent():- 后续事件将直接传递给 B 的
onTouchEvent()处理; - 后续事件将不会再传递给 B 的
onInterceptTouchEvent()方法,该方法一旦返回一次 true,就再也不会被调用了; - C 再也不会收到该事件列产生的后续事件。
- 后续事件将直接传递给 B 的
特别注意:如果 ViewGroup A 拦截了一个半路的事件(如 MOVE),这个事件将会被系统变成一个 CANCEL 事件并传递给之前处理该事件的子 View;该事件不会再传递给 ViewGroup A 的onTouchEvent(),只有再到来的事件才会传递到 ViewGroup A 的onTouchEvent()。
这段 CANCEL 机制正是后续实现滑动冲突“外部拦截法”时的理论基础:当父容器在 MOVE 阶段决定接管事件时,子 View 会先收到一个 CANCEL 来终止自己的滑动/点击状态,避免出现“子 View 还在处理、父 View 也在处理”的撕裂状态。
04. 拖动过程业务举例
事件分发机制落实到业务上,最常见的就是拖拽 / 滑动场景:手指按下(DOWN)选中目标,移动(MOVE)过程中根据坐标差实时改变控件位置或滚动内容,抬起(UP)结束。这一场景直接复用第 03 节的四种传递模型,并结合滑动方法实现。
4.1 拖动案例的流程设计
以一个可拖动的自定义 View 为例,在onTouchEvent()中根据事件类型分别处理:
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: // 1. 记录按下位置,标记开始拖动(对应事件序列的开始) lastX = (int) event.getRawX(); lastY = (int) event.getRawY(); return true; // 消费 DOWN,保证后续 MOVE/UP 都交给本 View case MotionEvent.ACTION_MOVE: // 2. 计算偏移量,更新控件位置(layout / offsetLeftAndRight 等滑动方式) int offsetX = (int) event.getRawX() - lastX; int offsetY = (int) event.getRawY() - lastY; layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); lastX = (int) event.getRawX(); lastY = (int) event.getRawY(); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // 3. 抬起或取消,结束拖动 return true; } return super.onTouchEvent(event); }关键点解读:
- DOWN 必须返回 true:只有 DOWN 被消费,事件序列才被当前 View“锁定”,后续 MOVE/UP 才会持续派发给它(对应 1.5 与 2.1 的规则);
- MOVE 中计算增量:使用
getRawX()/getRawY()(屏幕坐标)计算绝对偏移,避免嵌套容器时坐标偏移叠加问题; - CANCEL 需要妥善处理:当父容器中途拦截(如滑动冲突场景),系统会先派发 CANCEL,此时应复位拖动状态,否则控件会停留在半路。
4.2 拖动场景下的滑动冲突
当父容器与子 View 都可以滑动时,就会产生滑动冲突(例如外层 ScrollView 纵向滑动、内层 ViewPager 横向滑动)。解决 View 之间滑动冲突的方法分为两种:外部拦截法和内部拦截法(详见 View滑动冲突处理)。
外部拦截法:父容器在onInterceptTouchEvent()中根据需求选择性拦截,核心思路伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; // DOWN 不能拦截,否则后续事件都归父容器 break; } case MotionEvent.ACTION_MOVE: { if (满足父容器的拦截要求) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; // UP 原则上不拦截,保证子 View 的 onClick 能触发 break; } default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }要点:ACTION_DOWN 返回 false(否则后续 MOVE/UP 默认全归父容器);ACTION_MOVE 按业务判断;ACTION_UP 返回 false(父容器拦截 UP 会导致子 View 收不到 UP,onClick 无法触发)。
内部拦截法:父容器不拦截任何事件,所有事件先传递给子 View,子 View 根据需求决定自己消费还是交回父容器,核心是配合requestDisallowInterceptTouchEvent():
// 子 View 重写 dispatchTouchEvent public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { parent.requestDisallowInterceptTouchEvent(true); // 先请求父容器不要拦截 break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要此类点击事件) { parent.requestDisallowInterceptTouchEvent(false); // 交还父容器 } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); } // 父容器 onInterceptTouchEvent:DOWN 必须返回 false public boolean onInterceptTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } }注意:requestDisallowInterceptTouchEvent()对ACTION_DOWN 无效(源码中FLAG_DISALLOW_INTERCEPT的判定只在非 DOWN 阶段生效),所以父容器对 DOWN 的拦截必须显式返回 false。
典型的实战案例是 ScrollView 嵌套 ViewPager:当手指左右滑动(deltaX > deltaY)时父容器不拦截,交给 ViewPager 横向翻页;当上下滑动时父容器拦截,由 ScrollView 纵向滚动(完整布局示例与完整代码见 View滑动冲突处理)。
05. 事件传递规则要点速查
把前文规则浓缩成可直接用于排查问题的清单(汇总自 View事件总结案例 与 AndroidView事件问题):
- 事件传递顺序:
Activity -> Window -> View -> 分发;如果 View 的onTouchEvent()返回 false,则父容器的onTouchEvent()会被调用,最终可以传递到 Activity 的onTouchEvent()。 - View 一旦拦截事件,则整个事件序列都由它处理(ACTION_DOWN、UP 等),
onInterceptTouchEvent()不会再被调用(因为默认都拦截了)。 - 如果 View 开始处理事件(已经拦截),但不消耗 ACTION_DOWN(
onTouchEvent()返回 false),则同一事件序列的剩余内容都直接交给父onTouchEvent()处理。 - View 消耗了 ACTION_DOWN 但不处理其他事件,整个事件序列会“消失”(父
onTouchEvent()不会被调用);这些消失的点击事件最终会传给 Activity 处理。 - ViewGroup 默认不拦截任何事件(
onInterceptTouchEvent()默认返回 false)。 - View 没有
onInterceptTouchEvent()方法,一旦有事件传递给 View,onTouchEvent()就会被调用。 - View 的
onTouchEvent()默认都会消耗事件返回 true,除非该 View 不可点击(clickable和longClickable同时为 false)。 - View 的
enable属性不影响onTouchEvent()的默认返回值,即使是 disable 状态,可点击的 View 仍会消费触摸事件(只是不响应)。 onClick()的发生前提是当前 View 可点击,并且收到了 DOWN 和 UP 事件。- 事件传递过程是由父到子层层分发,可以通过
requestDisallowInterceptTouchEvent()让子元素干预父元素的事件分发(ACTION_DOWN 除外)。
06. 常见问题延伸:onTouch / onTouchEvent / onClick 优先级验证
三个方法的回调优先级为onTouch > onTouchEvent > onClick,可通过日志验证:
场景一:onTouch 返回 true
TextView textView = findViewById(R.id.tv_13); textView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("yc", "执行了onTouch(), 动作是:" + event.getAction()); return true; // 消费事件 } }); textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("yc", "执行了onClick()"); } });打印结果(action 为 0 是 ACTION_DOWN,2 是 ACTION_MOVE,1 是 ACTION_UP):
执行了onTouch(), 动作是:0 执行了onTouch(), 动作是:2 执行了onTouch(), 动作是:2 执行了onTouch(), 动作是:1场景二:onTouch 返回 false
执行了onTouch(), 动作是:0 执行了onTouch(), 动作是:2 执行了onTouch(), 动作是:2 执行了onTouch(), 动作是:1 执行了onClick()结论:onTouch()返回 true 就认为该事件被onTouch()消费掉,因而不会再继续向下传递,即不会执行onClick()。
如果设置了 onClickListener 但 onClick() 没有调用,可能产生的原因(排查清单):
- 父 View 拦截了事件,没有传递到当前 View;
- View 的
Enabled = false(setEnabled(false)):view 处于不可用状态,会直接返回; - View 的
Clickable = false(setClickable(false)/setLongClickable(false)):view 不可以点击,不会执行 onClick; - View 设置了
onTouchListener且消耗了事件,会提前返回; - View 设置了
TouchDelegate且消耗了事件,会提前返回。
特别提醒:onTouch能得到执行需要两个前提条件——mOnTouchListener的值不能为空,且当前点击的控件必须是 enable 的(源码中&&为短路与,前面条件为 false 将不再往下执行)。如果你有一个控件是非 enable 的,给它注册 onTouch 事件将永远得不到执行;此时必须通过重写该控件的onTouchEvent()方法来监听触摸。
07. 扩展阅读:本系列姊妹篇
本文是 YCBlogs 事件分发系列的第一篇,其余主题与本篇互相印证、层层递进:
- View事件机制源码分析:从 Activity、ViewGroup、View 三个层次逐段阅读 AOSP 源码,剖析
FLAG_DISALLOW_INTERCEPT、mFirstTouchTarget、dispatchTransformedTouchEvent()、addTouchTarget()等核心实现细节; - View滑动冲突处理:外部拦截法与内部拦截法的完整伪代码、ScrollView 嵌套 ViewPager 实战案例;
- 返回键事件分发流程:从 Native 层到 ViewRootImpl、DecorView、Activity 的完整按键分发链路,可对照理解触摸事件的系统级输入路径;
- View事件总结案例:事件分发高频面试问答与 View 滑动方法(layout、scrollTo/scrollBy、Scroller、属性动画、ViewDragHelper)汇总;
- AndroidView事件问题:14 个 View 事件面试题的问答式整理,涵盖分发机制、滑动冲突、GestureDetector、Scroller、SurfaceView 等。
- 教程
- 技术博客
- 文档
【免费下载链接】YCBlogs
技术博客笔记大汇总,包括Java基础,线程,并发,数据结构;Android技术博客等等;常用设计模式;常见的算法;网络协议知识点;部分flutter笔记;还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!转载请注明出处,谢谢!
相关推荐
Flet 事件系统深度解析:从 Event 基类到事件分发机制的完整指南
Flet 事件系统深度解析:从 Event 基类到事件分发机制的完整指南 导读 在 Flet 中,几乎所有交互能力都建立在事件(Event)机制之上:点击按钮、
前端跨平台桌面应用移动开发Golden Layout事件系统深度剖析:从基础事件到冒泡机制的完整解析
Golden Layout事件系统深度剖析:从基础事件到冒泡机制的完整解析 Golden Layout作为一款功能强大的多窗口布局管理器,其事件系统是开发者必须
前端UI组件如何快速实现多业务线程池管理:DynamicTp完整配置指南
如何快速实现多业务线程池管理:DynamicTp完整配置指南 DynamicTp是一款轻量级动态线程池框架,专为多业务线系统设计,通过内置监控告警功能和主流配置
后端任务调度可观测性
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考