本文为稀土掘金技术社区首发签约文章,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组件代码,便可以通过映射关系转换为对应平台的原生控件实现。
在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来说也会上升,希望对大家有帮助!