Skip to content

节点事件系统

input 对象支持了全局的 输入事件系统,全局输入事件包括鼠标、触摸、键盘和重力传感四种。而 Node 也实现了 EventTarget 的事件监听接口。在此基础上,我们提供了一些基础的节点相关的系统事件。

本篇文档着重介绍了与 UI 节点树相关联的鼠标和触摸事件,这些事件是被直接触发在 UI 相关节点上的,所以被称为节点事件,使用方式如下:

ts
node.on(Node.EventType.MOUSE_DOWN, (event) => {
  console.log('Mouse down');
}, this);

注意:我们不推荐直接使用事件的名称字符串注册事件监听了。例如上述代码示例,请不要使用 node.on('mouse-down', callback, target) 来注册事件监听。

2D UI 节点上的触摸事件监听依赖于 UITransform 组件。如果需要实现对 3D 物体的触摸检测,请参考文档 3D 物体的触摸检测

鼠标事件类型和事件对象

鼠标事件在 PC 端才会触发,系统提供的事件类型如下:

枚举对象定义事件触发的时机
Node.EventType.MOUSE_DOWN当鼠标在目标节点区域按下时触发一次。
Node.EventType.MOUSE_ENTER当鼠标移入目标节点区域时触发,不论是否按下。
Node.EventType.MOUSE_MOVE当鼠标在目标节点区域中移动时触发,不论是否按下。
Node.EventType.MOUSE_LEAVE当鼠标移出目标节点区域时触发,不论是否按下。
Node.EventType.MOUSE_UP当鼠标从按下状态松开时触发一次。
Node.EventType.MOUSE_WHEEL当鼠标滚轮滚动时触发。

鼠标事件(Event.EventMouse)的重要 API 请参考 鼠标事件 API(Event 标准事件 API 除外)。

触摸事件类型和事件对象

触摸事件在移动端和 PC 端都会触发,开发者若希望更好地在 PC 端进行调试,只需要监听触摸事件即可同时响应移动端的触摸事件和 PC 端的鼠标事件。系统提供的触摸事件类型如下:

枚举对象定义事件触发的时机
Node.EventType.TOUCH_START当手指触点落在目标节点区域内时。
Node.EventType.TOUCH_MOVE当手指在屏幕上移动时。
Node.EventType.TOUCH_END当手指在目标节点区域内离开屏幕时。
Node.EventType.TOUCH_CANCEL当手指在目标节点区域外离开屏幕时。

触摸事件(Event.EventTouch)的重要 API 请参考 触摸事件 API(Event 标准事件 API 除外)。

注意:触摸事件支持多点触摸,每个触点都会发送一次事件给事件监听器。

节点事件派发

Cocos Creator 在 Node 上支持了 dispatchEvent 接口,通过该接口派发的事件,会进入事件派发阶段。Creator 的事件派发系统是按照 Web 的事件冒泡及捕获标准 实现的,事件在派发之后,会经历下面三个阶段:

  • 捕获:事件从场景根节点,逐级向子节点传递,直到到达目标节点或者在某个节点的响应函数中中断事件传递
  • 目标:事件在目标节点上触发
  • 冒泡:事件由目标节点,逐级向父节点冒泡传递,直到到达根节点或者在某个节点的响应函数中中断事件传递

当调用 node.dispatchEvent() 时,意味着 node 就是上文提到的目标节点。在事件的传递过程中,我们可以通过调用 event.propagationStopped = true 来中断事件传递。

在 v3.0 中,我们移除了 Event.EventCustom 类,如果要派发自定义事件,需要先实现一个自定义的事件类,该类继承自 Event 类,例如:

ts
// Event 由 cc 模块导入
import { Event } from 'cc';

class MyEvent extends Event {
    constructor(name: string, bubbles?: boolean, detail?: any) {
        super(name, bubbles);
        this.detail = detail;
    }
    public detail: any = null;  // 自定义的属性
}

bubble-event

以上图为例,这张图展示了事件在 目标冒泡 阶段的传递顺序。当我们从节点 c 派发事件 “foobar”,倘若节点 a,b 均做了 “foobar” 事件的监听,则事件会经由 c 依次传递给 b、a 节点。代码实现示例如下:

ts
// 节点 c 的组件脚本中
this.node.dispatchEvent( new MyEvent('foobar', true, 'detail info') );

如果我们希望在 b 节点截获事件后就不再传递事件,可以通过调用 event.propagationStopped = true 函数来完成。代码实现示例如下:

ts
// 节点 b 的组件脚本中
this.node.on('foobar', (event: MyEvent) => {
  event.propagationStopped = true;
});

注意:在派发用户自定义事件的时候,请不要直接创建 cc 内的 Event 对象,因为它是一个抽象类。

如果希望将事件注册在捕获阶段,可以给 on 接口传递第四个参数,例如:

ts
// 节点 a 的组件脚本中
this.node.on('foobar', callback, target, true);

事件对象

在事件监听回调中,开发者会接收到一个 Event 类型的事件对象 event,propagationStopped 就是 Event 的标准 API,其它重要的 API 包含:

API 名类型意义
typeString事件的类型(事件名)。
targetNode接收到事件的原始对象。
currentTargetNode接收到事件的当前对象,事件在冒泡阶段当前对象可能与原始对象不同。
getTypeFunction获取事件的类型。
propagationStoppedBoolean是否停止传递当前事件。
propagationImmediateStoppedBoolean是否立即停止当前事件的传递,事件甚至不会被分派到所连接的当前目标。

触摸事件的传递

如上所述,注册在 Node 上的触摸事件,引擎内部就是通过 dispatchEvent 接口派发的。下面我们将介绍触摸事件在 目标冒泡 阶段的传递顺序。

触摸事件冒泡

触摸事件支持节点树的事件冒泡,以下图为例:

propagation

在上图的场景中,假设 A 节点拥有一个子节点 B,B 拥有一个子节点 C。开发者对 A、B、C 节点都监听了触摸事件(以下的示例默认节点都监听了触摸事件)。

当鼠标或手指在 C 节点区域内按下时,事件将首先在 C 节点触发,C 节点监听器接收到事件(该阶段为目标阶段);接着 C 节点会将事件向其父节点 B 传递这个事件,B 节点的监听器将会接收到事件;同理 B 节点会将事件传递给父节点 A 。这就是最基本的事件冒泡过程。

注意:虽然触点可能不在 A、B 节点区域内,但 A、B 节点也能触发触摸事件,这是通过子节点 C 的触摸事件冒泡机制传递过来的。在触摸事件冒泡的过程中不会有触摸检测。

触摸事件的冒泡过程与普通事件的冒泡过程并没有区别。所以,调用 event.propagationStopped = true 便可主动停止触摸事件的冒泡过程。

同级节点间的触点归属问题

同级节点间,触点归属于处于顶层的节点。假设上图中 B、C 为同级节点,C 节点部分覆盖在 B 节点之上。这时候如果 C 节点接收到触摸事件,那么表示触点归属于 C 节点,这意味着同级节点 B 就不会再接收到触摸事件了,即使触点同时也在 B 节点内。

此时如果 C 节点还存在父节点,那么通过事件冒泡机制 C 节点还可以将触摸事件传递给父节点。

在 v3.4.0 中,我们支持了 事件穿透派发 功能。在上述同级节点的例子中,如果需要把事件穿透也派发给 B 节点,则可以通过调用 event.preventSwallow = true 来阻止事件被 C 节点吞噬。

注意:如果要让 TOUCH END 事件可穿透的话,对应的 TOUCH START 事件也需要设置为可穿透。事件的穿透派发会降低事件派发的效率,请谨慎使用。

不同 Canvas 的触点归属问题

不同 Canvas 之间的触点拦截是根据节点优先级(可在 Canvas 节点默认自带的 Camera 节点的 priority 属性中设置)决定的。在下图的场景中,左侧节点树里的节点 Canvas 1-5 对应着右侧场景中的图片 priority 1-5。可以看出,即使节点 Canvas 3、4、5 在节点树里并没有按照顺序排列,但根据 Canvas 上的优先级(priority)关系,触点的响应先后顺序仍然是 Canvas 5 -> Canvas 4 -> Canvas 3 -> Canvas 2 -> Canvas 1。只有在优先级相同的情况下,Canvas 之间的排序才是按照节点树的先后顺序进行。

multi-canvas

将触摸或鼠标事件注册在捕获阶段

有时候我们需要父节点的触摸或鼠标事件先于它的任何子节点派发,比如 ScrollView 组件就是这样设计的。这时候事件冒泡已经不能满足我们的需求了,需要将父节点的事件注册在捕获阶段。

要实现这个需求,可以在给 node 注册触摸或鼠标事件时,传入第四个参数 true,表示 useCapture。代码示例如下:

ts
this.node.on(Node.EventType.TOUCH_START, this.onTouchStartCallback, this, true);

当节点触发 Node.EventType.TOUCH_START 事件时,会先将 Node.EventType.TOUCH_START 事件派发给所有注册在捕获阶段的父节点监听器,然后派发给节点自身的监听器,最后才到了事件冒泡阶段。

事件拦截

正常的事件会按照上文说明的方式去派发。但是如果节点身上带有 ButtonToggle 或者 BlockInputEvents 这几个组件的话,会停止事件冒泡。

例如下图,图中有两个按钮,分别是 Canvas 0 下的 priority 1 和 Canvas 1 下的 priority 2。如果点击两个按钮的交汇处,也就是图中的蓝色区域,会出现按钮 priority 2 成功接收到了触点事件,而按钮 priority 1 则没有。
这是因为按上文的事件接收规则,按钮 priority 2 优先接收到了触摸事件,并且对事件进行了拦截(event.propagationStopped = true),以防止事件穿透。如果是非按钮节点,也可以通过添加 BlockInputEvents 组件来对事件进行拦截,防止穿透。

注意:按钮 priority 1 和 priority 2 分别在 Canvas 0 和 Canvas 1 节点下,两个按钮并不是同级节点。

events-block

触摸事件举例

以下图举例,总结下触摸事件的传递机制。图中有 A、B、C、D 四个节点,其中 A、B 为同级节点。具体层级关系如下:

example

  1. 若触点在 A、B 的重叠区域内,此时 B 接收不到触摸事件,事件的传递顺序是 A -> C -> D
  2. 若触点在 B 节点内(可见的绿色区域),则事件的传递顺序是 B -> C -> D
  3. 若触点在 C 节点内,则事件的传递顺序是 C -> D
  4. 若以第 2 种情况为前提,同时 C D 节点的触摸事件注册在捕获阶段,则事件的传递顺序是 D -> C -> B

Node 的其它事件

所有的 Node 内置事件都可以通过 Node.EventType 获取事件名。

3D 节点事件

枚举对象定义事件触发的时机
TRANSFORM_CHANGED当变换属性修改时,会派发一个枚举值 TransformBit,根据枚举值定义修改的变换。

变换枚举值定义:

枚举值含义对应的变换
TransformBit.NONE属性无改变。
TransformBit.POSITION节点位置改变。
TransformBit.ROTATION节点旋转改变。
TransformBit.SCALE节点缩放改变。
TransformBit.RS节点旋转及缩放改变。
TransformBit.TRS节点平移,旋转及缩放都改变。

2D 节点事件

枚举对象定义事件触发的时机
SIZE_CHANGED当宽高属性修改时。宽高属性位于 UITransform 组件上。
ANCHOR_CHANGED当锚点属性修改时。锚点属性位于 UITransform 组件上。
COLOR_CHANGED当颜色属性修改时。颜色属性位于 UI 渲染组件上。
CHILD_ADDED添加子节点时。
CHILD_REMOVED移除子节点时。
PARENT_CHANGED父节点改变时。
SIBLING_ORDER_CHANGED兄弟节点顺序改变时。
SCENE_CHANGED_FOR_PERSISTS改变常驻节点所在场景时。
NODE_DESTROYED节点销毁时。
LAYER_CHANGEDlayer 属性改变时。
ACTIVE_IN_HIERARCHY_CHANGEDactiveInHierarchy 属性改变时。

多点触摸事件

引擎有多点触摸事件的屏蔽开关,多点触摸事件默认为开启状态。对于不需要多点触摸的项目,可以通过以下代码关闭多点触摸:

ts
macro.ENABLE_MULTI_TOUCH = false;

或者也可以通过 Creator 顶部菜单栏的 项目 -> 项目设置 -> Macro Config 进行配置,去掉ENABLE_MULTI_TOUCH 属性的勾选。

暂停或恢复节点系统事件

暂停节点系统事件,代码示例如下:

ts
// 暂停当前节点上注册的所有节点系统事件,节点系统事件包含触摸和鼠标事件。
// 如果传递参数 true,那么这个 API 将暂停本节点和它的所有子节点上的节点系统事件。
// example
this.node.pauseSystemEvents();

恢复节点系统事件,代码示例如下:

ts
// 恢复当前节点上注册的所有节点系统事件,节点系统事件包含触摸和鼠标事件。
// 如果传递参数 true,那么这个 API 将恢复本节点和它的所有子节点上的节点系统事件。
// example
this.node.resumeSystemEvents();