news 2026/10/24 5:28:50

React Native for OpenHarmony 实战:三方库 react-native-raw-bottom-sheet 的鸿蒙化适配指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React Native for OpenHarmony 实战:三方库 react-native-raw-bottom-sheet 的鸿蒙化适配指南

本文记录把react-native-raw-bottom-sheet(底部弹出面板 / Bottom Sheet)适配到 HarmonyOS 的完整过程。

它做的事很专一:从屏幕底部滑出一块指定高度的面板(带半透明遮罩),点遮罩关闭、可选支持下拉手势与顶部"拖拽条"。

结论:这是目前遇到的最干净的一个库 —— 零依赖、零 peer、九项老写法全空、函数组件、四组几何像素级精确命中,<Modal>在 RNOH 上完全可用。
只有一个小行为值得注意:close()不"幂等"(没打开时调用它也会触发onClose)。

★★★★★ 一、四组几何像素级命中(height是布局属性,dump 直接可量)

关键设计(src/index.js):

const{height=260,openDuration=300,closeDuration=200,useNativeDriver=false,…}=props;constanimatedHeight=useRef(newAnimated.Value(0)).current;// 打开Animated.timing(animatedHeight,{useNativeDriver,toValue:height,duration:openDuration}).start();// 关闭Animated.timing(animatedHeight,{useNativeDriver,toValue:0,duration:closeDuration}).start(cb);…<Animated.View style={[styles.container,{height:animatedHeight},…]}>

而styles.container是:

container:{backgroundColor:'#fff',width:'100%',height:0,overflow:'hidden'}

⇒面板高度 =heightprop(vp)、宽度 ='100%'= 屏宽✓
⇒而且useNativeDriver默认是false⇒ 高度变化【进布局】⇒ 布局 dump 直接可量✓✓
⇒拖拽条:draggableIcon: {width: 35, height: 5, borderRadius: 5, margin: 10}✓

实测(density 3;屏 1320×2232):

对象参数公式预言实测(布局 dump)
S1 面板height={200}200vp × 3/'100%'600 × 1320px✅h=600 w=1320 y=1632
S2 面板height={300}300vp × 3/'100%'900 × 1320px✅h=900 w=1320 y=1332
拖拽条(draggable)draggableIcon 35×535vp/5vp × 3105 × 15px✅105x15 y=1662
遮罩styles.wrapper flex:1全屏1320 × 2232px✅1320x2232

⇒四组全部精确命中✓✓
⇒ 顺带一个"合理"的现象:关闭状态下也能量到1320×2232的全屏块—— 那是测试页自己的根View(flex:1),不是 Modal 的遮罩(遮罩只在Modal visible时存在)✓

★★★★ 二、<Modal>在 RNOH 上完全可用(本轮最值得验证的一点)

这个库的整棵子树都挂在 RN 的<Modal>里:

<Modal testID="Modal"transparent visible={modalVisible}onRequestClose={…}><KeyboardAvoidingView behavior={Platform.OS==='ios'?'padding':'height'}…><TouchableOpacity style={styles.mask}…/>{/* 遮罩 */}<Animated.View style={[styles.container,{height:animatedHeight}]}>…</Animated.View></KeyboardAvoidingView></Modal>

实测(相位驱动):

相位动作dump 结果
P0两个实例都关闭没有面板✓
P1S1.open()面板 600×1320 + 拖拽条 105×15✓
P2S1.close()面板消失✓
P3S2.open()面板 900×1320✓
P4S2.close()面板消失✓

⇒<Modal>的transparent/visible开关、子树渲染、层级都正常✓✓
⇒ 而且Modal里的节点坐标是屏幕坐标(y=1632正好在屏幕底部上方 600px)⇒ 量起来很直接 ✓

★★★★ 三、useNativeDriver写对了:显式false,且没有告警

useNativeDriver=false,// ★ 默认值就是 false(props 里可覆盖)… Animated.timing(animatedHeight,{useNativeDriver,toValue:height,duration:openDuration}).start();Animated.spring(pan,{toValue:{x:0,y:0},useNativeDriver}).start();Animated.event([null,{dy:pan.y}],{useNativeDriver})(e,gestureState);

⇒5 处用到useNativeDriver的地方全部显式传值✓
⇒实测:LogBox 里【没有任何通知】✓✓
⇒ 这与"Animated未指定useNativeDriver会常驻一条通知"的情况正好相反 ——这个库的处理方式是正面的样本✓
⇒ 也正因为false,高度动画才会进布局(才能被 dump 量到)✓

★★★ 四、一个值得注意的行为:close()不"幂等"

consthandleSetVisible=visible=>{if(visible){…}else{Animated.timing(animatedHeight,{useNativeDriver,toValue:0,duration:closeDuration}).start(()=>{setModalVisible(false);pan.setValue({x:0,y:0});if(typeofonClose==='function'){onClose();}// ★ 无条件调用});}};useImperativeHandle(ref,()=>({open:()=>handleSetVisible(true),close:()=>handleSetVisible(false),}));

⇒close()分支里没有任何"当前是否已打开"的判断⇒在从未打开过的实例上调用close(),也会走完动画并触发onClose✓

实测(P0 就是"两个实例都关闭"的相位,而我的 P0 动作是对二者各调一次close()):

===== P0:全部关闭(dump 里不应有面板) ===== EVENT | S1 onClose(第 1 次) ← S1 从未打开过,却触发了 onClose EVENT | S2 onClose ← 同上 … EVENT | S1 onOpen(第 1 次) ← P1 EVENT | S1 onClose(第 2 次) ← P2 EVENT | S2 onOpen ← P3 EVENT | S2 onClose ← P4

⇒回调的次数会比你预期的多("从没开过 ⇒ 不会有 close"这个直觉不成立)✓
⇒实践建议:在onClose里做收尾逻辑时,自己记一个"之前是否真的打开过"的标志位,别默认它只在"真的关闭"时触发 ✓

★★★ 五、其余预检结论(都很干净)

预检项结果
依赖✅✅dependencies与peerDependencies皆空;import 图只有react+react-native
老写法九项✅✅全部为空(现代函数组件:forwardRef+useImperativeHandle+useState+useRef)
measure()/measureInWindow/onLayout/findNodeHandle/NativeModules✅全空⇒ 零环境坑
平台分支✅仅 1 处且安全:behavior={Platform.OS === 'ios' ? 'padding' : 'height'}⇒ 鸿蒙取'height'
useNativeDriver✅显式false(5 处全传)⇒ 无告警
手势✅ 用 RN 核心的PanResponder(onStartShouldSetPanResponder: () => draggable)⇒ 纯 JS、无原生依赖
scripts✅无prepare/postinstall

契约(反射):

NOTE | 默认导出的 typeof = object PASS | ★ 默认导出是 forwardRef 结果(typeof 为 object,不是 function) NOTE | $$typeof 的字符串 = Symbol(react.forward_ref) PASS | ★ render 存在(forwardRef 的渲染函数) PROBE | S1 ref 可得 = true / typeof open = function / typeof close = function

⇒forwardRef的结果typeof是'object'而不是'function'—— 如果你按"组件必须是 function"写断言,这条会误判 ✓


六、交付包质量与基线说明

类别结果
交付包内容index.js(57 B,re-export)+src/index.js(5290 B)+src/style.js(603 B)+index.d.ts⇒ 源码可读 ✓
spec.json⚠️缺upstreamCommit
validation⚠️"build-pass-device-pending"
⚠️ 声明的基线reactNative: "0.88.0"+rnoh: "0.84.4"(verification: evidence/batch50/…)⇒两字段自相矛盾,且既不是当前宿主(0.84.1/0.84.3)也不是权威 0.88 基线
自带测试__tests__/RBSheet.test.js+ 鸿蒙契约测试(契约测试只校验包名/版本/入口/中文说明)

关于基线:本次验证在RN0.84.1+ RNOH0.84.3上完成。这个库没有任何已移除的写法,
行为只依赖Modal/Animated/PanResponder/KeyboardAvoidingView⇒结论跨基线成立。


七、已知限制与本次验证边界

7.1 用这个库要知道的

事项说明
★★★close()不幂等没打开时调用也会走完动画并触发onClose⇒onClose里做收尾要自己记标志位
★★★★ 面板尺寸就是heightprop高度 =heightvp;宽度 ='100%'(屏宽);默认height=260
★useNativeDriver默认false所以高度动画进布局;想改成原生驱动要自己确认驱动的是不是纯 transform/opacity
★ 打开时机open()会先setModalVisible(true)(Modal才挂载),再跑高度动画;onOpen在动画【之前】同步触发
★ 关闭是"动画完再卸载"close()的onClose在动画结束回调里触发(closeDuration默认 200ms)
★draggableIcon固定 35×5、margin 10可用customStyles.draggableIcon覆盖
★ 拖拽默认不生效draggable默认false;onStartShouldSetPanResponder: () => draggable
★ 拖拽条与内容只能二选一挂手势dragOnContent决定panHandlers挂在内容上还是拖拽条上
★ 一处平台分支KeyboardAvoidingView的behavior:iOS 用'padding',其余(含鸿蒙)用'height'
★customStyles只能覆盖 4 个槽位wrapper/container/draggableIcon(另有mask未接)

7.2 本次验证的边界

  1. ★★ 手动按钮(Text+onPress)点击后没有产生任何事件⇒ 可能是 RNOH 上Text.onPress不生效,也可能是我的点击未命中按钮 ⇒未验证;
  2. ★ 拖拽手势未验证(draggable的渲染部分已验证:拖拽条 105×15px 出现 ✓;但下拉关闭/回弹需要真实手势);
  3. 未验证closeOnPressMask(点遮罩关闭);
  4. 未验证closeOnPressBack(Android 硬件返回键 —— 鸿蒙上本就无此键);
  5. 未验证dragOnContent;
  6. 未验证customStyles/customModalProps/customAvoidingViewProps的覆盖效果;
  7. 未验证openDuration/closeDuration的精确时序(只知onClose在动画后触发);
  8. 未验证height默认值 260 的实际渲染(我显式传了 200 / 300);也未验证children默认<View />;
  9. 未验证同时打开两个实例时的层叠行为;
  10. 未在RN 0.88.0/RNOH 0.84.4上复验(交付包声明的基线);
  11. 未测其它屏幕宽度;
  12. 未做跨平台对照。

未改动库代码。


八、常见问题

Q1:它在 RN 0.84 上能跑吗?

能,而且很干净。<Modal>正常、面板几何四组精确命中、onOpen/onClose回调正常、LogBox 零通知。

Q2:面板高度怎么控制?

就是heightprop(单位 vp):

<RBSheet ref={ref} height={200} /> // ⇒ 面板 200vp 高(density 3 ⇒ 600px)、宽为屏宽

Q3:onClose好像被多调用了一次?

对 ——close()不幂等:即使实例从未打开过,调用close()也会走完closeDuration的动画并触发onClose。
⇒ 在onClose里做收尾时,自己维护一个"是否真的打开过"的标志位。

Q4:为什么我调的open()没反应?

检查ref有没有真的接上(组件是forwardRef,只暴露open()/close()两个方法);
也可以用 ref 的可取性做判据:typeof ref.current?.open === 'function'。

Q5:怎么让底部出现"拖拽条"?

<RBSheet draggable={true} … />

拖拽条是draggableIcon:35×5vp、圆角 5、margin 10(density 3 ⇒ 105×15px,实测一致 ✓)。

Q6:下拉手势为什么不生效?

draggable默认false;而且真正挂panHandlers的位置取决于dragOnContent:

  • dragOnContent=false(默认)⇒ 手势挂在拖拽条上;
  • dragOnContent=true⇒ 手势挂在内容容器上(且要求gestureState.dy > 0)。
    另外useNativeDriver默认false⇒ 手势更新走 JS 侧。

Q7:为什么布局 dump 里能量到面板?

因为useNativeDriver默认false,而它驱动的是height(布局属性)⇒ 高度变化会反映到布局上。
⚠️ 反之,如果某个库用transform或opacity做动效,dump 就量不到。

Q8:想换成原生驱动可以吗?

可以传useNativeDriver={true},但要确认被驱动的属性支持原生驱动(本库驱动height⇒不建议)。


小结

react-native-raw-bottom-sheet是零依赖、零 peer、现代函数组件的底部面板库(forwardRef+useImperativeHandle),
九项老写法全空,设备侧表现完全正常,四组几何像素级命中,LogBox 零通知。

最值得记下来的是这几条:

  1. ★★★★★ 四组几何全部精确命中。
    面板height=200⇒600×1320px、height=300⇒900×1320px、拖拽条 ⇒105×15px、遮罩 ⇒1320×2232px✓✓
    ⇒ 关键在于height是布局属性 +useNativeDriver为false⇒ 高度变化进布局、dump 可量✓

  2. ★★★★<Modal>在 RNOH 上完全可用。
    transparent/visible开关、子树渲染、层级都正常;而且Modal 内节点用屏幕坐标,量起来很直接 ✓

  3. ★★★★useNativeDriver显式传false的正面样本。
    5 处全传 ⇒LogBox 零通知✓ —— 与"未指定useNativeDriver会常驻一条通知"的情况正好相反 ✓

  4. ★★★ 一个真实的行为发现:close()不幂等。
    源码close分支里没有"当前是否已打开"的判断⇒ 在从未打开过的实例上调用close()也会触发onClose
    (实测 P0 阶段两个实例都没开过,却各触发了一次onClose)⇒onClose里做收尾要自己记标志位✓

  5. ★★★ 平台分支只 1 处且安全:KeyboardAvoidingView的behavior用Platform.OS === 'ios' ? 'padding' : 'height'⇒
    鸿蒙自然落到'height'✓ ——二元平台判断在"第四平台"上天然安全✓

  6. ★★ 契约反射要记得forwardRef的typeof是'object'。
    Symbol(react.forward_ref)+render才是它的特征;按"组件必须是function"写断言会误判 ✓

  7. ★ 一次时序漂移的教训与补救:自动时间轴采样时会漂(我第一次漏掉了 900px 那一档),
    补救办法是重启后在目标相位的窗口内密集采样(本次采到两次P3,都给出h=900 w=1320)✓

  8. ⚠️ 一处未验证要如实写:页面上的手动按钮(Text+onPress)点击后没有产生任何事件⇒
    可能是 RNOH 上Text.onPress不生效、也可能是我点击未命中 ⇒未验证,不写成结论 ✓


本篇用到的库

项内容
三方库react-native-raw-bottom-sheet(上游3.0.0的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-raw-bottom-sheet
适配 TAG3.0.0-ohos-1.0.0
需要 HAR / 权限 / ohpm都不需要(纯 JS,手势用 RN 核心PanResponder)
库类型底部弹出面板(Bottom Sheet)
运行时依赖✅✅无(dependencies与peerDependencies皆空)
prepare/postinstall✅都没有
许可证MIT
上游仓库https://github.com/nysamnang/react-native-raw-bottom-sheet(★1203)
宿主工程RNOH084Demo(测试页rnAppKey = RawBottomSheetTestApp)
# 从适配仓库安装(零依赖)npminstall"git+https://atomgit.com/oh-react-native/react-native-raw-bottom-sheet.git#3.0.0-ohos-1.0.0"--ignore-scripts
import RBSheet from 'react-native-raw-bottom-sheet'; const ref = useRef(null); <RBSheet ref={ref} height={260} // 面板高度(vp)⇒ density 3 时 ×3 得像素 openDuration={300} closeDuration={200} closeOnPressMask // 点遮罩关闭(默认 true) closeOnPressBack={false} // Android 硬件返回键(默认 false) draggable={false} // 打开后顶部出现拖拽条(35×5vp) dragOnContent={false} // 手势挂在拖拽条还是内容上 useNativeDriver={false} // 默认 false ⇒ 高度动画进布局 customStyles={{container: {borderTopLeftRadius: 12, borderTopRightRadius: 12}}} onOpen={() => console.log('打开了')} onClose={() => console.log('关闭了 —— ⚠️ 未打开过时调用 close() 也会触发')}> <View style={{padding: 16}}><Text>面板内容</Text></View> </RBSheet> // ref.current.open(); // 打开 // ref.current.close(); // 关闭(⚠️ 不幂等) // ⚠️ 面板尺寸 = height vp(高)× 屏宽('100%') // ⚠️ onOpen 在高度动画【之前】同步触发;onClose 在【动画结束后】触发 // ⚠️ close() 未做"是否已打开"判断 ⇒ onClose 可能被多触发 // ⚠️ 一处平台分支:KeyboardAvoidingView 的 behavior(iOS=padding、其余=height)
# 换页启动测试页 —— force-stop 不能省hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey RawBottomSheetTestApp

判据的复现方式

# ① 契约 / 回调:逐条 PASS/FAIL 日志hdc shell"hilog -x | grep rbs-test"|Select-String'PASS|FAIL|SUMMARY|NOTE|PROBE|EVENT'# ⇒ forwardRef(typeof=object、$$typeof=Symbol(react.forward_ref))# ⇒ S1 onOpen/onClose、S2 onOpen/onClose 的顺序# ② 几何:面板高 = height prop × 3,宽 = 1320(屏宽)devecocli ui layout|Select-String'\[(\d+),(\d+),(\d+),(\d+)\]'# ⇒ S1(height=200)→ h=600 w=1320;S2(height=300)→ h=900 w=1320# ⇒ 拖拽条 105×15;遮罩 1320×2232# ③ LogBox:本库【零通知】(useNativeDriver 显式 false)devecocli ui layout|Select-String'logbox'

环境搭建

本手册的验证环境是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 Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony/@rnoh/react-native-openharmony0.84.3
Node.js / npmv24.14.0/11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 26(26.0.0.32)
设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器Pura X View(ohos-x64),1320×2232,density 3
本机状态栏高度117px = 39vp
宿主依赖✅无需补装(零依赖)
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap3 分 34 秒⇒ 一次成功(打包与构建退出码都检查了)
验证规模几何 4 组像素级命中(600/900×1320 面板、105×15 拖拽条、1320×2232 遮罩);契约反射(forwardRef特征 +ref.open/close);回调链路 5 个相位全对;LogBox 零通知;4 张截图互异

⚠️基线说明:交付包spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
本库无任何已移除写法,行为只依赖Modal/Animated/PanResponder⇒结论跨基线成立。


相关社区与项目地址

项目地址
React Native 鸿蒙官方社区(CPF-RN)https://atomgit.com/CPF-RN
RNOH 框架仓库https://atomgit.com/openharmony-RN/ohos_react_native
RNOH 三方库使用文档https://atomgit.com/OpenHarmony-RN/usage-docs
React Native for OpenHarmony 组织https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

欢迎加入 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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 5:27:36

Python aiida-inq 包详解与实战案例

1. 引言aiida-inq 是一个基于 AiiDA 工作流框架的 Python 包&#xff0c;专门用于高通量第一性原理计算与材料性质分析。它把密度泛函理论计算、数据管理和结果后处理封装成可复用的工作流&#xff0c;帮助科研人员以声明式方式组织计算任务&#xff0c;并自动记录计算 provena…

作者头像 李华
网站建设 2026/10/24 5:25:33

webgis 技能要求

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 5:25:25

LiveAutoRecord 跨平台直播录制完整教程:从安装到多频道自动录制

LiveAutoRecord 跨平台直播录制完整教程&#xff1a;从安装到多频道自动录制 【免费下载链接】LiveAutoRecord 基于 Electron 的多平台直播自动录制软件 项目地址: https://gitcode.com/GitHub_Trending/li/LiveAutoRecord LiveAutoRecord 是一款开源的跨平台直播自动录…

作者头像 李华
网站建设 2026/10/24 5:24:43

金蚕蛊留在业务系统里的伤口,ABAP 如何识别持续消耗型故障

一张销售订单今天保存成功,明天运行的交货作业却把它挑了出来,报一次错。作业次日照常执行,同一张订单又报一次错。过了几天,业务人员看到的仍然只是订单暂时无法交货,技术团队看到的却是不断累积的失败记录、重复调用和人工处理时间。这种故障很适合借《仙剑奇侠传 1》里…

作者头像 李华