〇、前言

这一篇,是鸿蒙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_referencenapi_get_reference_valuenapi_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 版本都比较低,所以,还没有亲眼看到上述代码的运行效果,只是在鸿蒙开发者平台的问题工单的对话记录中,看到了别人替我运行的效果,具体看最开始的那张图片。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐