HarmonyOS 鸿蒙Next RNOH如何写一个Fabric组件

发布于 1周前 作者 vueper 来自 鸿蒙OS

HarmonyOS 鸿蒙Next RNOH如何写一个Fabric组件

问题如题 我写了一个让内容变灰的组件,但是提供给rn后传递后子view展示不出来,this.descriptor.childrenTags是空的

export function providerArkTsComponentNames() {
  return [GRAY_VIEW_TYPE];
}

@Builder
export function buildCustomComponent(ctx: ComponentBuilderContext) {
  if (ctx.componentName === GRAY_VIEW_TYPE) {
    RNGrayView({ ctx: ctx.rnComponentContext, tag: ctx.tag, buildCustomComponent: buildCustomComponent })
  }
}

更多关于HarmonyOS 鸿蒙Next RNOH如何写一个Fabric组件的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

Fabric 自定义组件开发指导
ArkTS版本自定义组件

本节以示例工程中的 MarqueeView 为例,介绍了 Fabric ArkTS 自定义组件的实现步骤。

  1. 编写 RN 调用 Fabric 组件的代码

编写MarqueeViewNativeComponent.tsx,注意,如果要使用 Codegen ,文件必须以<ModuleName>NativeComponent命名。在文件中使用 codegenNativeComponent 创建 MarqueeView 组件,其中 MarqueeViewProps 里声明了 src 属性和 onStop 事件:

type OnStopEventData = Readonly<{
  isStop: boolean
}>;

interface MarqueeViewProps extends ViewProps {
  src: string,
  onStop?: DirectEventHandler<OnStopEventData>;
}

const MarqueeView = codegenNativeComponent<MarqueeViewProps>(
  'MarqueeView'
) as HostComponent<MarqueeViewProps>;

和其他标准组件的创建方式一样,在组件容器内添加 MarqueeView 标签:

<MarqueeView
  src="双十一大促,消费是社会再生产过程中的一个重要环节,也是最终环节。它是指利用社会产品来满足人们各种需要的过程。"
  style={{height: 180, width: '100%', backgroundColor: 'hsl(210, 80%, 50%)'}}
  onStop={(e) => {
    SampleTurboModule.rnLog("native调用了RN的 onStop,isStop = "+e.nativeEvent.isStop)
    setMarqueeStop(e.nativeEvent.isStop)
  }}
/>
  1. 编写 ArkTS 原生实现代码

Descriptor

Descriptor 的功能是封装 RN 侧组件代码传递到 ArkUI 组件的参数,MarqueeView 对 RN 侧公开了一个 src 参数,用于显示跑马灯的滚动内容。原生侧定义 MarqueeViewDescriptor 代码如下:

export interface MarqueeViewProps extends ViewBaseProps {
  src: string
}

export type MarqueeViewDescriptor = Descriptor<"MarqueeView", MarqueeViewProps>;

Descriptor 不需要我们手动创建,由 rnoh 自动生成;组件 tag 也不需要我们手动设置,rnoh 会为组件自动分配 tag。开发者只需要通过 getDescriptor 方法获取对应 tag 的 Descriptor:

this.descriptor = this.ctx.descriptorRegistry.getDescriptor<MarqueeViewDescriptor>(this.tag)

当 RN 侧传递过来的属性参数发生变化时,我们需要更新 Descripotor:

this.unregisterDescriptorChangesListener = this.ctx.descriptorRegistry.subscribeToDescriptorChanges(
  this.tag, 
  (newDescriptor) => {
    this.descriptor = (newDescriptor as MarqueeViewDescriptor)
})

RN 调用原生方法

RN 侧调用 UIManager.dispatchViewManagerCommand 向原生发送消息:

UIManager.dispatchViewManagerCommand(
  findNodeHandle(nativeRef.current),
  'toggleMarqueeState',
  [],
)

原生组件通过 commandDispatcher.registerCommandCallback 接收消息并执行对应方法:

this.ctx.commandDispatcher.registerCommandCallback(
  this.tag, 
  (commandName) => {
    if (commandName === "toggleMarqueeState") {
      this.start = !this.start
      console.log("will emitComponentEvent");
    }
  }
)

原生组件调用 RN 侧方法

RN 侧添加 onStop 方法实现:

<MarqueeView
  ...
  onStop={(e) => {
    // 原生组件调用了 RN 侧的 MarqueeView 的 onStop 方法
    const isStop = e.nativeEvent.isStop
    ...
  }}
/>

原生侧发送调用 RN 组件事件的消息:

this.ctx.rnInstance.emitComponentEvent(
  this.descriptor.tag,
  "MarqueeView",
  { type: "onStop", isStop: !this.start }
)

buildCustomComponent

创建 RNSurface 加载 JSBundle 时,传入 buildCustomComponent 用于加载原生 Fabric 组件:

import { RNAbility, ComponentBuilderContext, RNSurface } from "rnoh";
import { MarqueeView } from '../customView/MarqueeView'

[@Builder](/user/Builder)
public buildCustomComponent(ctx: ComponentBuilderContext) {
  if (ctx.descriptor.type === MarqueeView.NAME) {
    MarqueeView({
      ctx: ctx.rnohContext,
      tag: ctx.descriptor.tag
    })
  }
}

...

RNSurface({
  ...
  buildCustomComponent: this.buildCustomComponent,
})
  1. 编写 Codegen 的 C++ 代码

开发者可以使用 Codegen 生成C++侧的胶水代码,也可以手动实现这部分代码。在本节中会详细介绍如何手动实现这部分代码。

首先创建属性 Props 和 事件 Emitter 两部分的 C++ 类,在 Descriptor 中进行绑定。

实现 MarqueeViewEventEmitRequestHandler 的 handleEvent 方法,根据原生消息的事件名,调用 eventEmitter 向 RN 侧组件发送事件消息。

实现 MarqueeViewJSIBinder 类的属性和事件绑定方法。

实现 MarqueeViewNapiBinder 类的属性映射方法。

将以上文件引入到 SampleTurboModulePackage 的对应方法实现中进行绑定。

Props

创建 Props 的 C++ 文件用于定义 MarqueeView 的 Descriptor 对应的属性。

Props.h:

#include <jsi/jsi.h>
#include <react/renderer/components/view/ViewProps.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/debug/react_native_assert.h>

namespace facebook {
namespace react {

class JSI_EXPORT MarqueeViewProps final : public ViewProps {
 public:
  MarqueeViewProps() = default;
  MarqueeViewProps(const PropsParserContext &context, const MarqueeViewProps &sourceProps, const RawProps &rawProps);
#pragma mark - Props
  std::string src{""};
};

} // namespace react
} // namespace facebook

// Props.cpp

#include <react/renderer/components/rncore/Props.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/core/propsConversions.h>
#include "Props.h"

namespace facebook {
namespace react {

MarqueeViewProps::MarqueeViewProps(
    const PropsParserContext &context,
    const MarqueeViewProps &sourceProps,
    const RawProps &rawProps): ViewProps(context, sourceProps, rawProps),
    src(convertRawProp(context, rawProps, "src", sourceProps.src, {""}))
    {}

} // namespace react
} // namespace facebook

MarqueeViewEventEmitter

MarqueeViewEventEmitter.h 中添加 onStop 方法,并自定义了属性结构体:

#include <react/renderer/components/view/ViewEventEmitter.h>
#include <jsi/jsi.h>

namespace facebook {
namespace react {

class JSI_EXPORT MarqueeViewEventEmitter : public ViewEventEmitter {
 public:
  using ViewEventEmitter::ViewEventEmitter;
  struct OnStop {
    bool isStop;
  };
  void onStop(OnStop value) const;
};

} // namespace react
} // namespace facebook

MarqueeViewEventEmitter.cpp 中实现 onStop 事件的发送和参数绑定:

#include "MarqueeViewEventEmitter.h"

namespace facebook {
namespace react {

void MarqueeViewEventEmitter::onStop(OnStop event) const {
  dispatchEvent("stop", [event = std::move(event)](jsi::Runtime &runtime) {
    auto payload = jsi::Object(runtime);
    payload.setProperty(runtime, "isStop", event.isStop);
    return payload;
  });
}
} // namespace react
} // namespace facebook

MarqueeViewComponentDescriptor.h

将 MarqueeViewProps, MarqueeViewEventEmitter 绑定到 MarqueeViewComponentDescriptor 中:

#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/components/view/ViewShadowNode.h>
#include "MarqueeViewEventEmitter.h"
#include "Props.h"

namespace facebook {
namespace react {

extern const char MarqueeViewComponentName[] = "MarqueeView";

using MarqueeViewShadowNode = ConcreteViewShadowNode<MarqueeViewComponentName, MarqueeViewProps, MarqueeViewEventEmitter>;

using MarqueeViewComponentDescriptor = ConcreteComponentDescriptor<MarqueeViewShadowNode>;

} // namespace react
} // namespace facebook

MarqueeViewEventEmitRequestHandler

handleEvent 方法中根据事件名调用事件消息发送方法 eventEmitter->onStop(event):

class MarqueeViewEventEmitRequestHandler : public EventEmitRequestHandler {
 public:
  void handleEvent(EventEmitRequestHandler::Context const &ctx) override {
    if (ctx.eventName != "MarqueeView") {
      return;
    }
    ArkJS arkJs(ctx.env);
    auto eventEmitter = ctx.shadowViewRegistry->getEventEmitter<react::MarqueeViewEventEmitter>(ctx.tag);
    if (eventEmitter == nullptr) {
      return;
    }
    MarqueeViewEventType type = getMarqueeViewEventType(arkJs, ctx.payload);
    switch (type) {
    case MarqueeViewEventType::MARQUEE_VIEW_ON_STOP: {
      bool isStop = (bool)arkJs.getBoolean(arkJs.getObjectProperty(ctx.payload, "isStop"));
      react::MarqueeViewEventEmitter::OnStop event{isStop};
      eventEmitter->onStop(event);
      break;
    }
    default:
      break;
    }
  };
};

MarqueeViewJSIBinder

JSIBinder 是 RN 侧的属性和方法在 JSI 层的实现,主要调用了 object.setProperty(rt, “src”, “string”) 和 events.setProperty(rt, “topStop”, createDirectEvent(rt, “onStop”)) 这两个方法,events.setProperty 中注意 topStop 和 onStop 的命名规则:

#pragma once
#include "RNOHCorePackage/ComponentBinders/ViewComponentJSIBinder.h"

namespace rnoh {

class MarqueeViewJSIBinder : public ViewComponentJSIBinder {
 public:
  jsi::Object createNativeProps(facebook::jsi::Runtime &rt) override {
    auto object = ViewComponentJSIBinder::createNativeProps(rt);
    object.setProperty(rt, "src", "string");
    return object;
  }
  jsi::Object createDirectEventTypes(facebook::jsi::Runtime &rt) override {
    jsi::Object events(rt);
    events.setProperty(rt, "topStop", createDirectEvent(rt, "onStop"));
    return events;
  }
};

} // namespace rnoh

NapiBinder

实现 C++ 代码和原生组件代码之间的属性映射,其中 .addProperty(“src”, props->src) 为 MarqueeViewDescriptor 的 props 增加了 src 字段;如果未添加该代码,MarqueeView 就需要从 rawProps 中获取 src:

#include "RNOHCorePackage/ComponentBinders/ViewComponentNapiBinder.h"
#include "Props.h"

namespace rnoh {

class MarqueeViewNapiBinder : public ViewComponentNapiBinder {
 public:
  napi_value createProps(napi_env env, facebook::react::ShadowView const shadowView) override {
    napi_value napiViewProps = ViewComponentNapiBinder::createProps(env, shadowView);
    if (auto props = std::dynamic_pointer_cast<const facebook::react::MarqueeViewProps>(shadowView.props)) {
      return ArkJS(env)
        .getObjectBuilder(napiViewProps)
        .addProperty("src", props->src)
        .build();
    }
    return napiViewProps;
  }
};

} // namespace rnoh

SampleTurboModulePackage

在 SampleTurboModulePackage.h 中添加自定义组件相关的方法声明:

#include "RNOH/Package.h"

namespace rnoh {

class SampleTurboModulePackage : public Package {
 public:
  std::vector<facebook::react::ComponentDescriptorProvider> createComponentDescriptorProviders();
  ComponentNapiBinderByString createComponentNapiBinderByName();
  ComponentJSIBinderByString createComponentJSIBinderByName();
  EventEmitRequestHandlers createEventEmitRequestHandlers();
};

} // namespace rnoh

使用 MarqueeViewComponentDescriptor、MarqueeViewEventEmitRequestHandler、MarqueeViewNapiBinder、MarqueeViewJSIBinder 在 SampleTurboModulePackage.cpp 中完成对应方法实现:

std::vector<react::ComponentDescriptorProvider> SampleTurboModulePackage::createComponentDescriptorProviders() {
  return {
  react::concreteComponentDescriptorProvider<react::MarqueeViewComponentDescriptor>(),
  };
}

EventEmitRequestHandlers SampleTurboModulePackage::createEventEmitRequestHandlers() {
  return {std::make_shared<MarqueeViewEventEmitRequestHandler>()};
}

ComponentNapiBinderByString SampleTurboModulePackage::createComponentNapiBinderByName() {
  return {{"MarqueeView", std::make_shared<MarqueeViewNapiBinder>()}};
};

ComponentJSIBinderByString SampleTurboModulePackage::createComponentJSIBinderByName() {
  return {{"MarqueeView", std::make_shared<MarqueeViewJSIBinder>()}};
};
  1. 优化原生ArkTS组件

之前介绍的ArkTS组件实现中,是通过调用对应的属性设置接口完成属性的设置,这种实现方式存在两个缺点:

自定义组件属性过多,影响执行效率:若需要使用系统组件的全量属性方法,则需在封装的自定义组件中注册穷举每个属性值。这样会大大影响每个组件的Build效率。

不利于后期维护:当自定义组件中的系统组件属性发生变更时,自定义组件也需要同步适配。

为了解决上述缺点,ArkTS为每个系统组件提供了动态属性设置的方式,包括attributeModifier属性方法。该方法将组件属性设置分离到系统提供的AttributeModifier接口实现类实例中,通过自定义Class类实现AttributeModifier接口对系统组件属性进行扩展。

export class MarqueeModifier implements AttributeModifier<MarqueeAttribute> {
  private constructor() {}
  private static instance: MarqueeModifier;
  protected descriptor: ViewBaseDescriptor = {} as ViewBaseDescriptor;

  public static getInstance(): MarqueeModifier {
    if (!MarqueeModifier.instance) {
      MarqueeModifier.instance = new MarqueeModifier ();
    }
    return MarqueeModifier.instance;
  }

  setDescriptor(descriptor: ViewBaseDescriptor): MarqueeModifier {
    this.descriptor = descriptor;
    return MarqueeModifier.instance;
  }

  applyNormalAttribute(instance: MarqueeAttribute): void {
    instance.width(this.descriptor.layoutMetrics.frame.size.width);
    instance.height(this.descriptor.layoutMetrics.frame.size.height);
    instance.position({ y: this.descriptor.layoutMetrics.frame.origin.y, x: this.descriptor.layoutMetrics.frame.origin.x });
    if (this.descriptor.props.backgroundColor) {
      instance.backgroundColor(this.descriptor.props.backgroundColor);
    }
    /* ...... 其他需要设置的属性*/
  }
}

[@Builder](/user/Builder)
export function marqueeBuilder(ctx: RNOHContext, descriptor: ViewBaseDescriptor) {
  Marquee(···) {
    .attributeModifier(MarqueeModifier.getInstance().setDescriptor(descriptor))
  }
}
``

更多关于HarmonyOS 鸿蒙Next RNOH如何写一个Fabric组件的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS(鸿蒙)系统中,编写一个Fabric组件主要涉及到使用ArkUI框架进行UI组件的开发。Fabric组件通常指的是基于声明式UI的组件,这与ArkUI的设计理念相吻合。以下是关于如何编写一个Fabric组件的简要说明:

  1. 环境准备: 确保你的开发环境已经配置好HarmonyOS SDK,并且已经安装了必要的开发工具,如DevEco Studio。

  2. 创建项目: 在DevEco Studio中创建一个新的HarmonyOS项目,选择使用ArkUI(eTS或JS)进行开发。

  3. 定义组件: 在项目的src/main/ets/pages目录下创建一个新的.ets文件(如果是使用eTS语言),或者在src/main/resources/base/common下创建一个.json.hml文件(如果是使用JS语言)。在文件中定义你的Fabric组件,包括其属性和布局。

  4. 实现逻辑: 在.ets.js文件中编写组件的逻辑代码,包括事件处理、数据绑定等。

  5. 使用组件: 在其他页面或组件中引用你定义的Fabric组件,并传递必要的属性和事件。

  6. 编译与运行: 使用DevEco Studio编译并运行你的项目,查看Fabric组件的效果。

如果问题依旧没法解决请联系官网客服,官网地址是 https://www.itying.com/category-93-b0.html

回到顶部