React Native for OpenHarmony 三方库 expo-screen-orientation 57.0.2 适配实战:四向锁定、策略映射与事件清理

屏幕方向适配最容易被“手机转了”这件事骗过。窗口策略、系统实际方向和事件回调使用的是三套枚举。本篇面向新手,从这些概念开始,逐步说明如何改源码、构建 HAR、接入宿主并在真机恢复用户原设置。

适配仓库: oh-react-native/expo-screen-orientation
交付分支: main
适配 TAG: 57.0.2-ohos-1.0.0
受测提交: 20846fea3cd1d8e74c54ff55466c1961b496204b
上游基线: c300d2cc60c9e684e64f48d9bc90ea18a571d01d(对应 57.0.2 发布源码)
配套源码: expo-screen-orientation

一、这个库是做什么的

expo-screen-orientation 用来控制和观察应用窗口方向。阅读器可以锁定竖屏,横向表格可以锁定横屏,游戏可以读取当前方向并在方向变化时调整布局。公开 API 包括 lockAsync、unlockAsync、getOrientationAsync、getOrientationLockAsync、getPlatformOrientationLockAsync、supportsOrientationLockAsync、addOrientationChangeListener、removeOrientationChangeListener 和 removeOrientationChangeListeners。

新手常见的误解是“把 PORTRAIT 翻译成系统的 1,把 LANDSCAPE 翻译成系统的 2”就完成了。实际上这里至少有三套值:第一套是应用请求的窗口策略,例如 PORTRAIT_UP;第二套是显示对象当前的实际方向;第三套是系统 RotationChangeInfo 事件里的旋转编码。它们的数值和命名都不能凭直觉共用。此次适配最大的工作量不是写一个 setPreferredOrientation,而是把这三套语义分别验证清楚。

适配前在 oh-react-native 组织、CPF-RN 组织 和活动中心清单中按包名、去 scope 名和 rntpc 前缀去重,没有发现同上游的有效适配。上游 57.0.2 为 MIT,最终仓库只保留该库自己的源码、HAR 和说明。

初始方向和能力

图 1:测试开始时为正向竖屏,窗口原策略为 0;八种通用策略能力检查通过,原策略先保存,结束后恢复。

二、环境和交付身份

受测组合为 React Native 0.84.1、React 19.2.3、RNOH 0.84.3、DevEco Studio 26.0.0 Release、HarmonyOS SDK 26.0.0,ROM OpenHarmony-7.0.0.105。受测源码锁定在文首提交和 TAG;文章中的截图来自同一精简包和签名 HAP,不是后来改过代码的旧截图。

仓库保留 src/index.ts、ExpoScreenOrientationTurboModule.ts、Spec、harmony/screen_orientation/Index.ets、ExpoScreenOrientationPackage.cpp、ExpoScreenOrientationPackage.ets、CMake、HAR、双语 README、测试、spec.json 和许可证。完整宿主和测试探针属于验证工程,不作为三方库的一部分上传。

三、从 JS 到窗口系统的调用链

React 页面
  -> expo-screen-orientation 的 JS API
  -> ExpoScreenOrientationTurboModule Spec
  -> C++ Package methodMap_
  -> ArkTS TurboModule
  -> UIAbility 主窗口
  -> Window.setPreferredOrientationWithResult / getPreferredOrientation

锁定接口使用 Promise,因为系统设置可能返回 ACCEPT、PENDING 或 IGNORED。查询接口也保留 Promise,监听接口返回订阅对象。鸿蒙专属字段 screenOrientationConstantHarmony 用于记录窗口策略原值,不能把 Android、iOS 和 Web 的枚举数字混到同一个比较里。

本次锁映射为:DEFAULT 使用系统未指定策略;PORTRAIT_UP 和 PORTRAIT_DOWN 分别对应正向、倒置竖屏;LANDSCAPE_LEFT 对应窗口 LANDSCAPE_INVERTED;LANDSCAPE_RIGHT 对应窗口 LANDSCAPE。固定左右横屏的名字与显示对象方向恰好不是同一顺序,这是后面真机探针发现的关键差异。

四、为什么不能照搬方向枚举

第一版按 display.Orientation 的名字直接映射左右横屏,导致 getter 与旋转事件不一致。最终让独立 ArkTS 探针绕过被测库,对四种窗口策略逐一设置并读回:

窗口策略display.orientation公开 Orientation
PORTRAIT,10PORTRAIT_UP,1
PORTRAIT_INVERTED,32PORTRAIT_DOWN,2
LANDSCAPE,23LANDSCAPE_RIGHT,4
LANDSCAPE_INVERTED,41LANDSCAPE_LEFT,3

监听又采用 RotationChangeInfo 的实际编码,并且只在横竖类别变化时派发。也就是说,竖屏正向转倒置竖屏可能改变 getter,却不一定产生一次业务方向类别事件;同类 180 度旋转没有被错误地重复通知。

正向和倒置竖屏

图 2:固定竖屏与倒置竖屏分别读回公开方向 1 和 2,两个监听已订阅。

左右横屏和事件

图 3:转到左横屏时收到方向 3、锁 6;再转到右横屏,getter 为 4,而事件仍遵守“只跨横竖类别通知”的契约。

五、具体源码修改

  1. src/index.ts 保留 Expo 原 API,增加 HarmonyOS 平台选择和鸿蒙专属策略字段。
  2. ExpoScreenOrientationTurboModule.ts 把每个公开函数转到同名原生方法,统一 Promise、参数校验和错误传播。
  3. harmony/screen_orientation/Index.ets 和 ExpoScreenOrientationPackage.ets 注册 Package 工厂,模块名与 C++ methodMap_ 一致。
  4. ArkTS 通过当前 RN 所在 UIAbility 的主窗口设置策略、查询策略和实际方向;不使用缺失的 TYPE_MAIN 枚举,而是比较真实 window ID。
  5. 监听首订阅启动原生观察器,最后一个订阅移除时取消;每轮订阅有世代标识,旧回调不能污染新订阅。
  6. 销毁或恢复时先查询当前策略;如果目标策略已经生效就视为幂等完成,不把系统返回 IGNORED 的同值请求误报为失败。

恢复逻辑可以概括为:

const before = target.getPreferredOrientation();
if (before === policy) {
  return;
}
const result = await target.setPreferredOrientationWithResult(policy);
if (result.executionResult === ORIENTATION_IGNORED) {
  throw new Error('ERR_SCREEN_ORIENTATION_IGNORED');
}

只有同值已生效时才提前返回;不同策略被系统忽略仍然拒绝 Promise,调用者可以重试或提示用户。

六、用最终 tgz 接入宿主

受测使用与文首 TAG 对应的 expo-screen-orientation-57.0.2.tgz。把 tgz 放入独立宿主后按顺序执行:

export RNOH_HOST="$HOME/rnoh-qa"
export EVIDENCE_DIR="$RNOH_HOST/evidence/expo-screen-orientation"
mkdir -p "$EVIDENCE_DIR"
cd "$RNOH_HOST"
npm install "$HOME/Downloads/expo-screen-orientation-57.0.2.tgz" --save-exact
./node_modules/.bin/react-native link-harmony
cd harmony
ohpm install --all
cd ..
./node_modules/.bin/react-native bundle-harmony --dev false --sourcemap-output "$EVIDENCE_DIR/bundle.map"
hvigorw assembleHap --mode module -p product=default --no-daemon

等待 npm 完成后使用宿主本地 CLI,检查 bundle source map 来自安装包。–dev false 是 JS bundle 选项,不等于 release HAP;本轮使用签名 debug HAP。只有从 tgz 重新构建并安装,后面的真机截图才和文章中的提交、TAG 有关联。

真机操作前先确认目标设备并设置临时常亮:

hdc list targets
export DEVICE_ID="$(hdc list targets | awk 'NF {print $1; exit}')"
hdc -t "$DEVICE_ID" install "$RNOH_HOST/output/tested.hap"
hdc -t "$DEVICE_ID" shell power-shell wakeup
hdc -t "$DEVICE_ID" shell power-shell timeout -o 2147483647
hdc -t "$DEVICE_ID" shell aa start -a EntryAbility -b com.example.rnqa
hdc -t "$DEVICE_ID" shell uitest screenCap -p /data/local/tmp/orientation.png
hdc -t "$DEVICE_ID" file recv /data/local/tmp/orientation.png "$EVIDENCE_DIR/orientation.png"

结束后恢复原超时,并确认最终策略回到验证前保存的值;不能因为截图完成就跳过恢复。

七、业务侧调用和监听

import * as ScreenOrientation from 'expo-screen-orientation';

const before = await ScreenOrientation.getOrientationLockAsync();
try {
  await ScreenOrientation.lockAsync(
    ScreenOrientation.OrientationLock.PORTRAIT_UP
  );
  const actual = await ScreenOrientation.getOrientationAsync();
  console.log('实际方向', actual);
} finally {
  await ScreenOrientation.unlockAsync();
  // 真实业务可在这里恢复 before 对应的策略
}

const subscription = ScreenOrientation.addOrientationChangeListener(event => {
  console.log(event.orientationInfo.orientation);
});
// 组件卸载时调用
ScreenOrientation.removeOrientationChangeListener(subscription);

锁定完成只表示系统接受策略,不表示旋转动画已经结束。需要读取实际方向并等待页面布局稳定,再更新业务状态。事件监听要在组件卸载时删除,不能只把回调置空,否则原生观察器仍可能持有上下文。

平台策略读回

图 4:通用传感器、受限策略、当前方向锁定和默认策略均由系统读回;鸿蒙没有通用等价项的策略返回 OTHER。

八、真机验证过程

验证开始先保存原策略 0。随后依次测试四种方向锁、十种系统策略查询、能力检查、单个监听移除、全部监听移除、重新订阅和八种非法参数。每次 lock 后都等待系统读回,不把 Promise resolve 当作屏幕已经转完。结束时 unlock,再把原策略恢复;如果原策略本来就是 0,第二次设置 0 会被识别为同值幂等,而不是失败。

移除和重订阅

图 5:监听 A 重复移除后,B 仍收到变化;全部移除后原生观察器没有继续派发;重新订阅又能收到新的类别变化。

非法参数和恢复

图 6:八种非法请求被拒绝,OTHER 无操作,unlock 恢复默认,最后由独立入口核对原策略。

本轮还进行了两次不同进程的资源 bundle 冷启动。mock 覆盖系统拒绝、PENDING、缺少传感器和模块销毁;真机覆盖四向映射、读回、事件生命周期、恢复和冷启动。发布文章只保留仓库和官方文档链接;读者复现时应以自己的设备截图、日志、HAP 和包哈希为准。

九、踩坑、限制和结论

窗口主类型是一个版本差异点。SDK 声明 TYPE_MAIN,但本机对象没有该属性,实际 windowType 为 32;最终用 RN 窗口和 Stage 主窗口的真实 ID 判断,避免依赖缺失枚举。另一个问题是 unlock 后再次设置同一个默认策略时系统返回 IGNORED,这不是业务失败,只有目标策略与当前不同且被忽略才应拒绝。

本次没有人工物理转动手机或切换用户旋转开关,也没有覆盖外接屏、自由窗口、平板和其他 ROM。方向事件只承诺本次实现的横竖类别变化语义,不承诺每次 180 度姿态变化都触发回调。应用应在页面布局层同时使用实际方向查询和监听结果。

十、参考链接

欢迎加入 RN for OpenHarmony 社区。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐