react-native-elements 三方库鸿蒙版本适配与使用(MatePad Edge 双模式真机验证)

CPF-RN 社区地址:CPF-RN - 开源代码托管,代码协作 - AtomGit

上游三方库地址:https://github.com/react-native-elements/react-native-elements

npm 地址:https://www.npmjs.com/package/react-native-elements

适配后地址:https://gitcode.com/aasd23/rntpc_react-native-elements

react-native-elements 库概述

react-native-elements 是 React Native 生态中使用广泛的跨平台 UI 组件库,GitHub Star 超过 2.5 万,官方版本支持 Android 和 iOS。通过本次适配,该库已可在 OpenHarmony / HarmonyOS NEXT 平台上运行。

主要功能:提供 30 余个开箱即用的 UI 组件,覆盖按钮、卡片、输入框、头像、徽标、搜索栏、列表、复选框、开关、滑块、工具提示、底部弹窗等常见移动端 UI 场景。

跨平台 UI 组件:Button、ButtonGroup、Chip、FAB、SpeedDial 等按钮类组件,基于 TouchableOpacity 封装,支持主题定制和样式覆盖。

卡片与布局:Card、CardTitle、CardDivider、CardImage、Header、Divider、Tile、PricingCard 等容器类组件,支持圆角、阴影、图片背景等视觉效果。

表单与输入:Input、SearchBar、CheckBox、Switch、Slider 等交互组件,支持受控/非受控模式、验证状态、左侧图标等。

列表与导航:ListItem 及其子组件(ListItemContent、ListItemTitle、ListItemSubtitle、ListItemChevron、ListItemCheckBox、ListItemInput、ListItemButtonGroup、ListItemAccordion、ListItemSwipeable),支持多种列表项组合。

反馈与弹窗:Badge、withBadge、Tooltip、Overlay、Dialog、BottomSheet、AirbnbRating 等反馈类组件。

图标支持:Icon 组件封装 react-native-vector-icons,支持 Material、Ionicons、FontAwesome 等多种图标字体。

需要将上游仓库 clone 到国内 AtomGit,操作效率更高。

适配基础环境

React Native 版本:0.82.1

RNOH(React Native for OpenHarmony)版本:0.82.30

React 版本:19.1.1

HarmonyOS SDK:6.1.1(24),runtimeOS: HarmonyOS

DevEco Studio:6.0.0.858+

Node.js:20+(Metro 打包建议使用 Node 24)

上游库版本:react-native-elements 3.4.3

演示真机与系统版本

本次适配全部基于华为 MatePad Edge 二合一真机验证,未使用模拟器。该设备支持平板模式和电脑模式(PC 模式)两种使用形态,本次在两种模式下均完成了安装、运行和组件验证。

演示的鸿蒙系统版本:

适配过程

一、前言

react-native-elements 是纯 JavaScript/TypeScript 实现的 UI 组件库,上游版本(3.4.3)仅支持 Android 和 iOS。鸿蒙化目标:

  1. 组件 API 完全不变------业务方迁移时零改动,仅更换依赖来源,所有组件的 props、事件、用法与上游保持一致;
  2. 纯 JS 层适配------不新增原生模块(C++/ArkTS),所有平台差异收敛在 JS 层的平台判断、样式兼容和依赖处理中;
  3. React 19 兼容------RNOH 0.82 配套 React 19.1.1,上游库开发时基于 React 16,需修复 Context Consumer、key 警告等兼容性问题;
  4. 双模式真机验证------在 MatePad Edge 的平板模式(触摸全屏)和电脑模式(鼠标键盘窗口化)下均完成全部核心组件的渲染和交互验证;
  5. 平台语义差异显式处理------触摸反馈(Ripple)、图标字体、安全区、状态栏偏移等平台差异做明确的映射和文档说明。

二、基础环境

|---------------------------------------------|--------------------------------------|
| | 版本 |
| React Native | 0.82.1 |
| RNOH(@react-native-oh/react-native-harmony) | 0.82.30 |
| React | 19.1.1 |
| DevEco Studio | 6.0.0.858+ |
| OpenHarmony SDK | 6.1.1(24),runtimeOS: HarmonyOS |
| 原生编译器 | BiSheng |
| Node.js | 20+(Metro 建议 Node 24) |
| 真机 | 华为 MatePad Edge 二合一(平板模式 + PC 模式) |
| 上游库版本 | react-native-elements 3.4.3 |
| 图标依赖 | react-native-vector-icons 10.2.0 |
| 安全区依赖 | react-native-safe-area-context 5.5.2 |

三、库分析

3.1 库结构

react-native-elements v3.4.3 的源码全部位于 src/,是纯 TypeScript 实现,跨平台通用,适配过程中不修改组件的对外 API,仅调整平台判断和兼容性问题:

复制代码
src/
├── index.ts                    # 入口,导出全部组件
├── avatar/                     # Avatar、Accessory
├── badge/                      # Badge、withBadge
├── bottomSheet/                # BottomSheet
├── buttons/                    # Button、ButtonGroup、Chip、FAB、SpeedDial
├── card/                       # Card、CardTitle、CardDivider、CardImage 等
├── checkbox/                   # CheckBox、CheckBoxIcon
├── config/                     # ThemeProvider、withTheme、theme、colors、makeStyles
├── dialog/                     # Dialog、DialogTitle、DialogActions、DialogButton、DialogLoading
├── divider/                    # Divider
├── header/                     # Header
├── helpers/                    # renderNode、getIconType、normalizeText、平台判断
├── icons/                      # Icon(封装 react-native-vector-icons)
├── image/                      # Image
├── input/                      # Input
├── linearProgress/             # LinearProgress
├── list/                       # ListItem 及全部子组件
├── overlay/                    # Overlay
├── pricing/                    # PricingCard
├── searchbar/                  # SearchBar
├── slider/                     # Slider
├── social/                     # SocialIcon、SocialIconButton
├── switch/                     # Switch
├── tab/                        # Tab、TabView
├── text/                       # Text
├── tile/                       # Tile
└── tooltip/                    # Tooltip

全部组件通过 withTheme 高阶组件包裹,从 ThemeContext 获取主题配置。组件本身基于 React Native 核心组件(View、Text、TextInput、TouchableOpacity、Switch、Slider、Modal、Animated 等)封装,不包含任何 iOS 或 Android 原生模块代码。

3.2 核心依赖分析

react-native-elements 有两个 peerDependencies,直接影响鸿蒙适配:

|--------------------------------|-----------|-----------------------------------------------------------------|-------------------------------------------------------------|
| 依赖 | 版本要求 | 用途 | 鸿蒙适配状态 |
| react-native-vector-icons | > 7.0.0 | Icon 组件、CheckBox 勾选图标、ListItemChevron 箭头、Button/SearchBar 左侧图标等 | 已有鸿蒙适配版,需在 Index.ets 中通过 fontResourceByFontFamily 注册 TTF 字体 |
| react-native-safe-area-context | >= 3.0.0 | Header、BottomSheet 组件的安全区处理 | RNOH 0.82 下原生视图 RNCSafeAreaView 缺失,需改用 RN 核心 SafeAreaView |

运行时依赖(color、deepmerge、hoist-non-react-statics、lodash.isequal、react-native-ratings、react-native-size-matters)均为纯 JS 库,在 RNOH 环境下可直接使用,无需额外适配。

3.3 平台判断

上游代码中通过 Platform.OS === 'ios' 区分 iOS 和 Android 的样式与行为。在 RNOH 环境下,Platform.OS 的值为 'harmony''ohos',既不是 'ios' 也不是 'android',所有 else 分支的行为不可控。

全局排查后,需要修改的平台判断点集中在以下组件:

|-----------------|-----------------------|----------------------------------|-----------------------------------------------------------------|
| 组件 | 平台判断位置 | iOS 行为 | Android/鸿蒙行为 |
| ListItemChevron | 图标 type 和 name | Ionicons chevron-forward-outline | Material keyboard-arrow-right |
| DialogTitle | fontWeight | 500 | 700 |
| Switch | trackColor、thumbColor | iOS 风格开关 | Android 风格开关 |
| Tooltip | 状态栏偏移 key | ios 偏移量 | android/鸿蒙偏移量 |
| Button | Touchable 组件选择 | TouchableOpacity | Android 用 TouchableNativeFeedback(Ripple),鸿蒙回退 TouchableOpacity |

3.4 React 19 兼容性

RNOH 0.82 配套 React 19.1.1,而 react-native-elements 3.4.3 上游开发时使用 React 16。其中 withTheme 高阶组件使用了 ThemeConsumer 的 render-props 模式(children-as-function),在 React 19 的新 Context Consumer 路径下会出现渲染异常。这是适配过程中最隐蔽的一个兼容性问题,表现为所有组件不显示或主题不生效。

此外,ListItemPadView 中通过 React.Children.map 渲染子元素时未显式指定 key,React 19 下会产生 key 警告并可能影响性能。

四、适配方案设计

4.1 总体思路

复制代码
┌─────────────────────────────────────────────────────────────┐
│ 业务层(不变)                                                │
│ import { Button, Card, Input } from 'react-native-elements' │
├─────────────────────────────────────────────────────────────┤
│ react-native-elements 适配层(JS 层修改)                    │
│ ├── helpers/index.tsx     → 新增 isHarmony / isAndroidLike  │
│ ├── config/withTheme.tsx  → React 19 兼容(useContext 重写)│
│ ├── switch/Switch.tsx     → 鸿蒙走 Android 样式路径           │
│ ├── list/ListItemChevron  → 鸿蒙用 Material 图标              │
│ ├── dialog/DialogTitle    → 鸿蒙 fontWeight 700              │
│ ├── header/Header.tsx     → 改用 RN 核心 SafeAreaView        │
│ ├── bottomSheet/          → 改用 RN 核心 SafeAreaView        │
│ ├── tooltip/Tooltip.tsx   → 新增 harmony/ohos 状态栏偏移     │
│ └── list/ListItem.tsx     → 子元素加 key(React 19 警告)    │
├─────────────────────────────────────────────────────────────┤
│ RNOH 运行时(0.82.30)                                       │
│ React Native 核心组件 + ArkTS 桥接 + HarmonyOS NEXT          │
└─────────────────────────────────────────────────────────────┘
  1. 纯 JS 层适配,不新增原生模块------react-native-elements 本身不包含原生代码,所有平台差异均可在 JS 层通过 Platform 判断和样式覆盖解决,不需要编写 C++ 或 ArkTS 原生模块;
  2. 鸿蒙复用 Android 样式路径------鸿蒙和 Android 同为移动端,视觉风格接近,大部分组件的样式(阴影、字重、开关样式、图标类型)直接复用 Android 分支,通过 isAndroidLike 常量统一判断;
  3. TouchableNativeFeedback 保持 Android 独占------水波纹效果是 Android 独有,鸿蒙端通过 Platform.select 的 default 分支自动回退到 TouchableOpacity,不强行适配 Ripple;
  4. 图标字体在鸿蒙工程侧注册------vector-icons 的 TTF 字体不在 JS 库内处理,而是在 Demo 工程的 Index.ets 中通过 fontResourceByFontFamily 注册,与 RNOH 的字体加载机制对齐;
  5. withTheme 完全重写而非补丁------React 19 下 ThemeConsumer render-props 模式不兼容,直接重写为 useContext + forwardRef,一次解决所有组件的主题获取问题,而不是逐个组件打补丁。

4.2 核心适配设计

|-----------------------|-----------------------------------------------------------------------------------------|--------------------------------------------------------------|-----------------------------------------------------|
| 适配点 | 问题 | 方案 | 影响范围 |
| 平台判断 Helper | Platform.OS 为 harmony/ohos,所有 ios/android 判断的 else 分支不可控 | 新增 isHarmony、isAndroidLike 常量,组件中统一使用,鸿蒙走 Android 样式路径 | Switch、ListItemChevron、DialogTitle、Tooltip 等 |
| withTheme React 19 兼容 | ThemeConsumer render-props 在 React 19 下渲染异常,所有组件不显示 | 重写为 useContext(ThemeContext) + forwardRef,保持类组件和函数组件兼容 | 全部 30+ 组件(都通过 withTheme 包裹) |
| SafeAreaView 原生视图缺失 | react-native-safe-area-context 的 RNCSafeAreaView 在 RNOH 0.82 下不存在,Header/BottomSheet 崩溃 | 改用 React Native 核心自带的 SafeAreaView,不依赖额外原生模块 | Header、BottomSheet |
| 图标字体加载 | vector-icons 的 TTF 字体在鸿蒙端不会自动链接,Icon/CheckBox/Chevron 不显示 | 在 Index.ets 的 fontResourceByFontFamily 中注册所需 TTF,只注册实际使用的字体集 | Icon、CheckBox、ListItemChevron、Button/SearchBar 左侧图标 |
| React 19 key 警告 | ListItem/PadView 的 Children.map 未指定 key,React 19 警告并可能影响性能 | 映射后的子元素包裹在带 key 的 Fragment 中 | ListItem、PadView |
| Tooltip 状态栏偏移 | Tooltip 计算弹出位置时只处理了 ios/android 的状态栏偏移 key | 新增 harmony/ohos 的状态栏偏移 key | Tooltip |
| Button 触摸反馈 | Android 用 TouchableNativeFeedback(Ripple),鸿蒙无此原生组件 | 通过 Platform.select default 分支自动回退 TouchableOpacity,文档中明确行为差异 | Button、ButtonGroup、Chip、FAB、SpeedDial |

五、适配流程

5.1 代码拉取与创建分支

从上游 GitHub 克隆代码,切换到稳定版标签,创建鸿蒙适配分支:

复制代码
git clone https://github.com/react-native-elements/react-native-elements.git
cd react-native-elements
git checkout v3.4.3
git checkout -b feat/ohos_react-native-elements_3.4.3

分支命名遵循社区规范:feat/ohos_库名称_版本号

适配完成后,在仓库根目录补充以下文件:

  • README.OpenSource.md------开源声明,包含上游仓库信息、适配版本、适配摘要、组件支持状态表、依赖说明;
  • README_zh.md------中文使用说明,涵盖鸿蒙端安装、字体配置、组件使用方法;
  • CHANGELOG_ohos.md------鸿蒙适配变更日志,记录所有新增、修改和修复项。

5.2 Demo 工程创建

创建独立的 RNOH Demo 工程用于验证适配后的组件库。使用 React Native CLI 初始化工程,再按照 RNOH 接入文档添加 harmony 目录:

复制代码
npx react-native@0.82 init RNElementsDemo
cd RNElementsDemo

工程结构:

复制代码
RNElementsDemo/
├── App.tsx                     # 入口,ThemeProvider + 页面布局
├── src/components/
│   ├── StaticShowcase.tsx      # 静态组件展示(Button/Card/Avatar/ListItem)
│   ├── InteractivePanel.tsx    # 交互组件(Input/SearchBar/CheckBox/Switch/Slider)
│   └── FadeOverlay.tsx         # Modal + 动画遮罩演示
├── harmony/                    # 鸿蒙工程(DevEco Studio 打开此目录)
│   ├── entry/src/main/ets/pages/Index.ets  # 入口,注册字体 + RNApp
│   ├── entry/build-profile.json5             # ABI 配置(arm64-v8a + x86_64)
│   └── build-profile.json5                    # 构建配置
└── package.json                # RN 依赖,通过 file:../rntpc_react-native-elements 引入本地库

5.3 本地库引入与依赖配置

在 Demo 工程的 package.json 中通过本地文件路径引入适配后的库:

复制代码
"dependencies": {
  "react": "19.1.1",
  "react-native": "^0.82.1",
  "react-native-elements": "file:../rntpc_react-native-elements",
  "react-native-safe-area-context": "^5.5.2",
  "react-native-vector-icons": "^10.2.0"
},
"devDependencies": {
  "@react-native-oh/react-native-harmony": "^0.82.30",
  "@react-native-oh/react-native-harmony-cli": "^0.82.30"
}

执行 npm install 安装依赖。适配后的库通过 file 路径引入,修改源码后无需重新发布即可在 Demo 中验证。

六、核心适配实现

6.1 平台判断 Helper

src/helpers/index.tsx 中新增两个统一的平台判断常量,避免在每个组件中重复写 Platform.OS 判断:

复制代码
import { Platform, Dimensions } from 'react-native';

/** iOS only */
const isIOS = Platform.OS === 'ios';

/**
 * OpenHarmony / HarmonyOS NEXT (RNOH).
 * RNOH historically exposes Platform.OS as 'harmony' or 'ohos'.
 */
const isHarmony =
  (Platform.OS as string) === 'harmony' || (Platform.OS as string) === 'ohos';

/**
 * Android-like mobile styling path (Android + HarmonyOS).
 * Do NOT use for TouchableNativeFeedback / Ripple --- those stay Android-only.
 */
const isAndroidLike = Platform.OS === 'android' || isHarmony;

export { isIOS, isHarmony, isAndroidLike };

设计要点:isAndroidLike 用于样式路径(鸿蒙复用 Android 的视觉风格),但不能用于 TouchableNativeFeedback / Ripple------这些是 Android 独有的原生触摸反馈,鸿蒙端应回退到 TouchableOpacity。

6.2 withTheme React 19 重写

这是本次适配中最核心的修改。上游的 withTheme 使用 ThemeConsumer render-props 模式,在 React 19 下会导致主题上下文无法正确传递。重写为 useContext Hook + forwardRef:

复制代码
import React, { useContext } from 'react';
import deepmerge from 'deepmerge';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { ThemeContext, ThemeProps } from './ThemeProvider';
import DefaultTheme, { FullTheme } from './theme';

const isClassComponent = (Component: any) =>
  Boolean(Component.prototype && Component.prototype.isReactComponent);

const noop = () => {};

/**
 * OpenHarmony / React 19 compatible withTheme.
 * - Uses useContext instead of ThemeConsumer render-props
 * - Always wraps with forwardRef so hooks run in a real component
 */
function withTheme<P = {}, T = {}>(
  WrappedComponent: React.ComponentType<P & Partial<ThemeProps<T>>>,
  themeKey: string
):
  | React.FunctionComponent<Omit<P, keyof ThemeProps<T>>>
  | React.ForwardRefExoticComponent<P> {
  const name = themeKey
    ? `Themed.${themeKey}`
    : `Themed.${
        WrappedComponent.displayName || WrappedComponent.name || 'Component'
      }`;

  const Component = WrappedComponent as React.ComponentType<any>;

  const Themed = React.forwardRef<any, any>((props, forwardedRef) => {
    const { children, ...rest } = props;
    const context = useContext(ThemeContext);

    const theme = context?.theme ?? DefaultTheme;
    const updateTheme = context?.updateTheme ?? noop;
    const replaceTheme = context?.replaceTheme ?? noop;

    const newProps = {
      theme,
      updateTheme,
      replaceTheme,
      ...deepmerge<FullTheme>(
        (themeKey &&
          (theme[themeKey as keyof Partial<FullTheme>] as Partial<
            FullTheme
          >)) ||
          {},
        rest,
        {
          clone: false,
        }
      ),
      children,
    };

    if (isClassComponent(WrappedComponent)) {
      return <Component ref={forwardedRef} {...newProps} />;
    }
    return <Component {...newProps} />;
  });

  Themed.displayName = name;

  if (isClassComponent(WrappedComponent)) {
    return hoistNonReactStatics(Themed, WrappedComponent);
  }
  return Themed as any;
}

export default withTheme;

修改后,所有通过 withTheme 包裹的组件(Button、Card、Input 等 30+ 个)都能在 React 19 下正确获取主题,且保持了对类组件和函数组件的兼容。forwardRef 确保 ref 能正确传递到被包裹的组件。

6.3 组件样式适配

Switch 组件 :上游通过 isIOS 区分 iOS 和 Android 的轨道/滑块颜色逻辑。鸿蒙端复用 Android 的样式路径。修改 src/switch/Switch.tsx,将 Platform.OS 判断统一改为使用 isIOS 常量,鸿蒙自动走 Android 分支:

复制代码
import { isIOS } from '../helpers';

// HarmonyOS follows Android Switch styling (not iOS).
const onTintColor = isIOS || !disabled ? switchedOnColor : theme?.colors?.disabled;

const thumbTintColor = isIOS
  ? undefined
  : disabled || !value ? theme?.colors?.disabled : switchedOnColor;

ListItemChevron 组件:列表项的右箭头,iOS 使用 Ionicons,Android/鸿蒙使用 Material 图标:

复制代码
import React from 'react';
import { StyleSheet } from 'react-native';
import { withTheme } from '../config';
import { RneFunctionComponent, isIOS } from '../helpers';
import Icon, { IconProps } from '../icons/Icon';

const ListItemChevron: RneFunctionComponent<Partial<IconProps>> = ({
  containerStyle,
  ...props
}: Partial<IconProps>) => {
  return (
    <Icon
      type={isIOS ? 'ionicon' : 'material'}
      color="#D1D1D6"
      name={isIOS ? 'chevron-forward-outline' : 'keyboard-arrow-right'}
      size={16}
      containerStyle={StyleSheet.flatten([
        { alignSelf: 'center' },
        containerStyle,
      ])}
      {...props}
    />
  );
};

export default withTheme(ListItemChevron, 'ListItemChevron');

DialogTitle 组件:对话框标题的字重,iOS 为 500,Android/鸿蒙为 700。同样通过 isIOS 判断,鸿蒙自动走 700。

6.4 SafeAreaView 替换

上游的 Header 和 BottomSheet 组件使用了 react-native-safe-area-context 提供的 SafeAreaView,依赖原生视图 RNCSafeAreaView。在 RNOH 0.82 环境下,该原生视图不存在,会导致页面崩溃或布局异常。

适配方案:在 src/header/Header.tsxsrc/bottomSheet/BottomSheet.tsx 中,将 SafeAreaView 的导入从 react-native-safe-area-context 改为从 react-native 核心导入:

复制代码
// 修改前
import { SafeAreaView } from 'react-native-safe-area-context';

// 修改后
import { SafeAreaView } from 'react-native';

React Native 核心的 SafeAreaView 不依赖额外原生模块,在 RNOH 环境下可直接使用。功能上相比 safe-area-context 版本较少(不支持 edges 自定义等),但满足 Header 和 BottomSheet 的基本安全区需求。

6.5 其他细节修复

React 19 key 警告ListItemPadView 中通过 React.Children.map 渲染子元素时未显式指定 key。修改为将映射后的子元素包裹在带 key 的 Fragment 中:

复制代码
// 修改前
{React.Children.map(children, (child) => child)}

// 修改后
{React.Children.map(children, (child, index) => (
  <React.Fragment key={index}>{child}</React.Fragment>
))}

Tooltip 状态栏偏移:Tooltip 组件计算弹出位置时需要考虑状态栏高度,上游仅处理了 iOS 和 Android 的状态栏偏移 key。新增 harmony 和 ohos 的状态栏偏移 key,确保 Tooltip 在鸿蒙端弹出位置正确。

Button 触摸反馈:上游 Button 在 Android 上使用 TouchableNativeFeedback 实现水波纹效果。该组件是 Android 独有的,鸿蒙端通过 Platform.select 的 default 分支自动回退到 TouchableOpacity,无需额外修改。文档中明确说明鸿蒙端无水波纹效果,使用 TouchableOpacity 透明度反馈。

6.6 字体注册配置

react-native-elements 的 Icon 组件、CheckBox、ListItemChevron 等都依赖 react-native-vector-icons 渲染图标。在鸿蒙端,必须在 harmony/entry/src/main/ets/pages/Index.ets 的 RNApp 配置中通过 fontResourceByFontFamily 注册所需的 TTF 字体文件:

复制代码
import {
  AnyJSBundleProvider, MetroJSBundleProvider, RNApp,
  ResourceJSBundleProvider, RNOHErrorDialog, RNOHCoreContext
} from '@rnoh/react-native-openharmony';
import { getRNOHPackages } from '../PackageProvider';

@Entry
@Component
struct Index {
  @StorageLink('RNOHCoreContext') private rnohCoreContext: RNOHCoreContext | undefined = undefined;

  build() {
    Column() {
      if (this.rnohCoreContext) {
        if (this.rnohCoreContext?.isDebugModeEnabled) {
          RNOHErrorDialog({ ctx: this.rnohCoreContext })
        }
        RNApp({
          rnInstanceConfig: {
            name: "RNElementsDemo",
            createRNPackages: getRNOHPackages,
            fontResourceByFontFamily: {
              // 只注册 Demo 实际使用的字体集,控制 HAP 体积
              'MaterialIcons': $rawfile('assets/fonts/MaterialIcons.ttf'),
              'MaterialCommunityIcons': $rawfile('assets/fonts/MaterialCommunityIcons.ttf'),
            },
            enableDebugger: this.rnohCoreContext?.isDebugModeEnabled,
          },
          appKey: "RNElementsDemo",
          jsBundleProvider: this.rnohCoreContext?.isDebugModeEnabled ?
            new AnyJSBundleProvider([
              new ResourceJSBundleProvider(
                this.rnohCoreContext.uiAbilityContext.resourceManager, 'bundle.harmony.js'),
              new MetroJSBundleProvider(),
            ]) :
            new ResourceJSBundleProvider(
              this.rnohCoreContext.uiAbilityContext.resourceManager, 'bundle.harmony.js'),
        })
      }
    }
    .height('100%')
    .width('100%')
  }
}

注意事项:

  • 只注册实际使用到的字体集,避免 HAP 包体积过大。本 Demo 使用了 MaterialIcons 和 MaterialCommunityIcons 两套字体;
  • TTF 文件需放入 entry/src/main/resources/rawfile/assets/fonts/ 目录;
  • jsBundleProvider 优先使用 ResourceJSBundleProvider(本地离线 bundle),Metro 仅作为 debug 模式下的备选热更新通道,避免首启白屏。

七、Demo 示例工程

7.1 工程结构与入口

Demo 工程的入口 App.tsx 使用 ThemeProvider 包裹整个应用,将页面分为静态展示区和交互区两个独立组件,配合 FadeOverlay 动画遮罩:

复制代码
import React, {useCallback, useState} from 'react';
import {
  SafeAreaView,
  ScrollView,
  StyleSheet,
  Text,
  View,
  StatusBar,
  useWindowDimensions,
} from 'react-native';
import {ThemeProvider} from 'react-native-elements';
import {StaticShowcase} from './src/components/StaticShowcase';
import {InteractivePanel} from './src/components/InteractivePanel';
import {FadeOverlay} from './src/components/FadeOverlay';

function App(): React.JSX.Element {
  const [overlayVisible, setOverlayVisible] = useState(false);
  const {width} = useWindowDimensions();
  // MateBook / 2in1: keep content readable instead of full-bleed stretch
  const contentMaxWidth = Math.min(width - 32, 560);

  const openOverlay = useCallback(() => setOverlayVisible(true), []);
  const closeOverlay = useCallback(() => setOverlayVisible(false), []);

  return (
    <ThemeProvider>
      <View style={styles.root}>
        <StatusBar barStyle="light-content" backgroundColor="#2089dc" />
        <View style={styles.demoHeader}>
          <Text style={styles.headerTitle}>React Native Elements 鸿蒙 Demo</Text>
        </View>
        <SafeAreaView style={styles.flex}>
          <ScrollView
            contentContainerStyle={styles.scroll}
            removeClippedSubviews
            keyboardShouldPersistTaps="handled">
            <View style={[styles.content, {maxWidth: contentMaxWidth}]}>
              <StaticShowcase onOpenOverlay={openOverlay} />
              <InteractivePanel />
            </View>
          </ScrollView>
        </SafeAreaView>

        <FadeOverlay visible={overlayVisible} onClose={closeOverlay} />
      </View>
    </ThemeProvider>
  );
}

const styles = StyleSheet.create({
  root: {flex: 1, backgroundColor: '#f5f5f5'},
  flex: {flex: 1},
  scroll: {
    paddingVertical: 16,
    paddingBottom: 40,
    alignItems: 'center',
  },
  content: {
    width: '100%',
    paddingHorizontal: 16,
  },
  demoHeader: {
    backgroundColor: '#2089dc',
    paddingVertical: 14,
    paddingHorizontal: 16,
    alignItems: 'center',
  },
  headerTitle: {color: '#fff', fontSize: 16, fontWeight: '600'},
});

export default App;

7.2 静态展示区(StaticShowcase)

展示 Button(主按钮/描边按钮)、Card(卡片 + Avatar + Badge 组合)、ListItem(带 Chevron 箭头)、Icon(带图标的按钮)。使用 React.memo 包裹,避免交互区状态变化导致静态区重渲染:

复制代码
import React, { memo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button, Card, Avatar, Badge, ListItem, Divider, Icon } from 'react-native-elements';

function StaticShowcaseInner({ onOpenOverlay }) {
  return (
    <View>
      <Text style={styles.section}>Button</Text>
      <Button title="主按钮" onPress={() => {}} />
      <Button title="次按钮" type="outline" containerStyle={styles.gap} onPress={onOpenOverlay} />

      <Divider style={styles.divider} />
      <Text style={styles.section}>Card / Avatar / Badge</Text>
      <Card containerStyle={styles.card}>
        <Card.Title>卡片标题</Card.Title>
        <Card.Divider />
        <View style={styles.row}>
          <Avatar rounded title="鸿" containerStyle={styles.avatar} />
          <View style={styles.gapLeft}>
            <Text>用户昵称</Text>
            <Badge value="OHOS" status="success" />
          </View>
        </View>
      </Card>

      <Divider style={styles.divider} />
      <Text style={styles.section}>ListItem + Chevron</Text>
      <ListItem bottomDivider onPress={() => {}}>
        <ListItem.Content>
          <ListItem.Title>列表项一</ListItem.Title>
          <ListItem.Subtitle>带 Chevron</ListItem.Subtitle>
        </ListItem.Content>
        <ListItem.Chevron />
      </ListItem>

      <Divider style={styles.divider} />
      <Button title="打开 Overlay" icon={<Icon name="layers" color="#fff" />} onPress={onOpenOverlay} />
    </View>
  );
}

export const StaticShowcase = memo(StaticShowcaseInner);

7.3 交互区(InteractivePanel)

展示 Input(带左侧图标)、SearchBar(platform="default")、CheckBox(同意协议)、Switch(启用通知)、Slider(滑块,实时显示数值)。所有交互状态保持在组件内部:

复制代码
import React, { memo, useCallback, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Input, SearchBar, CheckBox, Switch, Slider, Divider } from 'react-native-elements';

function InteractivePanelInner() {
  const [name, setName] = useState('');
  const [search, setSearch] = useState('');
  const [checked, setChecked] = useState(false);
  const [enabled, setEnabled] = useState(true);
  const [sliderValue, setSliderValue] = useState(0.4);
  const [displayValue, setDisplayValue] = useState(0.4);

  const onSlidingComplete = useCallback((v) => {
    setSliderValue(v);
    setDisplayValue(v);
  }, []);

  return (
    <View>
      <Text style={styles.section}>Input</Text>
      <Input placeholder="请输入昵称" value={name} onChangeText={setName}
        leftIcon={{ type: 'material', name: 'person' }} />

      <Text style={styles.section}>SearchBar (default)</Text>
      <SearchBar platform="default" placeholder="搜索..." onChangeText={setSearch} value={search}
        lightTheme containerStyle={styles.searchContainer} inputContainerStyle={styles.searchInput} />

      <Divider style={styles.divider} />
      <Text style={styles.section}>CheckBox / Switch / Slider</Text>
      <CheckBox title="同意协议" checked={checked} onPress={() => setChecked(!checked)} />
      <View style={styles.rowBetween}>
        <Text>启用通知</Text>
        <Switch value={enabled} onValueChange={setEnabled} />
      </View>
      <Text style={styles.sliderLabel}>滑块: {displayValue.toFixed(2)}</Text>
      <Slider value={sliderValue} onValueChange={setDisplayValue} onSlidingComplete={onSlidingComplete}
        maximumValue={1} minimumValue={0} thumbStyle={styles.thumb} allowTouchTrack />
    </View>
  );
}

export const InteractivePanel = memo(InteractivePanelInner);

7.4 动画遮罩(FadeOverlay)

演示在 RNOH 上使用 React Native 核心 Modal + Animated 实现淡入缩放效果,useNativeDriver: true 开启原生驱动:

复制代码
import React, { useEffect, useRef } from 'react';
import { Animated, Modal, Pressable, StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native-elements';

export function FadeOverlay({ visible, onClose }) {
  const opacity = useRef(new Animated.Value(0)).current;
  const scale = useRef(new Animated.Value(0.92)).current;

  useEffect(() => {
    if (visible) {
      opacity.setValue(0);
      scale.setValue(0.92);
      Animated.parallel([
        Animated.timing(opacity, { toValue: 1, duration: 220, useNativeDriver: true }),
        Animated.spring(scale, { toValue: 1, friction: 7, tension: 80, useNativeDriver: true }),
      ]).start();
    }
  }, [visible, opacity, scale]);

  const closeAnimated = () => {
    Animated.parallel([
      Animated.timing(opacity, { toValue: 0, duration: 160, useNativeDriver: true }),
      Animated.timing(scale, { toValue: 0.94, duration: 160, useNativeDriver: true }),
    ]).start(({ finished }) => { if (finished) onClose(); });
  };

  return (
    <Modal visible={visible} transparent animationType="none" onRequestClose={closeAnimated} statusBarTranslucent>
      <View style={styles.center}>
        <Pressable style={StyleSheet.absoluteFill} onPress={closeAnimated}>
          <Animated.View style={[styles.backdrop, { opacity }]} />
        </Pressable>
        <Animated.View style={[styles.card, { opacity, transform: [{ scale }] }]}>
          <Text style={styles.title}>Overlay 示例</Text>
          <Text style={styles.body}>原生驱动淡入 / 缩放,点击遮罩关闭</Text>
          <Button title="关闭" containerStyle={styles.gap} onPress={closeAnimated} />
        </Animated.View>
      </View>
    </Modal>
  );
}

八、构建与真机验证

8.1 编译构建

使用 DevEco Studio 打开 RNElementsDemo/harmony 目录,通过 USB 连接 MatePad Edge 真机,点击 Run 编译安装。首次编译约 5-8 分钟(包含原生 so 库编译),后续增量编译约 30 秒。

构建配置注意事项:

  • entry/build-profile.json5 的 abiFilters 必须包含 arm64-v8a(真机架构),建议同时包含 x86_64(模拟器),一包两用;
  • 修改 abiFilters 后必须 Clean Project 再 Rebuild,清理 entry/.cxx 和 cmake/libs 中间产物,否则可能继续打出旧 ABI 包;
  • 打包后可解压 HAP(本质是 zip)校验 libs/ 目录下是否存在对应架构的 librnoh_app.so。

8.2 平板模式验证

设备以平板形态使用,触摸屏交互,屏幕分辨率约 2800×1840(横屏),应用全屏运行。验证结果:

|---------------|------------------|-------------------------------------|--------|
| 用例 | 操作 | 预期 | 结果 |
| Button 主按钮 | 触摸点击 | 透明度反馈,onPress 触发 | 通过 |
| Button 描边按钮 | 触摸点击 | 边框样式正常,点击触发 Overlay | 通过 |
| Card 卡片 | 视觉检查 | 圆角、elevation 阴影、标题分割线正常 | 通过 |
| Avatar 头像 | 视觉检查 | 圆形头像,文字"鸿"居中 | 通过 |
| Badge 徽标 | 视觉检查 | "OHOS"绿色徽标正常显示 | 通过 |
| Input 输入框 | 软键盘输入文字 | 输入正常,左侧 person 图标显示 | 通过 |
| SearchBar 搜索框 | 软键盘输入 | 搜索框样式正常,可输入文字 | 通过 |
| CheckBox 复选框 | 触摸切换 | 勾选状态切换,Material 勾选图标显示 | 通过 |
| Switch 开关 | 触摸切换 | 开关切换,Android 风格轨道/滑块颜色正确 | 通过 |
| Slider 滑块 | 触摸拖动 | 滑块可拖动,数值实时更新,allowTouchTrack 点击轨道跳转 | 通过 |
| ListItem 列表项 | 视觉检查 + 触摸 | 布局正常,右侧 Material Chevron 箭头显示 | 通过 |
| Overlay 遮罩 | 点击"打开 Overlay"按钮 | Modal 弹出,淡入缩放动画流畅,点击遮罩/按钮关闭 | 通过 |
| 页面滚动 | 上下滑动 | ScrollView 滚动流畅,无卡顿 | 通过 |

演示视频:

MatePad Edge react-native平板展示

8.3 PC 模式验证

设备连接键盘鼠标后切换到 PC 模式,应用以窗口化方式运行,支持鼠标点击、滚轮滚动、键盘输入。验证结果:

|----------------------|--------------------------|------------------------------|--------|
| 用例 | 操作 | 预期 | 结果 |
| 窗口化布局 | 调整窗口大小 | 内容 maxWidth 限制生效,不过度拉伸,布局自适应 | 通过 |
| 鼠标点击 Button | 鼠标左键点击 | 点击反馈正常,onPress 触发 | 通过 |
| 物理键盘输入 | Input/SearchBar 中用物理键盘打字 | 输入正常,光标跟随 | 通过 |
| 鼠标拖动 Slider | 鼠标按住滑块拖动 | 拖动流畅,数值实时更新 | 通过 |
| 滚轮滚动页面 | 鼠标滚轮上下滚动 | 页面滚动正常 | 通过 |
| Modal 居中显示 | 打开 Overlay | Modal 在窗口内居中显示,遮罩覆盖整个窗口 | 通过 |
| CheckBox/Switch 鼠标切换 | 鼠标点击切换 | 状态切换正常 | 通过 |

演示视频:

MatePad Edge react-native电脑展示

8.4 组件支持状态

|-----------------------------------------------|--------|------------------------------------------------|
| 组件 | 状态 | 备注 |
| Button / ButtonGroup / Chip / FAB / SpeedDial | 支持 | 鸿蒙使用 TouchableOpacity,无 Ripple 水波纹 |
| Card / CardTitle / CardDivider / CardImage | 支持 | 圆角和 elevation 阴影正常 |
| Input | 支持 | 左侧图标需配置字体 |
| Avatar / Accessory | 支持 | |
| Badge / withBadge | 支持 | |
| SearchBar | 支持 | 鸿蒙端使用 platform="default" 或 "android" |
| ListItem 及全部子组件 | 支持 | Chevron 使用 Material 图标 |
| CheckBox / CheckBoxIcon | 支持 | 需配置 vector-icons 字体 |
| Switch | 支持 | Android 风格样式 |
| Slider | 支持 | allowTouchTrack 正常 |
| Divider | 支持 | |
| Header | 支持 | 改用 RN 核心 SafeAreaView |
| LinearProgress | 支持 | useNativeDriver 动画正常 |
| Tab / TabView | 支持 | 建议视觉验证 |
| SocialIcon / PricingCard / Tile / Rating | 支持 | 图标需配置字体 |
| Icon | 有限支持 | 依赖 react-native-vector-icons + 字体注册,未注册的字体集不显示 |
| Overlay / Dialog / BottomSheet / Tooltip | 待充分验证 | 依赖 RN Modal,基本功能可用,复杂动画和边缘情况需进一步验证 |
| ListItemSwipeable | 待验证 | 依赖 react-native-gesture-handler,需确认该库的鸿蒙适配状态 |

九、常见问题与解决方案

问题一:真机安装后闪退,报 libRNOHApp is undefined

现象

在 PC 模拟器上运行正常,打包安装到 MatePad Edge 真机后,应用启动瞬间闪退。通过 hdc hilog 查看日志,核心报错是:

复制代码
Couldn't create bindings between ETS and CPP. libRNOHApp is undefined.
load librnoh_app.so failed ... No such file or directory

原因

这个报错和业务代码无关,是 HAP 包里的 native 库 ABI 和真机 CPU 架构不匹配。

RNOH 启动时,ArkTS 侧需要通过 NAPI 加载 librnoh_app.so 来建立 ETS 和 C++ 的绑定。如果 HAP 包里没有当前设备架构对应的 so 文件,加载就会失败,libRNOHApp 变成 undefined,初始化直接 Fatal,进程退出。

具体到这个项目,一开始为了加快模拟器的编译和安装速度,把 harmony/entry/build-profile.json5 里的 abiFilters 只保留了 x86_64(PC 模拟器用的架构)。但 MatePad Edge 真机是 arm64-v8a 架构,打出来的 HAP 里只有 libs/x86_64/librnoh_app.so,没有 libs/arm64-v8a/librnoh_app.so,真机启动时找不到对应 so,立刻闪退。

解决方法

  1. 修改 harmony/entry/build-profile.json5,把 abiFilters 改成同时包含 arm64-v8a 和 x86_64:

    "abiFilters": ["arm64-v8a", "x86_64"]

真机(手机/平板)需要 arm64-v8a,PC 模拟器需要 x86_64,双 ABI 一包两用。

  1. 改完 abiFilters 后一定要 Clean 再全量编译。只改配置不清理的话,cmake 可能继续用之前的缓存,打出来的还是旧 ABI 的包。在 DevEco Studio 里用 Build → Clean Project,然后再 Rebuild。

  2. 打包完成后,可以解压 HAP 文件确认里面是否包含两个架构的 so:

    libs/arm64-v8a/librnoh_app.so
    libs/x86_64/librnoh_app.so

  1. 确认无误后再安装到真机。

小结:遇到 libRNOHApp is undefined,先查 ABI 配置和 HAP 里的 so 文件,不要先去翻业务组件代码。模拟器能跑不代表真机能跑,为了提速只打单 ABI 是真机闪退的常见原因。

问题二:应用启动后长时间白屏,很久才出内容

现象

应用能正常安装和启动,但首屏长时间白屏(大概 30 秒到 1 分钟),然后才突然显示出页面内容。期间没有报错,进程也没有退出,看起来像是卡住了。

原因

这个问题出在 JS Bundle 的加载策略上。RNOH 页面初始化时通过 JSBundleProvider 来拉取 JS 包。如果配置里优先走 MetroJSBundleProvider(调试用的 Metro 服务),设备会尝试连接开发机的 Metro 服务(默认 8081 端口)。

当 Metro 服务没开、或者设备和开发机之间网络不通、或者端口没做反向代理时,连接会一直超时重试。等超时结束后,才会 fallback 到本地资源包。这就导致了"先进去白屏很久,然后才有内容"的现象。

具体到这个项目,一开始 harmony/entry/src/main/ets/pages/Index.ets 里 jsBundleProvider 的顺序是 Metro 优先,真机演示时 Metro 没开,就出现了长时间白屏。

解决方法

  1. 调整 Index.ets 里 jsBundleProvider 的顺序,优先用 ResourceJSBundleProvider(读本地 rawfile 里的 bundle.harmony.js),MetroJSBundleProvider 只作为 debug 模式下的备选:

    jsBundleProvider: this.rnohCoreContext?.isDebugModeEnabled
    ? new AnyJSBundleProvider([
    new ResourceJSBundleProvider(
    this.rnohCoreContext.uiAbilityContext.resourceManager,
    'bundle.harmony.js'
    ),
    new MetroJSBundleProvider(),
    ])
    : new ResourceJSBundleProvider(
    this.rnohCoreContext.uiAbilityContext.resourceManager,
    'bundle.harmony.js'
    ),

这样一进 App 就直接用打进 HAP 的离线 bundle 秒开,Metro 只在需要热更新时才连接,不再卡首启。

  1. 确保本地 bundle 已经打包进工程。发布或真机演示前,先执行 Metro 打包命令,把生成的 bundle.harmony.js 放到 harmony/entry/src/main/resources/rawfile/ 目录下,再编 HAP。

  2. 如果需要热更新调试,再手动开 Metro:

    npm start

然后用 hdc 做端口反向代理:

复制代码
hdc rport tcp:8081 tcp:8081

注意 Metro 对 Node 版本有要求,这个项目里用 Node 24 可以正常跑,DevEco 自带的 Node 18 可能会有兼容性问题。

小结:启动白屏优先查 Bundle Provider 的顺序。真机演示应该默认优先本地 Resource Bundle,Metro 只能当开发热更新通道,不能作为首启的硬依赖,否则弱网或没开 Metro 时就会出现长时间白屏。

十、总结

这次 react-native-elements 的鸿蒙适配走下来,最大的感受是:纯 JS UI 库的适配门槛不高,但真机调试的坑比想象中多。

代码层面主要是加平台判断、处理 React 19 兼容性、替换 SafeAreaView、注册图标字体,没有涉及原生模块,适合作为 RNOH 适配的入门练手。

真正花时间的是真机调试------ABI 配置不对导致闪退、Bundle 加载顺序不对导致白屏,这两个问题在模拟器上都复现不出来。所以适配完一定要上真机跑一遍,不能只在模拟器上验证。

整体来看,react-native-elements 是一个性价比很高的适配项目,难度适中,覆盖面广,做完对 RNOH 的整个开发流程会有比较完整的理解。

相关推荐
lqj_本人2 小时前
白泽上手:给小鸿 SE 写一个温控风扇工程
harmonyos
贾伟康2 小时前
【口算王|12】HarmonyOS ArkTS 启动页实战:处理 Splash 到训练首页的稳定切换
harmonyos·arkts·启动优化·uiability·windowstage
万物智能信息科技3 小时前
RK3568 的多路显示移植—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
linux·开发语言·华为·开源·harmonyos
万物智能信息科技4 小时前
MIPI DSI屏幕输出—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
嵌入式硬件·华为·开源·harmonyos·鸿蒙
贾伟康5 小时前
【口算王|13】HarmonyOS ArkTS 应用启动链路实战:从 EntryAbility 到首屏加载保持窗口与路由稳定
harmonyos·arkts·arkui·应用启动·entryability
万物智能信息科技7 小时前
LVDS屏幕输出桌面—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
人工智能·华为·开源·harmonyos·鸿蒙
万物智能信息科技7 小时前
RGB并行屏输出—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
单片机·嵌入式硬件·华为·鸿蒙
知潮网8 小时前
HarmonyOS 7正式发布:华为分享远程直传无距离限制,还能和iPhone、Apple Watch互联
华为·iphone·harmonyos
云运维笔记9 小时前
华为VRP系统全解析:网络设备的智能核心
服务器·网络·华为