节点事件系统

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

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

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

    鼠标事件(Event.EventMouse)的重要 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 请参考 (Event 标准事件 API 除外)。

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

    节点事件派发

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

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

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

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

    1. // Event 由 cc 模块导入
    2. import { Event } from 'cc';
    3. class MyEvent extends Event {
    4. constructor(name: string, bubbles?: boolean, detail?: any) {
    5. super(name, bubbles);
    6. this.detail = detail;
    7. }
    8. }

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

    1. this.node.dispatchEvent( new MyEvent('foobar', true, 'detail info') );

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

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

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

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

    触摸事件的传递

    如上所述,注册在 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 节点吞噬。

    不同 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 之间的排序才是按照节点树的先后顺序进行。

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

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

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

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

    当节点触发 事件时,会先将 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 节点下,两个按钮并不是同级节点。

    触摸事件举例

    以下图举例,总结下触摸事件的传递机制。图中有 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.EventType 获取事件名。

    3D 节点事件

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

    变换枚举值定义:

    枚举对象定义事件触发的时机
    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 属性改变时。

    多点触摸事件

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

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

    暂停或恢复节点系统事件

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

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