
Fabric 自定义组件开发指导
1.编写 RN 调用 Fabric 组件的代码
编写MarqueeViewNativeComponent.tsx,注意,如果要使用 Codegen ,文件必须以<ModuleName>NativeComponent命名。在文件中使用 codegenNativeComponent 创建 MarqueeView 组件,其中 MarqueeViewProps 里声明了 src 属性和 onStop 事件:
和其他标准组件的创建方式一样,在组件容器内添加 MarqueeView 标签:
2.编写ArkTS原生实现代码
Descriptor 的功能是封装 RN 侧组件代码传递到 ArkUI 组件的参数,MarqueeView 对 RN 侧公开了一个 src 参数,用于显示跑马灯的滚动内容。原生侧定义 MarqueeViewDescriptor 代码如下:
Descriptor 不需要我们手动创建,由 rnoh 自动生成;组件 tag 也不需要我们手动设置,rnoh 会为组件自动分配 tag。开发者只需要通过 getDescriptor 方法获取对应 tag 的 Descriptor:
当 RN 侧传递过来的属性参数发生变化时,我们需要更新 Descripotor:
RN 调用原生方法
RN 侧调用 UIManager.dispatchViewManagerCommand 向原生发送消息:
原生组件通过 commandDispatcher.registerCommandCallback 接收消息并执行对应方法:
原生组件调用 RN 侧方法
RN 侧添加 onStop 方法实现:
原生侧发送调用 RN 组件事件的消息:
buildCustomComponent
创建 RNSurface 加载 JSBundle 时,传入 buildCustomComponent 用于加载原生 Fabric 组件:
3. 编写 Codegen 的 C++ 代码
开发者可以使用Codegen生成C++侧的胶水代码,也可以手动实现这部分代码。在本节中会详细介绍如何手动实现这部分代码。
- 首先创建属性 Props 和 事件 Emitter 两部分的 C++ 类,在 Descriptor 中进行绑定。
- 实现 MarqueeViewEventEmitRequestHandler 的 handleEvent 方法,根据原生消息的事件名,调用 eventEmitter 向 RN 侧组件发送事件消息。
- 实现 MarqueeViewJSIBinder 类的属性和事件绑定方法。
- 实现 MarqueeViewNapiBinder 类的属性映射方法。
- 将以上文件引入到 SampleTurboModulePackage 的对应方法实现中进行绑定。
Props
创建 Props 的 C++ 文件用于定义 MarqueeView 的 Descriptor 对应的属性。Props.h:
MarqueeViewEventEmitter
MarqueeViewEventEmitter.h 中添加 onStop 方法,并自定义了属性结构体:
MarqueeViewEventEmitter.cpp 中实现 onStop 事件的发送和参数绑定:
MarqueeViewComponentDescriptor.h
将 MarqueeViewProps, MarqueeViewEventEmitter 绑定到MarqueeViewComponentDescriptor 中:
MarqueeViewEventEmitRequestHandler
handleEvent 方法中根据事件名调用事件消息发送方法 eventEmitter->onStop(event):
MarqueeViewJSIBinder
JSIBinder 是 RN 侧的属性和方法在 JSI 层的实现,主要调用了 object.setProperty(rt, "src", "string") 和 events.setProperty(rt, "topStop", createDirectEvent(rt, "onStop")) 这两个方法,events.setProperty 中注意 topStop 和 onStop 的命名规则:
NapiBinder
实现 C++ 代码和原生组件代码之间的属性映射,其中 .addProperty("src", props->src) 为 MarqueeViewDescriptor 的 props 增加了 src 字段;如果未添加该代码,MarqueeView 就需要从 rawProps 中获取 src:
SampleTurboModulePackage
在 SampleTurboModulePackage.h 中添加自定义组件相关的方法声明:
使用 MarqueeViewComponentDescriptor、MarqueeViewEventEmitRequestHandler、MarqueeViewNapiBinder、MarqueeViewJSIBinder 在 SampleTurboModulePackage.cpp 中完成对应方法实现:
4. 优化原生ArkTS组件
之前介绍的ArkTS组件实现中,是通过调用对应的属性设置接口完成属性的设置,这种实现方式存在两个缺点:
- 自定义组件属性过多,影响执行效率:若需要使用系统组件的全量属性方法,则需在封装的自定义组件中注册穷举每个属性值。这样会大大影响每个组件的Build效率
- 不利于后期维护:当自定义组件中的系统组件属性发生变更时,自定义组件也需要同步适配。
为了解决上述缺点,ArkTS为每个系统组件提供了动态属性设置的方式,包括attributeModifier属性方法。该方法将组件属性设置分离到系统提供的AttributeModifier接口实现类实例中,通过自定义Class类实现AttributeModifier接口对系统组件属性进行扩展。
如何创建C-API自定义组件
创建一个 Fabric 组件需要实现以下的代码:
- ComponentInstance:ComponentInstance文件是该自定义组件所有的逻辑集合,也是自定义组件主要需要实现的部分,简单理解就是,一个自定义组件文件对应一个ComponentInstance。
- Package文件:作用是声明创建该自定义组件的声明,指定创建Instance时会根据Packeage文件内的名称对应,从而生成对应的Instance对象。
- Props:Props是ComponentInstance的props参数声明,具体作用是JS的参数传递,在自定义组件内部由父组件往子组件发送数据的时候也需要用到props。
- EventEmitter:EventEmitter是ComponentInstance的事件声明,主要作用是获取前端设置的事件回调,在组件内部以合适的时机触发。
- ShadowNode:ShadowNode是ComponentInstance创建时所需要声明的其中一个类,将对应的Props,EventEmitter和ComponentName(就是创建ComponentInstance的名字)组合起来。
(1)编写 RN 调用 Fabric 组件的代码
本节以 ButtonView 为例,介绍了 Fabric C-API 自定义组件的实现步骤。
编写ButtonViewNativeComponent.tsx,注意,如果要使用 Codegen ,文件必须以<ModuleName>NativeComponent命名。在文件中使用 codegenNativeComponent 创建 ButtonView 组件,其中 ButtonViewProps 里声明了 buttonText 属性和 onButtonClick 事件:
和其他标准组件的创建方式一样,在组件容器内添加 ButtonView 标签:
(2)编写C-API 原生实现代码
C-API组件结构
原生端自定义组件主要包含两个部分,ButtonViewComponentInstance.cpp 以及 ButtonViewNode.cpp,其中ButtonViewComponentInstance为JS侧ButtonView组件对应的原生端实例,它继承RN框架中的组件实例模板类CppComponentInstance并转换为自己组件实现的ShadowNode类型,它应该重写基类上的一些通用方法,包括onChildInserted插入子节点、getLocalRootArkUINode获取根节点以及onPropsChanged设置组件属性等,也可以实现组件的特有方法。同时它继承ButtonViewNode中声明的代理类ButtonViewNodeDelegate,并重写代理类上的onXXX事件用于上报事件。CppComponentInstance是所有自定义组件ComponentInstance的父类,所有组件都继承于这个类,这个类包含了对组件进行操作的一些基础方法。
ButtonViewNode对应具体的鸿蒙原生组件节点,它作为成员被组件实例持有,当设置属性或处理指令的时候,ButtonViewComponentInstance会调用ButtonViewNode上实现的具体方法,以ButtonView在RN侧暴露的buttonText属性为例,在JS侧设置或更新属性的时候,框架会调用ButtonViewComponentInstance上的onPropsChanged方法,这个方法中会去调用ButtonViewNode上的setLabel方法去设置资源。
属性
在 RN 侧设置的属性,会通过props传递到ComponentInstance中。通过ComponentInstance中的onPropsChanged可以获取到变化后的 Props。在onPropsChanged中需要做一次diff判断,然后就可以调用ComponentInstance保存的ComponentNode对象所实现的各种属性设置方法:
在ComponentNode中需要定义并实现对应属性的设置:
指令
RN 侧需要调用 UIManager.dispatchViewManagerCommand 向原生发送消息:
RN框架内部已经封装好了指令通道,原生端只需在instance层重写实现handleCommand方法,根据接收指令名调用node层实现的对应方法即可,传入的参数以object对象的形式记录在args中:
事件
RN侧添加 onButtonClick 事件回调监听实现:
原生端在instance层重写事件代理类中的onButtonClick方法,并在该方法中调用ButtonViewEventEmitter.h的对应方法,将事件传到RN侧:
事件的接收入口在对应的node类中,node类在构造的时候需要注册组件需要监听的事件,并重写onNodeEvent方法,在该方法中调用instance层实现的具体事件方法:
(3)编写 Codegen 的 C++ 代码
由于当前C-API版本的Codegen尚未实现,所以C-API版本的组件需要开发者手动添加在其他平台上由 Codegen 生成的 C++ 代码。
- 首先创建属性 Props 和 事件 Emitter 两部分的 C++ 类,在 Descriptor 中进行绑定并注册Node类型。
- 实现 ButtonViewJSIBinder 类的属性和事件绑定方法。
- 将以上文件引入到 SampleTurboModulePackage 的对应方法实现中进行绑定。
Props
创建 Props 的 C++ 文件用于定义 ButtonView 的属性。Props.h:
ButtonViewEventEmitter
ButtonViewEventEmitter.h 中添加 onButtonClick 方法,并自定义了属性结构体:
ButtonViewEventEmitter.cpp 中实现 onButtonClick 事件的发送和参数绑定:
ButtonViewComponentDescriptor.h
将 ButtonViewProps, ButtonViewEventEmitter 绑定到 ButtonViewShadowNode中:
ButtonViewJSIBinder
JSIBinder 是 RN 侧的属性和方法在 JSI 层的实现,主要调用了 object.setProperty(rt, "buttonText", "string") 和 events.setProperty(rt, "topButtonClick", createDirectEvent(rt, "onButtonClick")); 这两个方法,events.setProperty 中注意 topButtonClick 和 onButtonClick 的命名规则:
SampleTurboModulePackage
在SampleTurboModulePackage.h中添加自定义组件相关的方法声明:
创建ButtonViewPackageComponentInstanceFactoryDelegate对象,使用ButtonViewPackageComponentInstanceFactoryDelegate、ButtonViewJSIBinder、ButtonViewComponentInstance 在 SampleTurboModulePackage.cpp 中完成对应方法实现:
(4)在自定义组件中使用其他自定义组件
在自定义的组件中,可能并不是由一个基础组件实现的,而是由多个基础组件相互组合实现,例如自定义的Scroll,内部可以持有一个Stack组件。每一个组件的ComponentInstance中,都持有各个组件的node对象,node中提供了修改属性发送事件等各种操作。所以开发者可以在同一个ComponentInstance中记录多个node对象,通过重写getLocalRootArkUINode()方法,用于定义对外暴露的组件节点;重写onChildInserted()方法,用于将子节点插入组件的容器类中;并在ComponentInstance中增加多个node之间相互处理的逻辑,即可完成开发。
