HarmonyOS 6 自定义人脸识别模型 4:OH_ArkUI_SurfaceHolder 绘制方式介绍

作者:轻口味
  • 2026-04-06
    北京
  • 本文字数:7676 字

    阅读完需:约 25 分钟

HarmonyOS 6 自定义人脸识别模型4:OH_ArkUI_SurfaceHolder绘制方式介绍

前面文章《HarmonyOS 6 自定义人脸识别模型 2:OH_NativeXComponent 方式绘制》、《HarmonyOS 6 自定义人脸识别模型 3:OH_NativeXComponent 基于 OpenGL 绘制》介绍了如何将 ArkTS 层的 XComponent 与 C++层的 OH_NativeXComponent 进行关联与映射,并使用 OpenGL 方式进行绘制。


使用 OH_NativeXComponent 方式存在一些弊端:


  • OH_NativeXComponent 实例生命周期与 XComponent 组件强相关,如果我们在 XComponent 组件销毁后仍然操作该对象将可能出现稳定性问题,造成应用的崩溃。

  • OH_NativeXComponent 提供的交互事件接口不够丰富,只提供基础的触摸、鼠标、键盘交互接口,我们如果想识别长按、拖拽等高级手势需要自己写识别逻辑。


HarmonyOS 提供了 OH_ArkUI_SurfaceHolder 来替代 OH_NativeXComponent 相关接口,文本接着介绍如何在 C++中通过 OpenGL 在 OH_ArkUI_surfaceHolder 中进行绘制等操作。

为什么要迁移到 OH_ArkUI_SurfaceHolder?

在 HarmonyOS 6 及更高版本中,推荐使用 OH_ArkUI_SurfaceHolder 配合 FrameNode 进行 Native 渲染。其核心优势在于:


  1. 更稳定的生命周期SurfaceHolder 作为独立的对象管理,解耦了 Native 资源与组件树的直接绑定,提供了更显式的创建与销毁回调。

  2. 丰富的事件体系:通过 ArkUI NDK 的 NodeEvent 接口,可以接入更完整的手势系统,支持手势拦截、冒泡等高级控制。

  3. 更灵活的组件管理:支持手动创建 FrameNode 并在 Native 侧进行属性设置(如 initialize),更符合现代复杂 UI 的按需渲染逻辑。

开发流程解析

1. ArkTS 层:手动绑定 Node

可以直接使用 XComponent 或者通过动态创建方式创建,这里我们通过native.bindNode(this.xComponentId, this.xComponent)XComponent 作为 FrameNode 手动绑定到 Native 侧。


// Index.etsclass MyNodeController extends NodeController {    public xComponent: typeNode.XComponent | undefined = undefined;    public xComponentId: string = 'xcp' + (new Date().getTime());    public node: FrameNode | undefined = undefined;    public column: typeNode.Column | undefined = undefined;    public attachButton: typeNode.Button | undefined = undefined;    public detachButton: typeNode.Button | undefined = undefined;  makeNode(uiContext: UIContext): FrameNode | null {    this.node = new FrameNode(uiContext);    // 创建 XComponent 类型的 Node    this.xComponent = typeNode.createNode(uiContext, 'XComponent', { type: XComponentType.SURFACE });        // 必须进行初始化,此时可以设置起始尺寸    this.xComponent.initialize({ type: XComponentType.SURFACE })      .width('100%')      .height('100%');      this.xComponent.attribute      .id(this.xComponentId)      .hitTestBehavior(HitTestMode.Block); // 确保事件不被拦截      // 调用 NAPI 接口,将 Node 句柄传给 C++    native.bindNode(this.xComponentId, this.xComponent);        this.node.appendChild(this.xComponent);    return this.node;  }}
NodeContainer(this.myNodeController) .width(200) .height(200) .focusable(true) .focusOnTouch(true) .defaultFocus(true)
复制代码
2. Native 层:获取 SurfaceHolder 并注册回调

在 C++ 端,我们通过 NAPI 获取 Node 句柄,并创建对应的 SurfaceHolder。以下是核心实现及其系统函数的详细解析:


// plugin_manager.cppvoid OnSurfaceCreatedNative(OH_ArkUI_SurfaceHolder *holder) {    auto window =        OH_ArkUI_XComponent_GetNativeWindow(holder); // 获取native window    auto render =        reinterpret_cast<EGLRender *>(OH_ArkUI_SurfaceHolder_GetUserData(holder));    render->SetUpEGLContext(window); // 初始化egl环境  }    void OnSurfaceChangedNative(OH_ArkUI_SurfaceHolder *holder, uint64_t width,                              uint64_t height) {    EGLRender *render =        reinterpret_cast<EGLRender *>(OH_ArkUI_SurfaceHolder_GetUserData(holder));    render->SetEGLWindowSize(width, height); // 设置绘制区域大小    render->DrawSphere(true);                // 绘制球体  }    void OnSurfaceDestroyedNative(OH_ArkUI_SurfaceHolder *holder) {    OH_LOG_Print(LOG_APP, LOG_ERROR, 0xff00, "onBind", "on destroyed");    EGLRender *render =        reinterpret_cast<EGLRender *>(OH_ArkUI_SurfaceHolder_GetUserData(holder));    render->DestroySurface(); // 销毁eglSurface相关资源  }
napi_value PluginManager::BindNode(napi_env env, napi_callback_info info) { ArkUI_NodeHandle handle; // 1. 获取 Node 句柄 OH_ArkUI_GetNodeHandleFromNapiValue(env, args[1], &handle); // 2. 创建 SurfaceHolder 管理对象 OH_ArkUI_SurfaceHolder *holder = OH_ArkUI_SurfaceHolder_Create(handle); // 3. 绑定自定义数据 (UserData) auto render = new EGLRender(); OH_ArkUI_SurfaceHolder_SetUserData(holder, render);
// 4. 配置生命周期回调 auto callback = OH_ArkUI_SurfaceCallback_Create(); OH_ArkUI_SurfaceCallback_SetSurfaceCreatedEvent(callback, OnSurfaceCreatedNative); OH_ArkUI_SurfaceCallback_SetSurfaceChangedEvent(callback, OnSurfaceChangedNative); OH_ArkUI_SurfaceCallback_SetSurfaceDestroyedEvent(callback, OnSurfaceDestroyedNative); // 5. 将回调注册到 Holder OH_ArkUI_SurfaceHolder_AddSurfaceCallback(holder, callback); // 6. 注册事件监听 (触摸手势) nodeAPI->addNodeEventReceiver(handle, onEvent); nodeAPI->registerNodeEvent(handle, NODE_TOUCH_EVENT, 0, nullptr); return nullptr;}
复制代码


在 OnSurfaceChangedNative 回调中开启真正的绘制


系统函数详解


BindNode 方法中涉及到的系统 API 如下:



生命周期回调详解


SurfaceHolder 体系中,渲染逻辑通常分布在以下两个核心回调中:


  • OnSurfaceCreatedNative(OH_ArkUI_SurfaceHolder *holder)

  • 触发时机:当 XComponent 对应的底层 Surface 成功创建并在内存中准备好时触发。

  • 职责

  • 通过 OH_ArkUI_XComponent_GetNativeWindow(holder) 获取 NativeWindow 指针。

  • 执行 EGL 环境初始化(包括创建 Display、Config、Context 和 Surface)。

  • 此时还不适合进行频繁绘制,因为尺寸可能尚未最终确定。

  • OnSurfaceChangedNative(OH_ArkUI_SurfaceHolder *holder, uint64_t width, uint64_t height)

  • 触发时机:Surface 尺寸发生变化(如组件初始化完成、转屏、分屏)时触发。首次创建后也会立即触发一次。

  • 职责

  • 获取最新的 widthheight 像素值。

  • 更新 OpenGL 的 Viewport(视口区域)。

  • 发起首帧绘制。这是确保应用启动后屏幕不黑屏的关键逻辑点。

  • 在分屏交互中,此方法会被频繁调用以保证画面按比例缩放。

3. 触摸手势事件识别

使用 OH_ArkUI_SurfaceHolder 方式时,事件通过 onEvent 统一分发。需要特别注意的是触摸动作常量的对应:



代码实现示例:


void onEvent(ArkUI_NodeEvent *event) {    auto eventType = OH_ArkUI_NodeEvent_GetEventType(event);    if (eventType == NODE_TOUCH_EVENT) {      ArkUI_NodeHandle handle = OH_ArkUI_NodeEvent_GetNodeHandle(event);      auto holder = PluginManager::surfaceHolderMap_[handle];      if (holder == nullptr) {        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "onEvent",                     "holder is null for handle %{public}p", handle);        return;      }      EGLRender *render = reinterpret_cast<EGLRender *>(          OH_ArkUI_SurfaceHolder_GetUserData(holder));      if (render == nullptr) {        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "onEvent",                     "render is null");        return;      }        auto inputEvent = OH_ArkUI_NodeEvent_GetInputEvent(event);      if (inputEvent == nullptr) {        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "onEvent",                     "inputEvent is null");        return;      }      int32_t action = OH_ArkUI_UIInputEvent_GetAction(inputEvent);      float x = OH_ArkUI_PointerEvent_GetX(inputEvent);      float y = OH_ArkUI_PointerEvent_GetY(inputEvent);        OH_LOG_Print(          LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "onEvent",          "Touch: action=%{public}d, x=%{public}f, y=%{public}f, type=%{public}d",          action, x, y, render->drawType_);        if (action == UI_TOUCH_EVENT_ACTION_DOWN) { // 1        render->lastX_ = x;        render->lastY_ = y;      } else if (action == UI_TOUCH_EVENT_ACTION_MOVE) { // 2        float deltaX = x - render->lastX_;        float deltaY = y - render->lastY_;        if (std::abs(deltaX) > 0.1f || std::abs(deltaY) > 0.1f) {          OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "onEvent",                       "Rotating: dx=%{public}f, dy=%{public}f", deltaX, deltaY);          render->UpdateRotation(deltaX, deltaY);          render->lastX_ = x;          render->lastY_ = y;            // 根据当前绘制类型强制重绘          if (render->drawType_ == 1) {            render->DrawSphere(false);          } else {            render->DrawStar(false);          }        }      }    }  }
复制代码


触摸事件负责将 XComponent 上的触摸手势转化为 3D 物体(球体、立方体)的旋转效果。系统调用 onEvent 时会传入一个 event。代码首先过滤出触摸事件 (NODE_TOUCH_EVENT),并通过映射表找到该节点对应的 SurfaceHolder 和 EGLRender 渲染器实例。


  • 当用户手指按下屏幕时,记录下当前的坐标。这作为后续计算滑动距离的“参考点”。

  • 通过当前坐标与上一次坐标的差值 (delta),确定用户滑动的方向和距离。

  • 通过 UpdateRotation 方法将 deltaX/Y 累加到渲染器的偏航角 (yaw) 和仰角 (pitch) 变量中

  • 在 UpdateRotation 修改了 3D 变换变量后,立即调用 DrawSphere 重绘画面。由于 OpenGL 渲染极快,用户会感觉到 3D 物体在随手指实时旋转。

精准绘制:3D 球体逻辑实现

之前文章介绍的是五角星绘制,本问介绍一个具有高度立体感的 3D 红色球体的绘制。

几何生成

通过经纬度法(Spherical Coordinates)生成球体网格。为了提升立体感,我们将分段数设置为 64x32,确保曲面平滑无棱角。数学原理为球面上任意一点可通过参数方程 计算得出。这里生成了 64x32 个网格点,保证球面的圆滑度。

光照模型 (Blinn-Phong)

为了表现球体的立体感,我们在 CPU 端预计算了光照效果:


  • 漫反射 (Diffuse):根据顶点法线与光源方向的点积计算。

  • 镜面高光 (Specular):引入 Blinn-Phong 模型,计算半程向量(Halfway Vector),实现在球面上产生一个明亮的聚光点,营造金属或光滑材质的质感。

  • 边缘光 (Rim Light):通过法线与视角方向的夹角计算增强轮廓感,使球体从背景中脱颖而出。下面是详细代码实现和说明:


void EGLRender::DrawSphere(bool drawColor) {    OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "EGLRender", "DrawSphere");    drawType_ = 1;  //标记当前为球体绘制模式  GLint position = PrepareDraw();  // 设置 Viewport,清理颜色缓冲区,启用着色器程序  if (position == POSITION_ERROR) {      OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLRender",                   "DrawSphere get position failed");      return;    }      // 绘制背景    if (!ExecuteDraw(position, BACKGROUND_COLOR, BACKGROUND_RECTANGLE_VERTICES)) {      OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLRender",                   "DrawSphere execute draw background failed");      return;    }      // 生成球体顶点和索引 (经纬度法)    const float radius = 0.6f;    const int sectors = 64; // 增加分段,使球面更圆滑    const int stacks = 32;  // 增加分段    std::vector<GLfloat> vertices;    std::vector<GLfloat> colors;    std::vector<GLuint> indices;      float x, y, z, xy;    float sectorStep = 2 * M_PI / sectors;    float stackStep = M_PI / stacks;    float sectorAngle, stackAngle;      for (int i = 0; i <= stacks; ++i) {      stackAngle = M_PI / 2 - i * stackStep; // 从 pi/2 到 -pi/2    xy = radius * cosf(stackAngle);        // r * cos(u)      z = radius * sinf(stackAngle);         // r * sin(u)        for (int j = 0; j <= sectors; ++j) {        sectorAngle = j * sectorStep; // 从 0 到 2pi      x = xy * cosf(sectorAngle);   // r * cos(u) * cos(v)        y = xy * sinf(sectorAngle);   // r * cos(u) * sin(v)          // 应用旋转 (CPU 预处理)        float rx = x;        float ry = y * cosf(pitch_) - z * sinf(pitch_);        float rz = y * sinf(pitch_) + z * cosf(pitch_);          float finalX = rx * cosf(yaw_) + rz * sinf(yaw_);        float finalY = ry;        float finalZ = -rx * sinf(yaw_) + rz * cosf(yaw_);          vertices.push_back(finalX);        vertices.push_back(finalY);        vertices.push_back(finalZ);          // 计算法向 (对于球心在原点的球,法向即归一化的原始坐标)        float nx = x / radius;        float ny = y / radius;        float nz = z / radius;          // 对法向应用相同的旋转,使其与世界空间的光源对齐        float rnx = nx;        float rny = ny * cosf(pitch_) - nz * sinf(pitch_);        float rnz = ny * sinf(pitch_) + nz * cosf(pitch_);          float finalNx = rnx * cosf(yaw_) + rnz * sinf(yaw_);        float finalNy = rny;        float finalNz = -rnx * sinf(yaw_) + rnz * cosf(yaw_);          // 简单平行光模型 (光源方向从右上角进入 [1, 1, 1])      float lx = 1.0f, ly = 1.0f, lz = 1.0f;        float lightLen = sqrtf(lx * lx + ly * ly + lz * lz);        lx /= lightLen;        ly /= lightLen;        lz /= lightLen;          // 点积计算漫反射强度 (Lambertian shading)      float diffuse = finalNx * lx + finalNy * ly + finalNz * lz;        diffuse = std::max(0.1f, diffuse); // 加一点环境光(0.1),防止阴影处纯黑          // 镜面高光反射 (Specular Highlight - Blinn-Phong)      float specPower = 50.0f;        float vx = 0.0f, vy = 0.0f, vz = 1.0f; // 视角方向 (正对屏幕)        float hx = lx + vx, hy = ly + vy, hz = lz + vz;        float hLen = sqrtf(hx * hx + hy * hy + hz * hz);        hx /= hLen;        hy /= hLen;        hz /= hLen;        float spec =            powf(std::max(0.0f, finalNx * hx + finalNy * hy + finalNz * hz),                 specPower);          // 边缘光 (Rim Light) - 增强轮廓立体感        float rim =            1.0f - std::max(0.0f, finalNx * vx + finalNy * vy + finalNz * vz);        rim = powf(rim, 3.0f);          // 综合光照计算,实现金属/塑料质感        float baseR = 0.95f, baseG = 0.05f, baseB = 0.05f; // 主色调        colors.push_back(            std::min(1.0f, baseR * diffuse + 0.1f + spec * 0.8f + rim * 0.2f));        colors.push_back(            std::min(1.0f, baseG * diffuse + 0.05f + spec * 0.8f + rim * 0.1f));        colors.push_back(            std::min(1.0f, baseB * diffuse + 0.05f + spec * 0.8f + rim * 0.1f));        colors.push_back(1.0f);      }    }      // 生成索引    for (int i = 0; i < stacks; ++i) {      int k1 = i * (sectors + 1); // 当前行起点      int k2 = k1 + sectors + 1;  // 下一行起点        for (int j = 0; j < sectors; ++j, ++k1, ++k2) {        if (i != 0) {          indices.push_back(k1);          indices.push_back(k2);          indices.push_back(k1 + 1);        }        if (i != (stacks - 1)) {          indices.push_back(k1 + 1);          indices.push_back(k2);          indices.push_back(k2 + 1);        }      }    }      // 设置顶点属性指针 (3个分量: x, y, z)    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 0, vertices.data());    glEnableVertexAttribArray(position);      // 设置颜色属性指针 (4个分量: r, g, b, a)    glEnableVertexAttribArray(1);    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, colors.data());      // 开启深度测试    glEnable(GL_DEPTH_TEST);    glClear(GL_DEPTH_BUFFER_BIT);      // 绘制三角形索引 mesh  glDrawElements(GL_TRIANGLES, (GLsizei)indices.size(), GL_UNSIGNED_INT,                   indices.data());      glDisableVertexAttribArray(position);    glDisableVertexAttribArray(1);    glDisable(GL_DEPTH_TEST);      // 提交绘制并交换缓冲    glFlush();    glFinish();    if (!eglSwapBuffers(eglDisplay_, eglSurface_)) {      OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLRender",                   "DrawSphere FinishDraw failed");    }  }
复制代码
效果

渲染出的球体可以触摸 XComponent 组件进行滑动旋转,通过改变光照参数看到不同效果。




总结

OH_ArkUI_SurfaceHolder 的引入是 HarmonyOS 原生开发向更稳健、更灵活架构演进的重要一步。它不仅解决了 XComponent 强耦合带来的稳定性隐患,还通过 ArkUI NDK 赋予了开发者更强大的交互控制能力。虽然迁移过程在事件常量匹配和节点初始化上需要更细致的对接,但换来的性能与可靠性提升是非常显著的。


示例代码地址:https://github.com/qingkouwei/ArkUISurfaceHolder


发布于: 2026-04-06阅读数: 83
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017-10-17 加入

Android、音视频、AI相关领域从业者。 欢迎加我微信wodekouwei拉您进InfoQ音视频沟通群 邮箱:qingkouwei@gmail.com

评论

发布
暂无评论