本文记录把react-native-loading-spinner-overlay适配到 HarmonyOS 的完整过程。
这个库很小(两个源文件、约 3 KB),也很纯粹:一个全屏的加载遮罩,零运行时依赖、没有prepare脚本、连编译产物都与上游逐字符一致。接入本身毫无难度。
但它带来的验证问题比接入问题有意思得多:
- 它的所有渲染都在 RN 的
Modal里(transparent+statusBarTranslucent)⇒ 要测的是RNOH 的 Modal + 半透明遮罩; - 遮罩一起,它就把页面上所有触摸都吞掉了—— 这让我原本设计的"遮罩显示期间点按钮触发重渲染"的实验根本无法执行;
- 它是半受控的:
cancelable被返回键关闭后,组件内部认为已关闭、父组件的visible却还是true,而库没有任何回调告知父组件。
第 3 条是这轮最有价值的发现:它是一个真实的行为缺陷,而且只靠读源码 + 一个四步实测就能坐实。
一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 3.0.1(MIT);gitHead=84896fa0284b705f136622dcd0d7f17184f747bb |
| 库类型 | 纯 JS 函数组件,渲染在 RN 的Modal内 |
| 是否需要原生适配 | 不需要(无harmony/、无NativeModules) |
| 运行时依赖 | 零(dependencies字段不存在) |
prepare/prepack | 都没有⇒ 安装路径干净 |
| 与 npm 上游的差异 | 11 / 12 个文件去掉 CR 后逐字符一致(含src/**与lib/**编译产物);唯一差异是package.json |
| 编译 | assembleHap3 分 52 秒;HAP82,083,102 字节(+32,770) |
| 设备侧断言 | 12 / 14(A 10/10、B 1/1、C 1/1、D 0/2 未验证,原因见第八节) |
| ★ 外部实测(全部命中) | 遮罩文字 Y =1281(几何预言逐像素吻合);rgba(0,0,0,0.25)实测亮度×0.75;rgba(255,0,0,1)全屏(254,0,0)(含状态栏区域);遮罩吞掉全部页面触摸(有对照组) |
| ★ 一个真实的行为缺陷 | 半受控:cancelable关闭后组件(false) 与父组件 prop(true)长期不一致,且库不接受onRequestClose、无任何回调 |
| ★ 未验证的一条 | 不传spinnerKey时"每次渲染都重挂载 Modal" ——代码级事实成立,运行时挂载计数本轮没取到(原因正是第 5 节那条) |
一句话结论:库本身能用、几何与像素都对;但要记住它会吞掉页面所有交互,以及它的关闭是"半受控"的。
二、判定过程:三步走完,而且是这一系列里最干净的一个
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json里有没有harmony.autolinking | 没有 |
| ② | 仓库里有没有harmony/ | 没有 |
| ③ | 代码里有没有NativeModules/requireNativeComponent | 全无 |
dependencies字段直接不存在 ⇒零运行时依赖。
再补两项这个库特有的确认(这两项在前面的库里都出过事):
| 检查 | 结果 | 为什么值得单独查 |
|---|---|---|
scripts里有没有prepare/prepack | 都没有(只有build/lint/test) | 有的库在prepare里会删掉随包发布的编译产物,导致安装失败、包静默损坏 |
| 入口在哪 | main: lib/index.js,lib/已提交 | 这类"入口是编译产物"的包要确认lib/与src/行为一致(否则你读的源码不是真正跑的东西) |
关于第二项的核实方式:这个包的tsconfig.json写的是"target": "esnext"且没有写module⇒ 编译产物是ESM(import/export default)。这意味着在 PC 上用 Noderequire()它是不行的,所以我没有做 PC 侧探针,而是直接读lib/index.js这份运行时真正执行的代码,并逐行核对它与src/index.tsx行为一致 ⇒排除"陈旧编译产物"这个风险。
三、交付包质量
3.1 与 npm 上游逐文件比对(先去 CR)
| 类别 | 结果 |
|---|---|
| 一致 | 11 个文件(src/**、lib/**全部编译产物、README.md、LICENSE、tsconfig.json等) |
| 不同 | 1 个:package.json |
| 交付包缺少 | 0 |
| 交付包多出 | 6 个:.gitignore、两个README.OpenHarmony*、代码检查报告、spec.json、契约测试 |
实现与产物双重零改动,而且没有prepare⇒ 这是本系列里安装路径最干净的一个。
3.2 一个"看着像错、查证后没错"的地方
spec.json里的上游地址写的是git+ssh://git@github.com/...(SSH 形式)。第一眼像是不该这么写 —— 但查 npm 的repository字段,上游自己就是这么写的⇒不是缺陷。
教训:"名字/格式不一致"要先查数据源再定性。
3.3 问题
| 问题 | 说明 |
|---|---|
spec.json缺upstreamCommit | 而 npm明确有gitHead(84896fa0…)⇒属应修项 |
validation是裸字符串"pass" | 没有可复核的测试项清单 |
| 契约测试是包级 smoke test | 只检查包名 / 版本 / 主入口存在 / 中文 README 存在,一次组件都没渲染过 |
| 两个交付 README都没写三件事 | ① 组件是半受控的;②遮罩会吞掉页面所有触摸;③ 不传spinnerKey会重挂载 |
四、几何与像素:三条预言全部命中
4.1 遮罩文字的位置(逐像素吻合)
源码里textContent的样式是:
textContent:{fontSize:20,fontWeight:'bold',height:50,top:80}而它的容器是全屏绝对定位 +justifyContent: 'center'。
先算一遍(设备 density = 3,屏高 2232px):
文字上边 = 屏幕中心 − 文字半高 + top = 2232/2 − (50vp/2 × 3) + (80vp × 3) = 1116 − 75 + 240 = 1281实测:Text [433,1281,887,1431] "SPIN-TEXT-DIM"⇒Y1 = 1281✓ 高度 150px 也正好是50vp × 3✓
⇒这个库的布局在 RNOH 上与设计完全一致。
4.2 半透明遮罩的"变暗比例"(数值精确)
overlayColor默认是'rgba(0, 0, 0, 0.25)',作用在全屏容器上。在同一个像素位置比较"无遮罩 / 有遮罩":
| 位置(下面的页面元素) | 无遮罩 | 有遮罩 | 比值 |
|---|---|---|---|
(100,1700) 蓝色按钮#1f6feb | (31,111,234) | (23,84,177) | 0.742 / 0.757 / 0.756 |
(660,1700) 页面背景#0f1115 | (16,17,21) | (12,13,17) | 0.750 / 0.765 / 0.810 |
⇒实测比值 ≈ 0.75 = 1 − 0.25✓ 遮罩确实是"整体压暗 25%",而不是别的混合方式。
4.3 纯红遮罩顺带证实了statusBarTranslucent
把overlayColor换成'rgba(255, 0, 0, 1)':
| 采样点 | 实测 |
|---|---|
| (100,300) / (660,900) / (1200,2100) | (254,0,0) |
| (60,60) —— 状态栏区域 | (254,0,0) |
遮罩全屏铺满,连状态栏区域都被盖住—— 这正好证实了源码里的:
<Modal … statusBarTranslucent={true}>是真的生效的。
五、遮罩会吞掉页面所有触摸(带对照组)
这一条我先踩坑、后做对照才定性。
现象:遮罩显示后,用工具按坐标点页面上的按钮,工具报成功,但设备日志里没有任何对应的记录,遮罩也没关。
对照实验:
| 情形 | 结果 | 是否产生日志 |
|---|---|---|
| 无遮罩时点「显示 纯红遮罩」 | 红遮罩出现 | ✅ 有 |
| 遮罩在时点「隐藏全部遮罩」 | 无任何日志,遮罩仍在 | ❌ 无 |
⇒Modal窗口把页面所有触摸都吞掉了。
两个结论:
- 遮罩显示期间,用户点不到下面任何东西(这是 loading 遮罩的意图,但写代码时要意识到:"加载中禁止操作"是自动的,不需要额外加
pointerEvents); - ⚠️方法论:坐标点击工具报成功只代表"坐标被点到了",不代表按钮被按下。有东西盖着时它照样报成功。以后凡是"点了没反应",都要先做一次"无遮挡时点同一个按钮"的对照。
六、cancelable的真实触发源是返回键,不是点遮罩
源码里:
constclose=()=>{setSpinnerVisibility(false);};const_handleOnRequestClose=()=>{if(cancelable){close();}};<Modal onRequestClose={()=>{_handleOnRequestClose();}}…>两个要点:
- 组件根本不接受
onRequestCloseprop—— 它内部只有一个私有的_handleOnRequestClose,从外面传什么都不起作用; - RN 的
Modal.onRequestClose由返回键 / 返回手势触发,不是点遮罩触发⇒cancelable必须用返回键来验。
实测:
| 实例 | 按返回键后 | 结论 |
|---|---|---|
cancelable未设(= false) | 遮罩仍在 | ✓ 与if (cancelable)一致;且返回键没有把应用退出(被 Modal 消费了) |
cancelable= true | 遮罩关闭 | ✓cancelable经Modal.onRequestClose生效 |
⚠️别把
cancelable理解成"点遮罩可以关闭"。它的实际含义是"允许返回键关闭"。
七、半受控语义:本轮最有价值的发现
7.1 源码
const[spinnerVisible,setSpinnerVisibility]=React.useState(visible);// ① 内部自己存一份React.useEffect(()=>{setSpinnerVisibility(visible);},[visible]);// ② 只在 visible 变化时同步constclose=()=>{setSpinnerVisibility(false);};// ③ 关闭只改内部状态⇒cancelable被返回键关闭时:只改了组件内部状态,没有通知父组件;而useEffect的依赖[visible]没变⇒不会重新同步。
7.2 四步实测(全部命中)
| 步骤 | 观测 | 结论 |
|---|---|---|
| ① 点「显示 cancelable」 | 遮罩出现(文字 Y = 1281) | 打开正常 |
| ② 按返回键 | 遮罩关闭;日志里没有父组件的新动作 | 父组件 prop 仍是true |
③ 让父组件用同样的visible={true}重渲染 3 次 | 遮罩没有重新出现 | ✅边界坐实:组件(false) 与父组件(true) 长期不一致 |
④ 把visible走一次 false→true | 遮罩重新出现 | ✅ 与[visible]依赖一致 |
7.3 为什么这是个缺陷
- 库不提供任何回调告知父组件"我被关掉了"(它连
onRequestCloseprop 都不接受); - ⇒ 父组件无从得知。如果它一直保持
visible={true},就会长期处于"父组件以为在显示、实际已经关了"的不一致状态; - 而且遮罩不会自己回来,除非父组件主动把
visible走一次 false→true。
规避做法:
// 父组件自己掌握状态;要用 cancelable 就自己监听返回键 const [visible, setVisible] = useState(false); useEffect(() => { const sub = BackHandler.addEventListener('hardwareBackPress', () => { if (visible) { setVisible(false); return true; } // 自己关,自己知道 return false; }); return () => sub.remove(); }, [visible]); <Spinner visible={visible} cancelable={false} textContent="加载中" />;八、一条"代码级成立、运行时未验证"的行为
8.1 代码级事实
<View style={[styles.container, …]} key={spinnerKey || `spinner_${Date.now()}`}>Date.now()每次渲染都不同⇒ 不传spinnerKey时这个key每次渲染都变⇒ 按 React 语义,该子树会被卸载再重新挂载。这解释了spinnerKey这个 prop 为什么存在:想避免重挂载就得给一个稳定的 key。
8.2 为什么本轮没测到
我原本的设计是:传一个挂载时会打日志的customIndicator,然后在遮罩显示期间点"重渲染"按钮,数挂载次数。
但这个设计被第 5 节推翻了:遮罩一起就吞掉所有触摸 ⇒我没法在遮罩显示期间点到页面按钮⇒ 父组件不会重渲染 ⇒挂载计数拿不到。
所以这一轮的 D 组两条断言如实失败:
✗ [D] 不传 spinnerKey 的实例挂载次数 > 1(每次渲染都重挂载)|期望 true|实际 false ✗ [D] 固定 spinnerKey 的实例挂载次数更少|期望 true|实际 false · NO-KEY 挂载次数 = 0 FIXED-KEY 挂载次数 = 0补测的正确做法:在页面里加一个自驱动计时器(遮罩显示期间自动触发父组件重渲染),这样就不依赖外部点击 —— 但这需要再改一次页面并重编。本轮为控制时长没有做。
⇒这一条只能记成"代码级事实成立、运行时未验证",不能写成"已实测"。
九、已知限制与本次验证边界
9.1 用这个库要知道的四件事
| 事项 | 说明 |
|---|---|
| 安装 | 无prepare,普通npm install即可(本系列仍统一加--ignore-scripts) |
| 遮罩会吞掉页面所有触摸 | 显示期间下面的按钮全点不到;也点不到遮罩来关闭 |
cancelable= 允许返回键关闭 | 不是"点遮罩关闭";而且不会通知父组件 |
| 半受控 | 组件自己关掉后,父组件的visible=true不会让它重开;要 false→true |
spinnerKey | 不给的话每次渲染都会重挂载 Modal 子树(代码级;运行时未验) |
不接受onRequestCloseprop | 想在关闭时做事情,只能自己在父组件监听返回键 |
9.2 本次验证的边界
- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机。 - 重挂载行为(D 组)未在运行时验证(原因见第八节)。
animation('none'/'slide'/'fade')完全未测—— 本轮只用默认的'none';Modal 动画在 RNOH 上的表现未知。size='small'与数值型size未测(只用了默认'large');源码注释说不支持'normal',但没实测传'normal'会怎样。color(指示器颜色)未做像素核对(本轮只核对了遮罩色)。indicatorStyle/textStyle自定义未测。customIndicator/children自定义内容只用于挂载计数,没有渲染可测内容的布局/交互。- 多个 Spinner 同时
visible的叠加/层级未测(本轮六个实例每次只显示一个)。 children与customIndicator同时存在时的优先级未做组合实测(源码是children || _renderDefaultContent()、customIndicator || ActivityIndicator)。visible从 true→false 的关闭时序/动画未测。- 点遮罩本身是否有其它副作用未细测(已确认"不会关闭")。
- 没有跨平台对照(未在 iOS/Android 上跑同一套用例)。
未改动库代码。
十、常见问题
Q1:怎么接入?
npminstall"git+https://atomgit.com/oh-react-native/react-native-loading-spinner-overlay.git#3.0.1-ohos-1.0.0"--ignore-scripts// metro.config.js —— file: / 软链装法需要把真实目录加进 watchFolderswatchFolders:[path.resolve(__dirname,'../react-native-loading-spinner-overlay')],import Spinner from 'react-native-loading-spinner-overlay'; <Spinner visible={loading} textContent="加载中…" />;不需要HAR、权限、ohpm、link-harmony。
Q2:遮罩显示时,我还能操作页面上的按钮吗?
不能。它渲染在一个全屏的Modal窗口里,会吞掉页面所有触摸(实测:有遮罩时点页面按钮,设备日志里没有任何反应)。
这通常正是你要的效果(加载中禁止操作),但要知道:这个"禁止操作"是自动发生的,你不需要另外加pointerEvents;反过来说,也别指望"点一下遮罩就能关掉它"。
Q3:cancelable是不是"点遮罩可以关闭"?
不是。cancelable的真实含义是"允许返回键关闭":
const_handleOnRequestClose=()=>{if(cancelable){close();}};<Modal onRequestClose={()=>_handleOnRequestClose()}…>Modal.onRequestClose由返回键/返回手势触发,点遮罩不会触发。实测:cancelable=false时按返回键不关;cancelable=true时按返回键关闭。
Q4:我想在遮罩被关闭时做点事情,可以传onRequestClose吗?
不行,这个组件根本不接受这个 prop —— 它内部只有一个私有的_handleOnRequestClose。
Q5:我传了visible={true},用户按返回键关掉之后,为什么它不再出现了?
因为它内部自己存了一份visible状态,cancelable关闭时只改了内部状态:
const[spinnerVisible,setSpinnerVisibility]=useState(visible);useEffect(()=>setSpinnerVisibility(visible),[visible]);useEffect的依赖是[visible],而父组件传的值一直是true(没变)⇒不会重新同步⇒ 遮罩保持关闭。实测已坐实:父组件用同样的visible={true}重渲染 3 次,遮罩也不会回来;只有把visible走一次false→true才会重新出现。
Q6:那正确的用法是什么?
父组件自己掌握状态,并且自己监听返回键(不要依赖库的内部行为):
const [visible, setVisible] = useState(false); useEffect(() => { const sub = BackHandler.addEventListener('hardwareBackPress', () => { if (visible) { setVisible(false); return true; } return false; }); return () => sub.remove(); }, [visible]); <Spinner visible={visible} cancelable={false} textContent="加载中…" />;Q7:spinnerKey是干什么的?
源码里那一行是:
key={spinnerKey || `spinner_${Date.now()}`}不传spinnerKey时,key每次渲染都不一样⇒ 按 React 语义,这个子树每次渲染都会被卸载再重新挂载(造成不必要的开销/闪烁)。给它一个固定字符串就能避免。
(⚠️ 这条是代码级事实;本轮的运行时挂载计数没能取到,原因见第八节,所以我没有把它写成"已实测"。)
Q8:遮罩的默认样式能改吗?
可以。overlayColor改全屏底色、color改指示器颜色、size改指示器大小、textContent/textStyle改文字、indicatorStyle改指示器与文字容器的样式。
实测:overlayColor默认'rgba(0, 0, 0, 0.25)'会把屏幕整体压暗到 75% 亮度;传'rgba(255, 0, 0, 1)'则整屏变纯红,连状态栏区域也盖住(因为statusBarTranslucent为 true)。
Q9:size能传'normal'吗?
源码注释明确写着size不支持'normal',类型是'small' | 'large' | number。(本轮只实测了默认的'large',传'normal'的实际表现未验证。)
Q10:遮罩打开后我卡住了,怎么把它弄掉?
cancelable=true的实例:按返回键;cancelable=false的实例:返回键关不掉(这是设计行为),页面按钮又被吞了 ⇒ 只能重启应用(aa force-stop+ 重新aa start)。
这是设计测试页时就要想好的事:有覆盖层的组件,每种状态都要有"退出来"的路,否则后续步骤会被卡死。
小结
react-native-loading-spinner-overlay是一个很小、很干净的纯 JS 库:零运行时依赖、没有prepare、11/12 个文件(含全部编译产物)与上游逐字符一致。几何与像素实测全部命中:遮罩文字位置误差0 像素、rgba(0,0,0,0.25)的压暗比例实测×0.75、纯红遮罩全屏(254,0,0)且覆盖状态栏。
值得记下来的是这几条:
"点了没反应"要先做无遮挡的对照。遮罩一起,坐标点击工具照样报成功,但点击被 Modal 窗口吞掉、什么都没发生。⇒工具报成功 ≠ 按钮被按下;先分离"没点到"和"功能没生效"。
读源码要读运行时真正执行的那一份。这个包
main指向ESM 的编译产物(target: esnext且没写module),Node 端require不了⇒ PC 探针这条路走不通,只能直接读lib/并核对它与src/一致。这一步顺带排除了"陈旧编译产物"的风险。cancelable这类 prop 要先确定"触发源"。我第一版页面给组件传了onRequestClose——它根本不接受;而Modal.onRequestClose是返回键触发的,不是点遮罩。⇒写交互断言前先确认"这个交互由什么事件触发",否则会写出一个永远不会触发的断言。"半受控"是这类组件的隐藏陷阱。组件内部
useState(props.x)+useEffect([x])同步,同时又允许自己改这份状态 ⇒它一旦自己改了,父组件就被蒙在鼓里。⇒凡是组件内有useState(props.x),都要专门测"组件自己改了这份状态之后,父组件的 prop 还有效吗"。这里的答案是:无效,而且库没有任何回调能通知你。有覆盖层的组件,测试设计要预留"退出路径"。遮罩吞触摸 ⇒ 页面按钮全失效 ⇒ 只能靠返回键或重启;而
cancelable=false的实例返回键也关不掉。⇒设计测试页时就要想好每种状态怎么退出来。"代码级成立"和"已实测"是两件事。
key每次渲染都变 ⇒ React 必然重挂载,这是确定的语义;但我这一轮没能在运行时把挂载次数测出来(被遮罩吞触摸挡住了)。⇒如实写成"代码级事实成立、运行时未验证",而不是含糊地当成已验证。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-loading-spinner-overlay(上游3.0.1的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-loading-spinner-overlay |
| 适配 TAG | 3.0.1-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS,零运行时依赖) |
| 库类型 | 纯 JS 函数组件:全屏加载遮罩,渲染在 RNModal内 |
| 运行时依赖 | 零(dependencies字段不存在) |
prepare/prepack | 都没有⇒ 安装路径干净 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/joinspontaneous/react-native-loading-spinner-overlay |
| 上游基线 | npmgitHead=84896fa0284b705f136622dcd0d7f17184f747bb |
| 宿主工程 | RNOH084Demo(测试页rnAppKey = LoadingSpinnerTestApp) |
# 从适配仓库安装npminstall"git+https://atomgit.com/oh-react-native/react-native-loading-spinner-overlay.git#3.0.1-ohos-1.0.0"--ignore-scripts# 或本地 file: 安装(装成软链,需要配 watchFolders)npminstall../react-native-loading-spinner-overlay --ignore-scripts// metro.config.js —— file: / 软链装法需要把真实目录加进 watchFolderswatchFolders:[path.resolve(__dirname,'../react-native-loading-spinner-overlay')],// 用法:父组件自己掌握状态 + 自己监听返回键(不要依赖库的内部行为) import Spinner from 'react-native-loading-spinner-overlay'; import {BackHandler} from 'react-native'; const [loading, setLoading] = useState(false); useEffect(() => { const sub = BackHandler.addEventListener('hardwareBackPress', () => { if (loading) { setLoading(false); return true; } return false; }); return () => sub.remove(); }, [loading]); <Spinner visible={loading} textContent="加载中…" overlayColor="rgba(0, 0, 0, 0.25)" // 实测:屏幕亮度 ×0.75 color="#ffffff" size="large" // 不支持 'normal';也可传数值 spinnerKey="main-spinner" // 建议给固定值,避免每次渲染重挂载 cancelable={false} // true 时允许返回键关闭(且不会通知父组件) />;# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey LoadingSpinnerTestApp环境搭建
本手册的验证环境是React Native for OpenHarmony(RNOH)。从零准备开发环境(DevEco Studio、HarmonyOS SDK API 26、Node.js、ohpm、hvigorw、模拟器与hdc调试)请按 CPF-RN 开发者指南的「环境初始化」操作:
- CPF-RN 开发者指南 · 02-搭建准备 · 环境初始化:https://atomgit.com/CPF-RN/docs/blob/main/开发者指南/02-搭建准备/环境初始化.md
环境就绪后,本次验证使用的具体版本组合见文末「验证环境」一节。
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony/@rnoh/react-native-openharmony0.84.3 |
| Node.js / npm | v24.14.0/11.9.0 |
| DevEco Studio | 26.0.0.621 |
| HarmonyOS SDK | API 26(26.0.0.32) |
| 设备 | HarmonyOS 7.0.0(26.0.0) Beta2 模拟器Pura X View(ohos-x64),1320×2232,density 3 |
| 宿主 HAP 产物 | entry-default-signed.hap(82,083,102 字节) |
| 本次增量构建 | assembleHap3 分 52 秒,HAP +32,770 字节;打包资源 14(无新增,符合预期) |
| 验证规模 | 设备侧12 / 14(A 10/10、B 1/1、C 1/1、D 0/2 未验证);外部含几何逐像素核对 1 次、像素取样 7 点、吞触摸对照 2 组、返回键 2 例、半受控四步、上游 12 个文件逐字符比对 |
欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN
React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview