鸿蒙NEXT系列之NDK UI监听组件事件
NDK UI 监听组件事件
- 〇、前言
- 一、Native 侧监听组件事件
- 1、ContentSlot 的限制
- 2、addNodeEventReceiver()
- 3、registerNodeEvent()
- 4、ArkUI_NodeEventType
- 5、整体流程
- 二、代码实践
- 1、封装 UI 回显
- 1.1、保持 ArkTS 回调引用
- 1.2、注册 NativeInvokeUpdateEventInfo 为 NAPI
- 1.3、释放 ArkTS 回调引用
- 1.4、传入 ArkTS 回调方法
- 2、处理点击事件
- 2.1、更新 ArkUINode.h
- 2.2、更新 ArkUIListNode.h
- 2.3、更新 NormalTextListExample.h
- 2.3.1、新增一个按钮
- 2.3.2、实现一个 onClick 函数:
- 2.4、完整项目地址
- 三、总结
〇、前言
这一篇,是鸿蒙NDK UI初探的后续,本来应该在前一周就发布的,只是因为代码在我自己的设备上,一直没有出现符合预期的运行效果,只是在鸿蒙开发者平台的工单支持人员那边的设备上有预期效果:

所以迟迟没有发布,等到今天才发布。因为今天得到的最新回复,将我的所有疑问都解消了。

我自己的设备,无论是手机还是PC,恰好 API 版本都尚未达到上述要求,不过至少证明了功能是没问题的,也就可以放心将相关内容公之于众。
一、Native 侧监听组件事件
1、ContentSlot 的限制
在上一篇,已经向大家介绍了 ArkTS Pages 中 ContentSlot 的作用:将C++代码实现的UI组件挂载到 UI 树上。在直接使用同样是 ArkTS 代码封装的组件时,公共的属性方法是可以在自定义组件之后继续调用的,然而,这一种方式在 ContentSlot 身上却不适用,屏幕前的你,尽管到 IDE 中在 ContentSlot之后用『.』去尝试调用哪些熟悉的 width、onClick 方法,会发现完全不支持。

这就说明,如果想要为这些用 C++ 代码实现的组件进行事件处理,就只能回到 Native 侧去添加相应的代码,好在最新的鸿蒙 API 特性已经对此进行了支持,具体如何在 C++ 中处理组件事件,下面便娓娓道来。
2、addNodeEventReceiver()
在 ArkTS 代码中,如果想要处理组件的事件如点击事件,直接使用 onClick 方法并传入回调函数即可,然而,到了 C++ 这边就没有这么简单了。在 C++ 这边,要捕获点击事件,需要在对应节点上注册一个事件监听器,该监听器会监听目标组件上的一切事件,不同的事件将由开发者自定义实现的事件分发处理代码进行分发。
具体到 API 上,鸿蒙 NDK 提供了一个 addNodeEventReceiver() 接口,去提供监听器注册能力:

当开启了时间监听的组件从 UI 树上摘下时,对应的监听器必须进行释放,从而避免内存泄露与资源占用,该操作可以通过下面的 API 接口进行实现:

addNodeEventReceiver() 往往与 removeNodeEventReceiver() 成对出现,形成监听器生命周期的闭环。
3、registerNodeEvent()
在组件上注册了事件监听器之后,就需要考虑具体要对哪些事件进行处理,换句话说,需要设置事件监听器具体监听什么事件、不监听什么事件。
为事件监听器设置监听目标的操作,主要通过 registerNodeEvent() 去声明:

与事件监听器本身的开启和关闭,是通过不同的两个 API 实现操作的一样,监听器目标的添加和移除,也是由两个 API 实现的。与 registerNodeEvent() 相互配合的另一个 API,就是 unregisterNodeEvent():

在 registerNodeEvent() 与 unregisterNodeEvent()、addNodeEventReceiver() 与 removeNodeEventReceiver() 这两对 API 的名称上,不难看出具有命名规范的官方 API 往往在取名的时候,会充分遵循 API 功能的连贯性,去选择彼此具有反义关系而词根相一致的单词,这一点也是大家需要充分吸纳的做法。
4、ArkUI_NodeEventType
C++ 代码中支持监听、或者说捕获的组件事件的类型,都在 ArkUI_NodeEventType 中进行了声明。
虽然 ArkUI_NodeEventType 整体是从 API 12 这一版本开始的,但具体到不同的组件事件,开始支持的版本并不相同,比如点击事件对应的 NODE_ON_CLICK_EVENT,API 的起始版本是 18。
5、整体流程
Native 侧监听组件事件的整体流程,可以归纳如下:
1)用addNodeEventReceiver() 在目标组件上注册一个事件监听器,在监听器中完成事件的分发;
2)用 registerNodeEvent() 声明具体要监听的 ArkUI_NodeEventType,如 NODE_ON_CLICK_EVENT;
3)在自定义事件处理函数中完成对目标事件的处理。
4)处理完事件需要移除事件的监听,或者整个UI组件将要从 UI 树上摘下时,用 unregisterNodeEvent() 去释放监听;
5)当已经不在需要监听目标组件上的事件,或者整个组件将要结束生命,用 removeNodeEventReceiver() 去释放事件监听器。
二、代码实践
清楚了实现原理后,下面开始真正用代码去实现鸿蒙 NDK UI 组件事件监听功能。
1、封装 UI 回显
由于直接用 C++ 代码实现 UI 更新比较麻烦,或者说,基于我自身目前对鸿蒙 NDK UI 的学习程度,还没掌握相关比较好用的 API,只能采用往 Native 侧透传 ArkTS 方法的方式,去实现 UI 更新。
如何往 Native 侧透传一个 ArkTS 回调方法,起始在从零开始开发纯血鸿蒙应用之NAPI一篇中,已经介绍过了,但此次与之前的代码稍微不同的是,需要用到一组新的 NAPI:napi_create_reference 、napi_get_reference_value 和 napi_delete_reference。

1.1、保持 ArkTS 回调引用
在 cpp 目录下,新增一个 share.h 文件,编写如下代码完成 ArkTS 回调方法的引用持有:
#ifndef NATIVEPC_SHARE_H
#define NATIVEPC_SHARE_H
#include "napi/native_api.h"
#include <string>
extern napi_env g_env;
struct DynamicParams {std::string desc;std::string* value;
};inline DynamicParams* globalParams;
// 全局保存ArkTS回调的napi_ref
inline napi_ref g_clickCallbackRef = nullptr;//inline void OnListClick(ArkUI_NodeEvent* event) {
//
//}static napi_value NativeInvokeUpdateEventInfo(napi_env env, napi_callback_info info){size_t argc = 1;napi_value args[1] = { nullptr};napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);// 保存ArkTS回调引用napi_create_reference(env, args[0], 1, &g_clickCallbackRef);if (globalParams && globalParams->value) {napi_value argv = nullptr;std::string data = globalParams->desc + ": " + *globalParams->value;
// std::string data = "测试";napi_status status = napi_create_string_utf8(env, data.c_str(), data.length(), &argv);if(status != napi_ok) {return nullptr;}napi_value result = nullptr;status = napi_call_function(env, nullptr, args[0], 1, &argv, &result);if (status != napi_ok) {return nullptr;}return result;} else {// 处理空指针异常或默认值情况napi_value argv = nullptr;std::string defaultStr = "default";napi_status status = napi_create_string_utf8(env, defaultStr.c_str(), defaultStr.length(), &argv);if(status != napi_ok) {return nullptr;}napi_value result = nullptr;status = napi_call_function(env, nullptr, args[0], 1, &argv, &result);if (status != napi_ok) {return nullptr;}return result;}}#endif //NATIVEPC_SHARE_H
1.2、注册 NativeInvokeUpdateEventInfo 为 NAPI
在 src/main/cpp/types/libentry/Index.d.ts 中新增一行 export const updateEventInfo: (cb: (msg: string) => void ) => void 完成 C++ 方法到 ArkTS 侧的暴露,紧接着在 src/main/cpp/napi_init.cpp 的 Init 方法中完成 NativeInvokeUpdateEventInfo 的注册:
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{g_env = env;globalParams = new DynamicParams{"Default", new std::string("value")};// 将 globalParams 封装为 napi_value(示例使用 external)napi_value customData;napi_create_external(env, globalParams, nullptr, nullptr, &customData);napi_property_descriptor desc[] = {{ "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr },{"createNativeRoot", nullptr, NativeModule::CreateNativeRoot, nullptr, nullptr, nullptr, napi_default, nullptr},{"destroyNativeRoot", nullptr, NativeModule::DestroyNativeRoot, nullptr, nullptr, nullptr, napi_default, nullptr},{"updateEventInfo", nullptr, NativeInvokeUpdateEventInfo, nullptr, nullptr, customData, napi_default, nullptr}};napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);return exports;
}
EXTERN_C_END
1.3、释放 ArkTS 回调引用
由于 ArkTS 回调引用也是一种内存资源,为了避免内存泄露和资源占用,需要在 NativeEntry.cpp 中的 DestroyNativeRoot 方法中,用 napi_delete_reference(g_env, g_clickCallbackRef) 进行资源释放。
1.4、传入 ArkTS 回调方法
在 src/main/ets/pages/NativeListPage.ets 中,补充如下图所示的 ArkTS 代码。

UI 回显逻辑,在鸿蒙开发指南的监听组件事件中,并没有提供,是我自己根据自身的经验进行补充的,官方案例代码中只有一个日志的打印。
2、处理点击事件
准备好 UI 回显用的 ArkTS 回调方法后,可以开始安装官方案例进行组件点击事件处理功能的实现。
2.1、更新 ArkUINode.h
首先,新增几个私有成员字段:
private:std::function<void(ArkUI_NodeEvent *event)> onClick_;std::function<void()> onDisappear_;std::function<void()> onAppear_;std::function<void(int32_t type, float x, float y)> onTouch_;
其次,在protected成员区域,准备一个事件分发处理函数:
void ProcessNodeEvent(ArkUI_NodeEvent *event) {auto eventType = OH_ArkUI_NodeEvent_GetEventType(event);switch (eventType) {case NODE_ON_CLICK_EVENT: {if (onClick_) {onClick_(event);}break;}case NODE_TOUCH_EVENT: {if (onTouch_) {auto *uiInputEvent = OH_ArkUI_NodeEvent_GetInputEvent(event);float x = OH_ArkUI_PointerEvent_GetX(uiInputEvent);float y = OH_ArkUI_PointerEvent_GetY(uiInputEvent);auto type = OH_ArkUI_UIInputEvent_GetAction(uiInputEvent);onTouch_(type, x, y);}}case NODE_EVENT_ON_DISAPPEAR: {if (onDisappear_) {onDisappear_();}break;}case NODE_EVENT_ON_APPEAR: {if (onAppear_) {onAppear_();}break;}default: {// 组件特有事件交给子类处理OnNodeEvent(event);}}}
和一个事件监听器函数:
static void NodeEventReceiver(ArkUI_NodeEvent *event) {// 获取事件发生的UI组件对象。auto nodeHandle = OH_ArkUI_NodeEvent_GetNodeHandle(event);// 获取保持在UI组件对象中的自定义数据,返回封装类指针。auto *node = reinterpret_cast<ArkUINode *>(NativeModuleInstance::GetInstance()->GetNativeNodeAPI()->getUserData(nodeHandle));// 基于封装类实例对象处理事件。node->ProcessNodeEvent(event);}
最后,更新 public 区域的类构造函数和析构函数:
explicit ArkUINode(ArkUI_NodeHandle handle) : ArkUIBaseNode(handle) {nativeModule_ = NativeModuleInstance::GetInstance()->GetNativeNodeAPI();// 事件触发时需要通过函数获取对应的事件对象,这边通过设置节点自定义数据将封装类指针保持在组件上,方便后续事件分发。nativeModule_->setUserData(handle_, this);// 注册节点监听事件接受器。nativeModule_->addNodeEventReceiver(handle_, ArkUINode::NodeEventReceiver);}~ArkUINode() override {if (onClick_) {nativeModule_->unregisterNodeEvent(handle_, NODE_ON_CLICK_EVENT);}if (onTouch_) {nativeModule_->unregisterNodeEvent(handle_, NODE_TOUCH_EVENT);}if (onDisappear_) {nativeModule_->unregisterNodeEvent(handle_, NODE_EVENT_ON_DISAPPEAR);}if (onAppear_) {nativeModule_->unregisterNodeEvent(handle_, NODE_EVENT_ON_APPEAR);}nativeModule_->removeNodeEventReceiver(handle_, ArkUINode::NodeEventReceiver);}
以及添加对应的通用事件的注册函数,包括 onClick 在内:
// 处理通用事件。void RegisterOnClick(const std::function<void(ArkUI_NodeEvent *event)> &onClick) {assert(handle_);onClick_ = onClick;// 注册点击事件。nativeModule_->registerNodeEvent(handle_, NODE_ON_CLICK_EVENT, 0, globalParams);}void RegisterOnTouch(const std::function<void(int32_t type, float x, float y)> &onTouch) {assert(handle_);onTouch_ = onTouch;// 注册触碰事件。nativeModule_->registerNodeEvent(handle_, NODE_TOUCH_EVENT, 0, nullptr);}void RegisterOnDisappear(const std::function<void()> &onDisappear) {assert(handle_);onDisappear_ = onDisappear;// 注册卸载事件。nativeModule_->registerNodeEvent(handle_, NODE_EVENT_ON_DISAPPEAR, 0, nullptr);}void RegisterOnAppear(const std::function<void()> &onAppear) {assert(handle_);onAppear_ = onAppear;// 注册挂载事件。nativeModule_->registerNodeEvent(handle_, NODE_EVENT_ON_APPEAR, 0, nullptr);}
2.2、更新 ArkUIListNode.h
ArkUIListNode 类中,同样需要增加一些事件处理相关的代码。主要调整,一是在 protected 区域新增一个 OnNodeEvent 方法:
// 处理List相关事件。
void OnNodeEvent(ArkUI_NodeEvent *event) override {auto eventType = OH_ArkUI_NodeEvent_GetEventType(event);switch (eventType) {case NODE_LIST_ON_SCROLL_INDEX: {auto index = OH_ArkUI_NodeEvent_GetNodeComponentEvent(event)->data[0];if (onScrollIndex_) {onScrollIndex_(index.i32);}}default: {}}
}
而是在 public 区域加上:
// 注册列表相关事件。
void RegisterOnScrollIndex(const std::function<void(int32_t index)> &onScrollIndex) {assert(handle_);onScrollIndex_ = onScrollIndex;nativeModule_->registerNodeEvent(handle_, NODE_LIST_ON_SCROLL_INDEX, 0, nullptr);
}
2.3、更新 NormalTextListExample.h
官方原本的案例,大家不妨去这里查看,这里我贴出我自己的改造代码。
我自己的改造,主要有如下变动:
2.3.1、新增一个按钮
基于自己对鸿蒙 NDK UI 节点实现代码的了解,自己定义并实现了一个按钮节点,并将其用在了上一篇中的 list 组件上:
auto btnText = std::make_shared<ArkUITextNode>();btnText->SetTextContent("点击");btnText->SetFontSize(16);btnText->SetFontColor(0xFFffffff);button->AddChild(btnText);button->RegisterOnClick([button](ArkUI_NodeEvent *event) {delete globalParams->value;globalParams->value = new std::string("按钮被点击");globalParams->desc = "点击列表项";std::string data = globalParams->desc + ": " + *globalParams->value;napi_env env = g_env; // 获取当前napi环境napi_value callback;napi_get_reference_value(env, g_clickCallbackRef, &callback);// 构造传递参数napi_value argv;napi_create_string_utf8(env, data.c_str(), data.length(), &argv);// 调用ArkTS回调napi_value result;napi_call_function(env, nullptr, callback, 1, &argv, &result);});list->AddChild(button);
2.3.2、实现一个 onClick 函数:
为了处理列表项的点击事件,需要实现一个 onClick 函数,我是基于官方案例进行代码的增加,主要就是用上之前准备好的 UI 回显:
listItem->AddChild(textNode);// 列表项注册点击事件。auto onClick = [i](ArkUI_NodeEvent *event) {delete globalParams->value;globalParams->value = new std::string("第" + std::to_string(i) + "个列表项被点击");globalParams->desc = "点击列表项";std::string data = globalParams->desc + ": " + *globalParams->value;napi_env env = g_env; // 获取当前napi环境napi_value callback;napi_get_reference_value(env, g_clickCallbackRef, &callback);// 构造传递参数napi_value argv;napi_create_string_utf8(env, data.c_str(), data.length(), &argv);// 调用ArkTS回调napi_value result;napi_call_function(env, nullptr, callback, 1, &argv, &result);// 从组件事件中获取基础事件对象auto *inputEvent = OH_ArkUI_NodeEvent_GetInputEvent(event);if (inputEvent == nullptr) {return;}// 从组件事件获取事件类型auto eventType = OH_ArkUI_NodeEvent_GetEventType(event);OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "eventInfo", "inputEvent = %{public}p", inputEvent);OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "eventInfo", "eventType = %{public}d", eventType);auto componentEvent = OH_ArkUI_NodeEvent_GetNodeComponentEvent(event);// 获取组件事件中的数字类型数据OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "eventInfo", "componentEvent = %{public}p",componentEvent);// 获取触发该事件的组件对象auto nodeHandle = OH_ArkUI_NodeEvent_GetNodeHandle(event);if (nodeHandle == nullptr) {return;}OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "eventInfo", "nodeHandle = %{public}p", nodeHandle);// 根据eventType来区分事件类型,进行差异化处理,其他获取事件信息的接口也可类似方式来进行差异化的处理switch (eventType) {case NODE_ON_CLICK_EVENT: {// 触发点击事件所进行的操作,从基础事件获取事件信息auto x = OH_ArkUI_PointerEvent_GetX(inputEvent);auto y = OH_ArkUI_PointerEvent_GetY(inputEvent);auto displayX = OH_ArkUI_PointerEvent_GetDisplayX(inputEvent);auto displayY = OH_ArkUI_PointerEvent_GetDisplayY(inputEvent);auto windowX = OH_ArkUI_PointerEvent_GetWindowX(inputEvent);auto windowY = OH_ArkUI_PointerEvent_GetWindowY(inputEvent);auto pointerCount = OH_ArkUI_PointerEvent_GetPointerCount(inputEvent);auto xByIndex = OH_ArkUI_PointerEvent_GetXByIndex(inputEvent, 0);auto yByIndex = OH_ArkUI_PointerEvent_GetYByIndex(inputEvent, 0);auto displayXByIndex = OH_ArkUI_PointerEvent_GetDisplayXByIndex(inputEvent, 0);auto displayYByIndex = OH_ArkUI_PointerEvent_GetDisplayYByIndex(inputEvent, 0);auto windowXByIndex = OH_ArkUI_PointerEvent_GetWindowXByIndex(inputEvent, 0);auto windowYByIndex = OH_ArkUI_PointerEvent_GetWindowYByIndex(inputEvent, 0);auto pointerId = OH_ArkUI_PointerEvent_GetPointerId(inputEvent, 0);auto pressure = OH_ArkUI_PointerEvent_GetPressure(inputEvent, 0);auto action = OH_ArkUI_UIInputEvent_GetAction(inputEvent);auto eventTime = OH_ArkUI_UIInputEvent_GetEventTime(inputEvent);auto sourceType = OH_ArkUI_UIInputEvent_GetSourceType(inputEvent);auto type = OH_ArkUI_UIInputEvent_GetType(inputEvent);std::string eventInfo ="x: " + std::to_string(x) + ", y: " + std::to_string(y) +", displayX: " + std::to_string(displayX) + ", displayY: " + std::to_string(displayY) +", windowX: " + std::to_string(windowX) + ", windowY: " + std::to_string(windowY) +", pointerCount: " + std::to_string(pointerCount) + ", xByIndex: " + std::to_string(xByIndex) +", yByIndex: " + std::to_string(yByIndex) +", displayXByIndex: " + std::to_string(displayXByIndex) +", displayYByIndex: " + std::to_string(displayYByIndex) +", windowXByIndex: " + std::to_string(windowXByIndex) +", windowYByIndex: " + std::to_string(windowYByIndex) +", pointerId: " + std::to_string(pointerId) + ", pressure: " + std::to_string(pressure) +", action: " + std::to_string(action) + ", eventTime: " + std::to_string(eventTime) +", sourceType: " + std::to_string(sourceType) + ", type: " + std::to_string(type);OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "eventInfoOfCommonEvent", "eventInfo = %{public}s",eventInfo.c_str());}default: {break;}}};listItem->RegisterOnClick(onClick);list->AddChild(listItem);
onClick 函数体内部,最上面的一段代码,便是我自己添加的。
在列表项的循环体之外,还有如下几行代码:
// 3:注册List相关监听事件.
list->RegisterOnScrollIndex([](int32_t index) { OH_LOG_INFO(LOG_APP, "on list scroll index: %{public}d", index); });
// 4: 注册挂载事件。
list->RegisterOnAppear([]() { OH_LOG_INFO(LOG_APP, "on list mount to tree"); });
// 5: 注册卸载事件。
list->RegisterOnDisappear([]() { OH_LOG_INFO(LOG_APP, "on list unmount from tree"); });
2.4、完整项目地址
由于改造的地方比较多,为了避免大家没有实现预期的功能,这里给出对应的项目仓库地址:NativePC。
三、总结
由于自己的设备的 API 版本都比较低,所以,还没有亲眼看到上述代码的运行效果,只是在鸿蒙开发者平台的问题工单的对话记录中,看到了别人替我运行的效果,具体看最开始的那张图片。
