React Native 三方库「react-native-fold-detection」的鸿蒙化适配指南
开发工具: 华为云码道
react-native-fold-detection 将折叠状态与窗口内布局信息封装为 React Native Provider 和 Hook,应用可以依据系统返回的数据调整页面布局。本文以 @react-native-ohos/react-native-fold-detection 0.3.2-0.0.1 为例,介绍源码准备、OHOS 工程配置、原生实现和 example 设备运行。
本适配保留上游 Android/iOS 实现,新增鸿蒙支持。源码位于 AtomGit 配套仓库,文中的代码以提交 4d7627331b00f1a7d3d00c6b5eb2b147ec6639d0 为参考。配套仓库包含对应的鸿蒙实现和示例工程,文章中的版本、接口与运行结果以该仓库为准。

给初学者的阅读提示:
本文会同时出现四个目录或产物:插件仓库是源码所在的位置,example/是用来演示插件的应用,HAR 是插件的鸿蒙原生模块包,HAP 是最终安装到手机上的应用包。除非特别说明,命令都在插件仓库根目录执行。可以先记住几个名词:RNOH 是 React Native 连接鸿蒙的运行层,ArkTS 是鸿蒙原生代码使用的语言,TurboModule 是 React Native 调用原生能力的一种模块,Metro 是把 JS/TS 打包成应用资源的工具。调用链是:React Native 页面调用 JS/TS 接口,RNOH 把调用转给 ArkTS 原生模块,ArkTS 再调用 HarmonyOS 系统能力,结果沿原路返回页面。
一、插件简介与适配目标
折叠适配既涉及是否支持折叠,也涉及当前状态、折痕方向以及折痕在应用窗口中的位置。本库保留上游单折痕模型,通过系统显示和窗口接口生成布局快照。
例如,阅读页面可以根据窗口内折痕分栏,拍摄页面可以在可确认的桌面姿态下重新安排控件。没有折痕几何数据时应保留普通布局,不应凭屏幕尺寸虚构铰链位置。
Provider 订阅 onLayoutInfoChange 后启动原生监听,事件负载为 {displayFeatures: LayoutInfo}。多个 Provider 共享原生监听;最后一个卸载停止。上游只有 FLAT/HALF_OPENED,闭合、未知和不支持场景不能表达独立闭合枚举,isFlat 不等于物理完全展开。
二、环境准备
环境搭建参考社区文档:React Native OpenHarmony 环境初始化,完成 Node、对应 RN/RNOH 依赖、环境变量和 DevEco Studio 配置;框架能力与版本关系见 ohos_react_native。框架版本按下表选择,不直接套用最新分支。
完成后,在宿主机终端执行以下命令,确认当前工具链,并能发现目标设备:
node --version
npm --version
hdc list targets
node --version 和 npm --version 用来确认 Node.js 与包管理器可用;hdc list targets 用来确认电脑已经发现鸿蒙设备或模拟器。前两条命令只要能打印版本号即可,最后一条至少应出现一个设备 ID;如果列表为空,先处理 USB 调试授权或模拟器连接,再继续后面的构建。
工程使用的工具链和 SDK 配置如下:
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Node.js | 24.15.0 | 验证环境中的版本 |
| React Native | 0.84.1 | 鸿蒙宿主 RN 版本 |
| React | 19.2.3 | 与鸿蒙宿主配套 |
| RNOH / Harmony CLI | 0.84.3 | JS 框架与鸿蒙 CLI |
| OHPM RNOH | 0.84.3 | 原生运行时依赖 |
| compileSdkVersion | 26.0.0 | 编译 SDK |
| targetSdkVersion | 26.0.0 | 应用目标行为版本 |
| compatibleSdkVersion | 5.0.5(17) | 工程最低安装配置,不是最低设备实测结论 |
| DevEco Studio | 26.0.0 Release(HarmonyOS SDK 26.0.0 / API 26) | 打开并构建 example/harmony 工程 |
| 适配包版本 | 0.3.2-0.0.1 | package.json 中的交付版本 |
| 原生实现 | ArkTS / C++ | 系统能力与 RNOH 桥接 |
| 插件产物 | harmony/fold_detection.har | 供宿主消费的 release HAR |
鸿蒙工具依赖位于 example/harmony/package.json,与上游根目录及 Android/iOS 示例依赖分开。不要从旧示例的 RN 版本推断鸿蒙运行版本。
在这张表里,React Native 和 React 是页面使用的 JavaScript 层;RNOH 和 Harmony CLI 负责把这层代码接到鸿蒙工程;compileSdkVersion 是编译时使用的 API,targetSdkVersion 是应用面向的系统行为版本,compatibleSdkVersion 是工程声明的最低兼容版本。HAR 是插件交付包,HAP 是可以安装到设备上的应用包。
2.1 开发套件版本与工程中的 SDK 版本配置
26.0.0 是本工程 compile/target 字段值,对应编译 API 26;5.0.5(17) 声明最低兼容 API 17。SDK、IDE 版本和手机系统版本是不同信息。
对应 example/harmony/build-profile.json5 的 product 配置如下,合并到现有工程时保留其他产品和模块:
{
"name": "default",
"compileSdkVersion": "26.0.0",
"compatibleSdkVersion": "5.0.5(17)",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS"
}
这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 17。安装后能否使用对应能力,还取决于设备系统和具体硬件,最低配置不代表对应版本设备已通过真机验证。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方插件时,先从 npm 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
本例上游为 @logicwind/react-native-fold-detection,基线版本 0.3.2,采用 MIT 许可证。当前配套仓库已经同步了适配结果;直接接入时无需再次导入上游。
需要继续维护时,在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、package.json、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
3.2 将代码拉取到宿主机
在安装了对应 RN/RNOH 工具链和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://atomgit.com/oh-react-native/react-native-fold-detection.git
cd react-native-fold-detection
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD
git clone 会创建 react-native-fold-detection/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 package.json、harmony/ 和 example/。Git 仓库名与 npm 包名 @react-native-ohos/react-native-fold-detection 不是同一标识。
需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:
git switch --detach 4d7627331b00f1a7d3d00c6b5eb2b147ec6639d0
3.3 在仓库根目录创建适配分支
配套仓库的参考分支为 main。如果只是运行示例,保留固定提交即可;需要继续修改时,从该提交创建自己的工作分支。下面是建议的新分支名,不表示远端已经存在同名分支:
git switch -c feat/ohos_fold_detection_0.3.2-0.0.1
git branch --show-current
如果该分支已存在,使用 git switch feat/ohos_fold_detection_0.3.2-0.0.1 切换,并核对起点是否仍为本文参考版本。
3.4 自动补全 OHOS 适配结构
配套仓库已经包含 harmony/fold_detection/ 和 example/harmony/,直接运行示例时可以跳过结构生成。JS/TS 入口、原生 Package、C++ 桥接和宿主工程共同完成接线,不能只新建一个目录就称为适配完成。
本库的两种原生方法使用手写 C++ ARK_METHOD_METADATA 注册,不存在需要照搬的库级 codegen 命令。构建直接使用仓库中的桥接源码。
3.5 适配后的项目目录
适配后的关键目录如下:
react-native-fold-detection/
├── package.json
├── src/context/FoldingFeatureContext.tsx
├── src/types/index.ts
├── harmony/
│ ├── fold_detection.har
│ └── fold_detection/
│ ├── index.ets
│ ├── oh-package.json5
│ └── src/main/
│ ├── ets/FoldingFeatureTurboModule.ts
│ ├── ets/RNFoldDetectionPackage.ets
│ ├── cpp/
│ └── module.json5
├── example/harmony/App.tsx
├── example/harmony/entry/
├── scripts/build-harmony.sh
├── README.harmony.md
└── VALIDATION.md
| 文件 | 主要职责 |
|---|---|
src/context/FoldingFeatureContext.tsx | 业务公开接口及 RN 层行为 |
src/types/index.ts | 类型/模块协议 |
harmony/fold_detection/src/main/ets/FoldingFeatureTurboModule.ts | 系统能力与生命周期 |
RNFoldDetectionPackage.ets | 注册 UI TurboModule 工厂 |
RNFoldDetectionPackage.h | C++ Package 与模块映射 |
example/harmony/entry | 可安装宿主应用 |
example/harmony/App.tsx | 完整示例页面与诊断操作 |
四、JS/TS 接口与原生桥接分析
OHOS 实现需要遵循 JS/TS 层已有的方法、参数和回调约定。
对初学者来说,可以把这一层看成一条“传话链”:页面只负责调用公开函数,RNOH 负责把调用送到 ArkTS,ArkTS 负责访问系统 API,最后把成功结果或错误交回 JS/TS。只改页面代码而没有接好这条链,通常会得到“找不到原生模块”或“方法未实现”。先阅读 src/context/FoldingFeatureContext.tsx 和 src/types/index.ts,再查看 harmony/fold_detection/src/main/ets/FoldingFeatureTurboModule.ts 的原生调用。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。
本例的对应关系如下:
| 公开入口 | 模块协议 | OHOS 实现 | 应保持的行为 |
|---|---|---|---|
| FoldingFeatureProvider | FoldingFeature.startListening / stopListening | display 与 window 事件 | 先注册 JS 再发当前快照,多 Provider 共享 |
| useFoldingFeature() | onLayoutInfoChange.displayFeatures | createFoldLayout | 返回 layoutInfo 与派生布尔值 |
| isTableTop / isBook / isFlat | HALF_OPENED + orientation | 布局字段派生 | 缺折痕时方向默认值不能当实测 |
| 错误事件 | onError: {error:string} | 系统读取或监听异常 | Provider 记录日志,Hook 不提供 error 字段 |
4.1 跨端架构与调用时序
本例通过 RNOH UI TurboModule 执行系统调用,JS 模块名称为 FoldingFeature。ArkTS 和 C++ Package 同时参与注册;JS/npm 依赖安装成功不代表原生模块已经接入。
4.1.1 一次完整调用的时序
4.2 数据模型与模块声明
以下声明来自 src/types/index.ts:
export enum FoldingFeatureState {
FLAT = 'FLAT',
HALF_OPENED = 'HALF_OPENED',
}
export enum FoldingFeatureOrientation {
VERTICAL = 'VERTICAL',
HORIZONTAL = 'HORIZONTAL',
}
export enum FoldingFeatureOcclusionType {
NONE = 'NONE',
FULL = 'FULL',
}
export type LayoutInfo = {
state: FoldingFeatureState;
occlusionType: FoldingFeatureOcclusionType;
orientation: FoldingFeatureOrientation;
isSeparating: boolean;
isFoldSupported: boolean;
bounds?: {
top: number;
bottom: number;
left: number;
right: number;
};
};
Provider 订阅 onLayoutInfoChange 后启动原生监听,事件负载为 {displayFeatures: LayoutInfo}。多个 Provider 共享原生监听;最后一个卸载停止。上游只有 FLAT/HALF_OPENED,闭合、未知和不支持场景不能表达独立闭合枚举,isFlat 不等于物理完全展开。
4.3 公开 API 与接口封装
以下为 src/context/FoldingFeatureContext.tsx 的实现,保留完整上下文以说明生命周期和状态管理:
import React, {
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import type { PropsWithChildren } from 'react';
import { NativeEventEmitter, Platform } from 'react-native';
import { retainFoldListener } from '../FoldingFeature';
import {
FoldingFeatureOcclusionType,
FoldingFeatureOrientation,
FoldingFeatureState,
} from '../types';
import type { LayoutInfo } from '../types';
type FoldingFeatureContextProps = {
layoutInfo: LayoutInfo;
isTableTop: boolean;
isBook: boolean;
isFlat: boolean;
};
export const FoldingFeatureContext = createContext<FoldingFeatureContextProps>({
layoutInfo: {
state: FoldingFeatureState.FLAT,
occlusionType: FoldingFeatureOcclusionType.NONE,
orientation: FoldingFeatureOrientation.VERTICAL,
isSeparating: false,
isFoldSupported: false,
},
// helper state
isTableTop: false,
isBook: false,
isFlat: true,
});
export const useFoldingFeature = () => {
const context = useContext(FoldingFeatureContext);
if (context === undefined) {
throw new Error('useFoldingFeature was used outside of its provider');
}
if (Platform.OS === 'ios') {
return {
layoutInfo: {},
isTableTop: false,
isBook: false,
isFlat: true,
};
}
return context;
};
export const FoldingFeatureProvider = ({ children }: PropsWithChildren<{}>) => {
const value = useProvideFunc();
if (Platform.OS === 'ios') {
return <>{children}</>;
}
return (
<FoldingFeatureContext.Provider value={value}>
{children}
</FoldingFeatureContext.Provider>
);
};
const useProvideFunc = (): FoldingFeatureContextProps => {
const [layoutInfo, setLayoutInfo] = useState<LayoutInfo>({
state: FoldingFeatureState.FLAT,
occlusionType: FoldingFeatureOcclusionType.NONE,
orientation: FoldingFeatureOrientation.VERTICAL,
isSeparating: false,
isFoldSupported: false,
});
const updateLayoutInfo = (event: LayoutInfo) => {
setLayoutInfo(event);
};
const isTableTop = useMemo(() => {
return (
layoutInfo.state === FoldingFeatureState.HALF_OPENED &&
layoutInfo.orientation === FoldingFeatureOrientation.HORIZONTAL
);
}, [layoutInfo]);
const isBook = useMemo(() => {
return (
layoutInfo.state === FoldingFeatureState.HALF_OPENED &&
layoutInfo.orientation === FoldingFeatureOrientation.VERTICAL
);
}, [layoutInfo]);
const isFlat = useMemo(() => {
return !(isTableTop || isBook);
}, [isTableTop, isBook]);
useEffect(() => {
if (Platform.OS === 'ios') {
return; // Just return early from the effect
}
const eventEmitter = new NativeEventEmitter();
const layoutSubscription = eventEmitter.addListener(
'onLayoutInfoChange',
(event) => {
if (event?.displayFeatures) {
updateLayoutInfo(event.displayFeatures);
}
}
);
const errorSubscription = eventEmitter.addListener('onError', (event) => {
if (event?.error) {
console.log('FoldingFeature', event.error);
}
});
let release: () => void;
try {
// Subscribe before starting: Harmony emits an initial snapshot immediately.
release = retainFoldListener();
} catch (error) {
layoutSubscription.remove();
errorSubscription.remove();
throw error;
}
return () => {
layoutSubscription.remove();
errorSubscription.remove();
release();
};
}, []);
return {
layoutInfo,
isTableTop,
isBook,
isFlat,
};
};
4.4 模块协议与资源处理
原生端需要保持模块名、方法名及参数语义一致。FoldingFeature 是桥接标识,不等同于 npm 包名或 HAR 文件名。
Provider 自己注册布局和错误事件,并在卸载时移除两种事件订阅与释放引用计数。业务只读取 Hook,不额外重复启动原生监听。初始默认值不能证明系统已识别;需要诊断时使用示例应用的原生事件计数和错误区域。
五、补全 OHOS 原生实现与工程配置
这一章只处理鸿蒙侧文件。插件 HAR 负责提供原生能力,example/harmony/entry 负责把这些能力装进一个可以安装的 HAP;两者都配置好,手机上的示例页面才有机会调用到系统功能。
5.1 在 FoldingFeatureTurboModule.ts 中实现原生能力
业务沿用第四节公开接口;下面按源码说明原生层如何完成系统调用、结果转换和异常处理。
5.1.1 引入 RNOH 与系统能力
原生文件位于 harmony/fold_detection/src/main/ets/FoldingFeatureTurboModule.ts。本例实现继承 UITurboModule,通过 this.ctx 获取宿主运行时上下文。
5.1.2 核心实现与错误处理
FoldLayout 将自然屏幕折痕按 rotation 转到当前屏幕,再换算和裁剪为窗口内物理像素坐标。只有半折叠且折痕贯穿窗口才设置 isSeparating=true;柔性屏折痕不等于不透明铰链,occlusionType 保持 NONE。异步窗口请求通过 generation 防止停止后重新注册;清理失败会保留回调以便重试。
以下为该原生文件的完整源码:
// SPDX-License-Identifier: MIT
import display from '@ohos.display';
import window from '@ohos.window';
import { UITurboModule } from '@rnoh/react-native-openharmony/ts';
import { createFoldLayout, Rect } from './FoldLayout';
export class FoldingFeatureTurboModule extends UITurboModule {
static readonly NAME: string = 'FoldingFeature';
private listening: boolean = false;
private generation: number = 0;
private cleanups: Array<() => void> = [];
private hostWindow: window.Window | undefined;
private readonly onFold = (_status: display.FoldStatus): void => { this.emitSnapshot(); };
private readonly onMode = (_mode: display.FoldDisplayMode): void => { this.emitSnapshot(); };
private readonly onDisplay = (_id: number): void => { this.emitSnapshot(); };
private readonly onWindow = (_options: window.RectChangeOptions): void => { this.emitSnapshot(); };
startListening(): void {
if (this.listening) { this.emitSnapshot(); return; }
if (this.cleanups.length > 0) {
this.stopListening();
if (this.cleanups.length > 0) {
this.emitError('startListening', new Error('Previous SDK subscriptions could not be removed'));
return;
}
}
this.listening = true;
const generation = ++this.generation;
try {
display.on('foldStatusChange', this.onFold);
this.cleanups.push(() => display.off('foldStatusChange', this.onFold));
display.on('foldDisplayModeChange', this.onMode);
this.cleanups.push(() => display.off('foldDisplayModeChange', this.onMode));
display.on('change', this.onDisplay);
this.cleanups.push(() => display.off('change', this.onDisplay));
this.cleanups.push(this.ctx.rnInstance.subscribeToLifecycleEvents('FOREGROUND', () => this.emitSnapshot()));
this.emitSnapshot();
this.ctx.rnInstance.getRNWindow().then((hostWindow: window.Window) => {
if (!this.listening || generation !== this.generation) { return; }
this.hostWindow = hostWindow;
try {
hostWindow.on('windowRectChange', this.onWindow);
this.cleanups.push(() => hostWindow.off('windowRectChange', this.onWindow));
this.emitSnapshot();
} catch (error) { this.emitError('watch window', error as Error); }
}).catch((error: Error) => {
if (this.listening && generation === this.generation) { this.emitError('get window', error); }
});
} catch (error) {
this.stopListening();
this.emitError('startListening', error as Error);
}
}
stopListening(): void {
this.listening = false;
++this.generation;
this.hostWindow = undefined;
// Always attempt all removals; one SDK failure must not leak other listeners.
const pending = this.cleanups;
this.cleanups = [];
pending.reverse().forEach((cleanup: () => void) => {
try { cleanup(); } catch (error) {
this.cleanups.push(cleanup);
this.emitError('stopListening', error as Error);
}
});
}
__onDestroy__(): void { this.stopListening(); }
private emitSnapshot(): void {
if (!this.listening) { return; }
try {
const supported = display.isFoldable();
const status = supported ? display.getFoldStatus() : display.FoldStatus.FOLD_STATUS_UNKNOWN;
const screen = display.getDefaultDisplaySync();
let crease: Rect | undefined;
let viewport: Rect | undefined;
if (supported && status !== display.FoldStatus.FOLD_STATUS_UNKNOWN &&
status !== display.FoldStatus.FOLD_STATUS_FOLDED && status <= 3) {
try {
const region = display.getCurrentFoldCreaseRegion();
// Some emulator images return undefined despite the SDK's non-null type.
// Bounds are optional: missing geometry must not invalidate fold state.
if (region && region.displayId === screen.id) { crease = region.creaseRects[0]; }
} catch (error) { this.emitError('get crease region', error as Error); }
}
if (this.hostWindow) {
const properties = this.hostWindow.getWindowProperties();
if (properties.displayId === undefined || properties.displayId === screen.id) {
viewport = properties.windowRect;
}
}
if (status > 3) { this.emitError('read posture', new Error('Multiple fold axes are not representable by the upstream single-hinge API')); }
const layout = createFoldLayout(supported, status, crease, viewport,
screen.rotation, screen.width, screen.height);
this.ctx.rnInstance.emitDeviceEvent('onLayoutInfoChange', { displayFeatures: layout });
} catch (error) { this.emitError('read folding feature', error as Error); }
}
private emitError(operation: string, error: Error): void {
this.ctx.rnInstance.emitDeviceEvent('onError', { error: `${operation}: ${error.message}` });
}
}
5.1.3 数据转换与边界处理
辅助文件 harmony/fold_detection/src/main/ets/FoldLayout.ts 将纯数据处理与系统读取分开,便于覆盖边界;测试系统替身不能代替真机能力。
// SPDX-License-Identifier: MIT
export interface Rect { left: number; top: number; width: number; height: number; }
export interface Bounds { left: number; top: number; right: number; bottom: number; }
export interface FoldLayout {
state: string;
orientation: string;
occlusionType: string;
isSeparating: boolean;
isFoldSupported: boolean;
bounds?: Bounds;
}
// The public upstream model represents one hinge. Do not invent a combined
// posture for devices with two fold axes (statuses 11, 12, 13, 21, 22, 23).
export function createFoldLayout(supported: boolean, status: number, crease: Rect | undefined,
viewport: Rect | undefined, rotation: number, width: number, height: number): FoldLayout {
const result: FoldLayout = {
state: supported && status === 3 ? 'HALF_OPENED' : 'FLAT',
orientation: 'VERTICAL', occlusionType: 'NONE',
isSeparating: false, isFoldSupported: supported
};
if (!supported || status === 0 || status === 2 || status > 3 || !crease) { return result; }
// FoldCreaseRegion uses natural-display pixels. Rotate into current screen
// coordinates, then translate/clip to the host window (also physical pixels).
let rect: Rect = crease;
if (rotation === 1) {
rect = { left: width - crease.top - crease.height, top: crease.left,
width: crease.height, height: crease.width };
} else if (rotation === 2) {
rect = { left: width - crease.left - crease.width, top: height - crease.top - crease.height,
width: crease.width, height: crease.height };
} else if (rotation === 3) {
rect = { left: crease.top, top: height - crease.left - crease.width,
width: crease.height, height: crease.width };
}
const horizontal = rect.width > rect.height;
result.orientation = horizontal ? 'HORIZONTAL' : 'VERTICAL';
if (!viewport) { return result; }
const left = Math.max(0, rect.left - viewport.left);
const top = Math.max(0, rect.top - viewport.top);
const right = Math.min(viewport.width, rect.left + rect.width - viewport.left);
const bottom = Math.min(viewport.height, rect.top + rect.height - viewport.top);
// An off-window crease cannot split the application's content. Zero-width
// or zero-height fold lines remain valid if they lie inside the window.
if (left > right || top > bottom ||
(horizontal ? top <= 0 || bottom >= viewport.height : left <= 0 || right >= viewport.width)) {
return result;
}
result.bounds = { left, top, right, bottom };
result.isSeparating = status === 3 && (horizontal
? left === 0 && right === viewport.width
: top === 0 && bottom === viewport.height);
// A flexible OLED crease is not an opaque hinge, regardless of its thickness.
return result;
}
5.1.4 调用结束与实例销毁
Provider 自己注册布局和错误事件,并在卸载时移除两种事件订阅与释放引用计数。业务只读取 Hook,不额外重复启动原生监听。初始默认值不能证明系统已识别;需要诊断时使用示例应用的原生事件计数和错误区域。
5.2 声明插件和宿主权限
本库上述系统能力无需额外运行时权限,不应沿用其他传感器或定位库的权限清单。
5.2.1 插件 HAR 的权限
harmony/fold_detection/src/main/module.json5 声明 HAR 模块,本库没有额外 requestPermissions。能力是否存在仍需按系统和设备判断。
5.2.2 应用 entry 的权限
example/harmony/entry/src/main/module.json5 中的 ohos.permission.INTERNET 用于 RN 宿主的调试连接,不是此库读取或操作系统能力所必需的授权。
最终安装的是宿主应用。修改 module 配置时应合并现有字段,保留原有 Ability 等配置;权限声明和运行时授权是两个步骤。
5.3 注册并导出插件
ArkTS Package 注册如下,对应文件为 harmony/fold_detection/src/main/ets/RNFoldDetectionPackage.ets:
// SPDX-License-Identifier: MIT
import { RNOHPackage, UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony';
import { FoldingFeatureTurboModule } from './FoldingFeatureTurboModule';
export class RNFoldDetectionPackage extends RNOHPackage {
getUITurboModuleFactoryByNameMap(): Map<string, (ctx: UITurboModuleContext) => UITurboModule> {
return new Map<string, (ctx: UITurboModuleContext) => UITurboModule>([
[FoldingFeatureTurboModule.NAME,
(ctx: UITurboModuleContext): UITurboModule => new FoldingFeatureTurboModule(ctx)]
]);
}
}
C++ Package 对应文件为 harmony/fold_detection/src/main/cpp/RNFoldDetectionPackage.h:
// SPDX-License-Identifier: MIT
#pragma once
#include "RNOH/Package.h"
#include "FoldingFeature.h"
namespace rnoh {
class FoldDetectionTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
public:
SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override {
if (name == "FoldingFeature") { return std::make_shared<FoldingFeature>(ctx, name); }
return nullptr;
}
};
class RNFoldDetectionPackage : public Package {
public:
explicit RNFoldDetectionPackage(Package::Context ctx) : Package(ctx) {}
std::unique_ptr<TurboModuleFactoryDelegate> createTurboModuleFactoryDelegate() override {
return std::make_unique<FoldDetectionTurboModuleFactoryDelegate>();
}
};
} // namespace rnoh
两种 void 方法的手写映射如下:
// SPDX-License-Identifier: MIT
#include "FoldingFeature.h"
namespace rnoh {
FoldingFeature::FoldingFeature(const ArkTSTurboModule::Context ctx, const std::string name)
: ArkTSTurboModule(ctx, name) {
methodMap_ = {
ARK_METHOD_METADATA(startListening, 0),
ARK_METHOD_METADATA(stopListening, 0),
};
}
} // namespace rnoh
HAR 入口 harmony/fold_detection/index.ets 导出 Package:
// SPDX-License-Identifier: MIT
export * from './ts';
export { RNFoldDetectionPackage, RNFoldDetectionPackage as default } from './src/main/ets/RNFoldDetectionPackage';
package.json 中的自动链接元数据如下:
{
"harmony": {
"alias": "@logicwind/react-native-fold-detection",
"autolinking": {
"ohPackageName": "@react-native-ohos/react-native-fold-detection",
"etsPackageClassName": "RNFoldDetectionPackage",
"cppPackageClassName": "RNFoldDetectionPackage",
"cmakeLibraryTargetName": "rnoh_fold_detection"
}
}
}
宿主启用自动链接后,还必须调用生成的 ArkTS/C++ 工厂,并在 CMake 中包含 autolinking.cmake、执行 autolink_libraries(rnoh_app)。只生成文件不等于接入完成。自动和手动注册选择一种,避免重复注册同一个 Package。
5.4 检查 example 的 OHOS 应用结构
示例工程的 example/harmony/build-profile.json5 同时包含 entry 和库模块。下面只列出需要关注的配置,合并时保留其他模块:
{
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "fold_detection",
"srcPath": "../../harmony/fold_detection",
"targets": [
{
"name": "default"
}
]
}
]
}
配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:
| 文件 | 应写清楚的内容 |
|---|---|
| README.OpenSource | 上游名称、源码地址、基线版本与 MIT 许可 |
| README.md / README.upstream.md | 项目说明、上游信息与平台入口 |
| README.harmony.md | 鸿蒙安装、配套版本、权限、接口和限制 |
| VALIDATION.md | 测试、构建、设备场景及未覆盖项 |
| LICENSE | 保留上游许可证和版权 |
| package.json / index 类型入口 | 包名、版本、导出、构建脚本与发布文件;类型入口按实际 package.json |
| harmony/fold_detection/oh-package.json5 | OHPM 依赖、原生入口与版本 |
| harmony/fold_detection.har | 已构建的 release HAR,含消费者所需 C++ 源码 |
| .gitignore | 排除构建缓存和本机签名材料 |
README.OpenSource 记录库本身的来源与版本。本例上游为 @logicwind/react-native-fold-detection 0.3.2,交付包为 @react-native-ohos/react-native-fold-detection 0.3.2-0.0.1;RN 和 HarmonyOS SDK 版本写入环境说明。OAT.xml 的语法有效性与官方扫描结果应分别记录。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。
6.3 提交并推送到 AtomGit
文档和代码整理完成后,在自己的维护分支根目录暂存并提交。以下文件清单覆盖本库主要改动,新增其他文件时逐项补充:
git add package.json src/context/FoldingFeatureContext.tsx src/types/index.ts harmony scripts
git add README.md README.harmony.md README.OpenSource VALIDATION.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "docs: document OHOS support for react-native-fold-detection 0.3.2-0.0.1"
git remote -v
git branch --show-current
git push -u origin feat/ohos_fold_detection_0.3.2-0.0.1
DevEco 可能向 example/harmony/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。暂存 example 时逐文件核对;推送时,origin 应指向自己有权限的 AtomGit 仓库,当前分支为前面创建的维护分支。
推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
七、使用插件包内 example 演示接入
这一章站在使用者角度操作:先安装依赖,再让示例应用找到插件,最后点击页面按钮或执行调用。遇到问题时,先确认示例使用的是本章指定的包来源,避免把上游包和鸿蒙适配包混在一起。
仓库自带 example/,可以直接用来调试插件和体验折叠姿态检测。鸿蒙实际页面为 example/harmony/App.tsx,原生工程为 example/harmony。
7.1 本地适配时使用源码依赖
源码联调使用 example/harmony/metro.config.js 和 scripts/build-harmony.sh;不要改成 file:../,否则可能跳过仓库已有的 Harmony 配置。Metro 的项目根目录、watchFolders、别名和入口配置共同决定 JS 来源;示例 OHPM 指向库源码模块。只修改 JavaScript 后可以重新打包;原生代码变化还需重新构建。
按仓库文档,在插件根目录安装对应工具依赖:
npm install
cd example/harmony
npm install --workspaces=false
cd ../..
安装完成后,终端会回到命令提示符,并在项目中生成或更新 node_modules 与锁文件;如果出现红色错误,先解决依赖安装问题,不要直接进入构建步骤。
下面是本库的 Harmony Metro 配置。上游 Android/iOS 配置不能直接用于鸿蒙:
const path = require('path');
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const {createHarmonyMetroConfig} = require('@react-native-oh/react-native-harmony/metro.config');
const projectRoot = __dirname;
module.exports = mergeConfig(
getDefaultConfig(projectRoot),
createHarmonyMetroConfig({reactNativeHarmonyPackageName: '@react-native-oh/react-native-harmony'}),
{
projectRoot,
watchFolders: [path.resolve(__dirname, '../..')],
resolver: {
disableHierarchicalLookup: true,
nodeModulesPaths: [path.join(__dirname, 'node_modules')],
extraNodeModules: {'@logicwind/react-native-fold-detection': path.resolve(__dirname, '../..')},
},
transformer: {babelTransformerPath: require.resolve('./transformer')},
},
);
7.2 通过 AtomGit 源码构建并引入插件
当前没有可直接安装的 npm 版本。先按第三章从 AtomGit 获取固定提交,构建并打包,再在业务应用中安装生成的 tgz 文件。Git 仓库地址不能直接当作 npm 依赖使用。
以下命令在插件根目录执行;先完成 7.1 的依赖安装:
npm run build:har
mkdir -p artifacts
npm pack --pack-destination artifacts
产物名为 react-native-ohos-react-native-fold-detection-0.3.2-0.0.1.tgz。在采用 RN 0.84.1 / RNOH 0.84.3 的业务应用 JS 根目录执行,将绝对路径改为本机生成的文件位置:
把下一条命令中的 /absolute/path/to/...tgz 替换为你实际生成的 tgz 文件路径;示例路径不能直接照抄。
npm install /absolute/path/to/react-native-ohos-react-native-fold-detection-0.3.2-0.0.1.tgz
npx react-native link-harmony --harmony-project-path ./harmony
cd harmony
ohpm install
上述消费工程假设原生根目录为 harmony/。布局不同应调整 --harmony-project-path 和依赖路径。应用的 Harmony Metro 应使用 createHarmonyMetroConfig;包内 harmony.alias 支持上游导入名 @logicwind/react-native-fold-detection,TypeScript 的 paths 需要独立配置。下面最小页面直接使用适配包名,避免依赖 TypeScript 别名。
自动链接生成工厂后按 5.3 接入宿主。如果选择手动接入,ArkTS 注册 RNFoldDetectionPackage、C++ 注册 rnoh::RNFoldDetectionPackage,CMake 添加 HAR 内的 src/main/cpp 并链接 rnoh_fold_detection。完整手动工程见仓库 example/harmony/entry。
检查业务应用的依赖锁文件、实际 node_modules 和 OHPM 锁文件,确认 JS 与 HAR 来自同一版本,不要混用上游包的 JS 和另一个版本的原生实现。
7.3 调用接口实现折叠姿态检测
下面是已安装适配包的 RNOH 业务应用可使用的最小 App.tsx,保留本库的公开 API。
若要在仓库源码示例中使用,按 example/harmony/App.tsx 的相对位置与现有源码解析配置调整 import,保留注册入口。不要覆盖 Android/iOS 的历史示例入口。
import React, {useState} from 'react';
import {Button, ScrollView, Text} from 'react-native';
import {FoldingFeatureProvider, useFoldingFeature}
from '@react-native-ohos/react-native-fold-detection';
function FoldPanel() {
const {layoutInfo, isTableTop, isBook, isFlat} = useFoldingFeature();
return (
<>
<Text selectable>{JSON.stringify({layoutInfo, isTableTop, isBook, isFlat}, null, 2)}</Text>
<Text>初始值不等于已收到系统事件;没有 bounds 时不推断折痕位置。</Text>
</>
);
}
export default function App() {
const [mounted, setMounted] = useState(true);
return (
<ScrollView contentContainerStyle={{padding: 24, gap: 16}}>
<Button title="挂载 / 卸载 Provider" onPress={() => setMounted(value => !value)} />
{mounted ? <FoldingFeatureProvider><FoldPanel /></FoldingFeatureProvider>
: <Text>Provider 已卸载</Text>}
</ScrollView>
);
}
7.4 页面退出时处理资源
Provider 自己注册布局和错误事件,并在卸载时移除两种事件订阅与释放引用计数。业务只读取 Hook,不额外重复启动原生监听。初始默认值不能证明系统已识别;需要诊断时使用示例应用的原生事件计数和错误区域。
八、验证、构建与鸿蒙设备运行效果
建议按“工具链—设备—构建产物—页面功能”的顺序排查。先确认命令能运行,再确认 hdc 能看到设备,然后检查是否生成 HAP,最后才判断插件功能;这样可以快速分辨是环境问题、安装问题还是接口问题。 文中的 <device-id> 是占位符,请用 hdc list targets 输出的真实设备 ID 替换。
8.1 分别验证插件与 example
以下命令作为复现步骤,在已完成 7.1 安装的插件仓库根目录执行:
npm test -- --runInBand
npm run typecheck
npm run build
npm run build:har
npm run build:example
npm run verify:integration
verify:integration 会创建一个临时应用,检查 npm 包、HAR、自动链接和打包能否一起工作。命令运行前先准备好应用的签名配置;如果使用过临时工程,后续直接按该工程的构建流程继续即可。
测试覆盖快照、多 Provider、旋转映射、窗口裁剪、无折叠能力、双折轴回退、异步竞态、SDK 异常和空折痕等情况。应用接入时还要按第八章步骤确认源码示例和打包结果。
源码测试覆盖接口和业务逻辑;系统能力与设备行为需要在鸿蒙环境中验证。
8.2 确认设备连接
hdc list targets
设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。
8.3 配置签名
真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置签名:
- 用 DevEco Studio 打开
example/harmony,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名、证书和 Profile 匹配;
- 再回到终端执行构建,或在 DevEco 运行 entry。
签名材料保存在本机,公开仓库中只保留构建所需的通用配置。HAR 不需要应用签名;要验收最终交付 HAR,应使用独立 npm 消费宿主,而不是以源码宿主代替。
8.4 运行示例
从插件根目录构建源码示例:
npm run build:example
本库脚本生成离线生产 JS bundle,并构建默认 debug HAP,不要求另开 Metro 服务。典型产物位于:
example/harmony/entry/build/default/outputs/default/
未签名文件 entry-default-unsigned.hap 仅能说明打包完成。配置有效签名并生成 entry-default-signed.hap 后,可在仓库根目录安装:
hdc -t <device-id> install -r example/harmony/entry/build/default/outputs/default/entry-default-signed.hap
将 <device-id> 替换为设备列表中的实际 ID;安装后从设备应用图标启动,或在 DevEco 直接运行 entry。构建工具默认来自 macOS DevEco Studio,其他位置使用脚本支持的 DEVECO_ROOT、DEVECO_SDK_HOME、OHPM_BIN、HVIGOR_JS 配置。
8.5 在设备上测试核心操作
- 检查示例应用的 Native bridge、原生事件与 Provider 数据。
- 切换展开、半折叠、闭合,对照系统状态及上游映射。
- 停止监听后切换状态,检查事件计数保持,再启动获取快照。
- 卸载重挂 Provider、前后台与冷启动;只有 SDK 返回真实折痕时才验收 bounds、方向和分隔。
页面默认值或“已请求”文本不等于系统操作完成。查询要检查结果来源,监听要观察真实事件,窗口操作要观察实际系统显示。
8.6 鸿蒙设备运行效果
示例覆盖展开、半折叠和闭合状态,Provider 返回值与原生模块保持一致;重复启动、停止后切换、回到前台和重新挂载等生命周期场景也有对应处理。

模拟器旧折痕接口返回 undefined,新接口返回空 creaseRects,当前正确省略 bounds。半折叠时 isBook=true 来自默认 VERTICAL,不能证明真实铰链方向。实际 bounds、tabletop、分隔、实体折叠屏和屏幕旋转仍未验收;修正版本非折叠真机完整回归也未完成。
九、FAQ:适配过程与使用问题
9.1 Missing SDK components
这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
- 检查构建脚本与 DevEco 使用的 SDK 路径是否一致;
- 根据实际安装位置设置
DEVECO_SDK_HOME或其他工具覆盖变量; - 确认 SDK 包中工具链、ETS、JS 和 native 组件可用;
- 重新执行 DevEco Studio Sync,并查看脚本输出的构建日志。
因为 Sync 和 Compile 首先读取 Mac 本地 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。
当前工程的 compatibleSdkVersion 是 API 17,因此 API 24 在安装版本门槛上是满足的;但设备还必须支持本库使用的能力,并满足签名要求。
9.2 DevEco Studio 中看不到 entry 模块
插件的 harmony/fold_detection 目录是 HAR 模块,可安装应用的 entry 模块位于 example/harmony/entry。
请直接使用 DevEco Studio 打开:
react-native-fold-detection/example/harmony
如果仍看不到 entry,先解决 SDK Sync 错误,再检查应用 build-profile 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。
9.3 无法手动签名
签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。
建议先确认:
- 打开的是
example/harmony; - SDK 组件完整并且 Sync 成功;
entry的模块类型为entry;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效。
9.4 isFlat=true 是否表示屏幕完全展开
不是。它由“不处于书本或桌面姿态”派生,上游模型无法独立表示闭合和未知。因此闭合或不支持也可能得到 true。
9.5 找不到原生模块 FoldingFeature
这通常表示 JS 入口找不到已注册的原生模块。新增原生插件后需要重新构建应用;检查 npm 适配包、OHPM HAR、ArkTS Package、C++ Package 和 CMake 链接,特别是自动工厂是否真正由宿主调用。不要用一个固定返回值或开发刷新替代原生接入。
9.6 重复操作或页面退出后的行为
Provider 自己注册布局和错误事件,并在卸载时移除两种事件订阅与释放引用计数。业务只读取 Hook,不额外重复启动原生监听。初始默认值不能证明系统已识别;需要诊断时使用示例应用的原生事件计数和错误区域。
9.7 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
根据安装错误码区分签名、版本和包名冲突,再处理对应配置。
9.8 RN CLI 或 Metro 使用了错误入口
在对应依赖目录确认安装的是本文配套 RN/RNOH 和 Harmony CLI。使用 example/harmony/metro.config.js,检查包的 react-native/main 入口与 Harmony 平台选择;Metro 别名不会自动给 TypeScript 提供 paths。调试时不要同时加载上游包和适配包的不同实现。
9.9 AtomGit 依赖提示找不到分支或无权限
先检查 URL 是否指向已同步的目标仓库,再确认需要的分支已推送。本文已固定到 4d7627331b00f1a7d3d00c6b5eb2b147ec6639d0;自己新建的 feat/ohos_fold_detection_0.3.2-0.0.1 不会自动存在于远端。私有 Fork 还需在本机配置 Git 认证。
9.10 改了本地 ArkTS,Demo 为什么没变化
先检查应用消费的是源码模块还是打包的 HAR:已安装的 tgz 不会自动读取工作区修改。本地源码联调按 7.1 的配置,测试交付包则重建 HAR、重新打包并更新宿主依赖与 OHPM 锁文件。原生代码变动后停止应用并重新构建运行,不能只做 JS Fast Refresh。
9.11 半折叠有状态但没有 bounds
当前模拟器没有返回有效折痕。实现保留系统状态、省略可选坐标;VERTICAL 可能是默认值,不能拿 isBook=true 证明实体书本姿态。
相关链接
更多推荐




所有评论(0)