ArkUI开发 - 利用C-API(NDK)开发UI控件

1,264 阅读7分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

通过前文ArkUI Engine系列,我们明白了在ArkTS侧一系列基础组件,最终其实都会映射为C++环境下的实现,比如ts侧的Row最终转换为C++侧的RowComponent,在Engine中被赋予属性并参与后续的一些列布局

创建一个Row,并设置对应的间距
void RowModelImpl::Create(const std::optional<Dimension>& space, AlignDeclaration* declaration, const std::string& tag)
{
    std::list<RefPtr<Component>> children;
    RefPtr<RowComponent> rowComponent =
        AceType::MakeRefPtr<OHOS::Ace::RowComponent>(FlexAlign::FLEX_START, FlexAlign::CENTER, children);
    ViewStackProcessor::GetInstance()->ClaimElementId(rowComponent);
    rowComponent->SetMainAxisSize(MainAxisSize::MIN);
    rowComponent->SetCrossAxisSize(CrossAxisSize::MIN);
    if (space.has_value() && space->Value() >= 0.0) {
        rowComponent->SetSpace(space.value());
    }
    if (declaration != nullptr) {
        rowComponent->SetAlignDeclarationPtr(declaration);
    }
    ViewStackProcessor::GetInstance()->Push(rowComponent);
}

虽然前期开发者大部分的UI代码编写都在ArkTS侧,但是一些其他特殊场景下,开发者更加希望直接调用C++侧的最终实现,最终在这种诉求下,鸿蒙next官方也提出了C-API的接口,通过这些接口,开发者可以直接在C++侧创建出对应的UI产物

ArkUI 提供C/C++环境接口

跨平台方案中,其中有比较代表性的就是ReactNative了,在ReactNative中,开发者可以直接通过编写js组件代码,便可以通过映射关系转换为对应平台的原生控件实现。

image.png

在ReactNative中,最终原生控件生成需要进行一层映射,比如Yoga生成的构建树需要转换为对应原生控件的树,但是在鸿蒙中,转换流程却略微复杂。鸿蒙中对应的原生控件即ArkTS侧的组件,如下图:

我们发现控件的创建需要经过多层的复杂映射,这无疑是对性能有了进一步的损耗,同时ArkTS本身的中间产物就是js,最终映射产物是C++侧的控件实现,但是两者均无法在运行时更方便的融合入ReactNative方案中。C-API方案就应运而生了:

引用自华为官方帖子

C-API方案 可以不仅可以运用在ReactNative中,很多跨平台方案也可以运用,还有特殊高性能场景下我们也能够使用

使用C-API进行UI布局

ArkUI提供了一系列接口,能够让开发者通过C/C++以命令式进行UI控件的需求开发,目前支持的内容有UI布局、UI组件生成、动画、弹窗、响应事件等等。下面我们以官方的例子,让我们对C-API 的了解更近一步。

创建鸿蒙C++工程

运用C-API,首先我们还是要创建一个NativeC++工程:

工程生成后就是一个标准C++工程,因为本次的重点是如何使用C-API进行UI的布局,所以这里有关napi的介绍我们就不再一一列举了,笔者之前在Harmony OS 应用开发 - 如何迁移Crash监控 这篇文章中,有对napi进行过介绍。

导入链接库

我们需要鸿蒙的napi能力以及C-API能力,因此需要在CMake中链接以下两个库libace_napi.z.so libace_ndk.z.so

target_link_libraries(entry PUBLIC libace_napi.z.so libace_ndk.z.so)

创建Native控件三部曲

查询获取ArkUI接口对象

在C-API中,提供了以下四种枚举给开发者,开发者需要通过OH_ArkUI_GetModuleInterface 这个接口查询获得支持的能力

typedef enum {
    /** API related to UI components. For details, see the struct definition in <arkui/native_node.h>. */
ARKUI_NATIVE_NODE,
    /** API related to dialog boxes. For details, see the struct definition in <arkui/native_dialog.h>. */
ARKUI_NATIVE_DIALOG,
    /** API related to gestures. For details, see the struct definition in <arkui/native_gesture.h>. */
ARKUI_NATIVE_GESTURE,
    /** API related to animations. For details, see the struct definition in <arkui/native_animate.h>.*/
ARKUI_NATIVE_ANIMATE,
} ArkUI_NativeAPIVariantKind;

OH_ArkUI_GetModuleInterface接受3个参数,第一个是需要调用能力的枚举值,即我们上文提到的ArkUI_NativeAPIVariantKind,第二个是ArkUI中关于对应能力的版本类型,这里官方目前是ArkUI_NativeNodeAPI_1 ,第三个参数是系统返回给我们的一个句柄,后续我们创建一些控件,比如创建一个Text都需要它,因此我们都会通过传入一个指针获取

OH_ArkUI_GetModuleInterface(nativeAPIVariantKind, structType, structPtr)        

比如我们想要在Native侧进行生成一个UI控件

ArkUI_NativeNodeAPI_1 *arkUINativeNodeApi_ = nullptr;
OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, arkUINativeNodeApi_);

创建一个Native控件对象

比如我们想要生成一个Native侧的控件Text,我们可以通过第一步获取的arkUINativeNodeApi_调用它的createNode方法,即可获得一个Text控件的Native句柄(后续可以操作Text的属性,比如背景、颜色等等),createNode方法接受一个参数,这个参数代表着我们即将创建的控件类型

 /** Custom node. */
ARKUI_NODE_CUSTOM = 0,
/** Text. */
ARKUI_NODE_TEXT = 1,
/** Text span. */
ARKUI_NODE_SPAN = 2,
/** Image span. */
ARKUI_NODE_IMAGE_SPAN = 3,
/** Image. */
ARKUI_NODE_IMAGE = 4,
/** Toggle. */
ARKUI_NODE_TOGGLE = 5,

createNode返回一个ArkUI_NodeHandle类型的对象,这个对象用于我们后续对Text的属性进行操作,大家可以简单理解为Text的Native句柄

ArkUI_NodeHandle textHandle = arkUINativeNodeApi_ -> createNode(ARKUI_NODE_TEXT) 

设置Native 控件的属性

有了textHandle ,我们就可以通过arkUINativeNodeApi_ 对象的setAttribute 方法设置对应的属性,属性也是一个枚举(在native_node.h 中),比如我们设置了Text的NODE_FONT_SIZE,即字体大小

void SetFontSize(float fontSize) {
    assert(handle_);
    // 创建一个中间对象,ArkUI_NumberValue 包含者float double等native类型,这里我们设置想要的数值即可
    ArkUI_NumberValue value[] = {{.f32 = fontSize}};
    ArkUI_AttributeItem item = {value, 1};
    // 设置Text的大小
    arkUINativeNodeApi_->setAttribute(textHandle, NODE_FONT_SIZE, &item);
}

通过这一步,我们就创建了一个Text控件,并设置了它的一些属性,如字体大小。但是我们仅仅只是创建了这么一个对象,我们还没有把它跟当前展示的UI绑定起来

绑定创建好的控件到当前UI中

使用ndk创建的控件也并不能完全脱离ArkTS,我们还需要在ArkTS侧创建一个占位的控件,即ContentSlot,ContentSlot后续还会接受一个NodeContent对象,我们的Native侧创建的控件,就会被挂载在这个控件当中

import { NodeContent } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  // 初始化NodeContent对象。
  private rootSlot = new NodeContent();
  aboutToAppear(): void {
  需要调用一个napi方法,把NodeContent对象通过napi传递给C++侧,这样C++的控件才能与当前UI进行绑定
  nativeNode.createNativeRoot(this.rootSlot)
  }
  build() {
    Column() {
      Row() {
        // 将NodeContent和ContentSlot占位组件绑定。
        ContentSlot(this.rootSlot)
      }.layoutWeight(1).onApear
    }
    .width('100%')
    .height('100%')
  }
}

我们选择在合适的时候调用一个napi方法,用于把NodeContent对象通过napi传递到C++侧,后续我们创建的控件就依靠这个对象添加到当前的UI树中

napi_value CreateNativeRoot(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);

    // 获取NodeContent
    ArkUI_NodeContentHandle contentHandle;
    OH_ArkUI_GetNodeContentFromNapiValue(env, args[0], &contentHandle);
    
    return nullptr;
}

通过OH_ArkUI_GetNodeContentFromNapiValue 接口,我们获取到ArkTS侧传递的NodeContent对象,这个对象就非常关键了,后续需要绑定UI控件的地方都需要这个对象。

添加自定义控件到组件树中

通过contentHandle 对象,我们拿到了当前UI的上下文,接着我们可以调用OH_ArkUI_NodeContent_AddNode 方法,把我们上文创建好的Text组件进行绑定到对应的UI树中

OH_ArkUI_NodeContent_AddNode(contentHandle, textHandle);

同样的,我们也封装好一些对外的方法,类似SetFontSize ,本质都是通过setAttribute 对textHandle进行对应的属性设置

// 创建单一文本
std::string content = "我是来自native创建的Text";
auto textNode = std::make_shared<ArkUITextNode>();
textNode->SetTextContent(content);
textNode->SetFontSize(16);
textNode->SetPercentWidth(1);
textNode->SetHeight(100);
textNode->SetBackgroundColor(0xFFfffacd);
textNode->SetTextAlign(ARKUI_TEXT_ALIGNMENT_CENTER);
OH_ArkUI_NodeContent_AddNode(contentHandle, textHandle);

最终,我们完成了一个Native Text上屏的操作:

当然,更多更加复杂的场景,比如动画、事件响应,父子布局等均可以通过类似的api实现,我们可以通过官网查看对用的API

总结

通过本文,我们初步了解到如何通过C-API的方式创建一个Native的Text并把这个Text与ArkTS侧进行了绑定,在这个过程中,我们涉及了大量的C++接口以及大量的枚举场景,因此日常开发中,如果不是特殊的需求,比如封装跨平台方案等,笔者不太建议使用者直接使用这些接口创建UI控件。一方面是C++与NAPI有一定的学习成本,同时维护成本相对于普通ArkTS编写UI来说也会上升,希望对大家有帮助!