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 一包两用。

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

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

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

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

小结:遇到 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 只在需要热更新时才连接,不再卡首启。

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

3. 如果需要热更新调试,再手动开 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 的整个开发流程会有比较完整的理解。

Logo

一站式 AI 云服务平台

更多推荐