React Native for OpenHarmony 实战:三方库 react-native-collapsible 的鸿蒙化适配指南
本文记录把 react-native-collapsible(可折叠容器 + 手风琴)适配到 HarmonyOS 的完整过程。
这个库有两个特点让这轮验证特别"干净":
- 它动画的是普通布局高度(
Animated+useNativeDriver: false+overflow: hidden),所以折叠高度可以被布局系统直接量出来,而不必靠像素比对; - 纯 JS、零运行时依赖,接进宿主只需要三处改动,没有任何原生接线。
结果是:折叠/展开的高度逐像素精确,而且顺手挖出了两个很隐蔽的行为边界——"挂载时就展开"其实不生效,以及 align 在没测过高度之前完全无效。

一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 1.6.2(npm gitHead = c79b7c01…,MIT) |
| 是否需要原生适配 | 不需要——纯 JS,无原生模块,零运行时依赖 |
| 与 npm 上游的差异 | 7 / 8 个文件去掉 CR 后逐字符一致,唯一差异是 package.json;Collapsible.js / Accordion.js / 两个 .d.ts 实现零改动 |
| 自动链接信号 | linked 10 libraries, skipped 2 libraries(计数未增加 ✓) |
| 编译 | assembleHap 4 分 22 秒;HAP 81,725,574 → 81,774,729 字节(+49,155 ≈ 48 KB) |
| 设备侧断言 | ✅ 34 / 34 全部通过 |
| 几何实测 | ✅ 逐像素精确:collapsedHeight=40 → +120px;展开到内容 120vp → +360px;内容 90vp → +270px;手风琴章节展开 → +270px |
| 动画 | ✅ 中途采样 331(严格介于 0 与 360 之间)⇒ 是过渡,不是瞬变 |
| 发现的行为边界 | ⚠️ ① 挂载时 collapsed={false} 高度锁 0;② align 在 contentHeight 为 0 时静默无效;③ align='bottom' 位移与预期差 47px |
| 发现的类型问题 | ⚠️ 两个组件的 .d.ts 与 React 19 类型不兼容(TS2607 / TS2786),运行时不受影响 |
一句话结论:可用,而且折叠高度精确到像素。但有三件事必须知道:Accordion 要走子路径导入、"默认展开"不能只写 collapsed={false}、align 需要先测过一次高度。
二、判定过程:纯 JS、零依赖,但有两个入口
判断一个 RN 库要不要做原生适配,固定走三步:
| 步 | 做法 | 这个库的结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules / requireNativeComponent | 没有 |
三步都指向"纯 JS,不需要原生适配"。而且这个库连运行时依赖都没有:
"dependencies": {}, // 实际没有这个字段
"peerDependencies": {"react": "*", "react-native": "*"}
Animated 和 Easing 都来自 react-native 本身,没有任何第三方依赖 —— 这在三方库里其实少见,也意味着接入时不会遇到"依赖没被装上"这类问题。
2.1 但它有两个入口
package.json 的 main 是 Collapsible.js,而这个文件只导出 Collapsible 一个 default:
// Collapsible.js
export default class Collapsible extends Component { ... }
// Accordion.js ← 同级另一个文件
export default class Accordion extends Component { ... }
⇒ Accordion 必须走子路径:
import Collapsible from 'react-native-collapsible';
import Accordion from 'react-native-collapsible/Accordion'; // ← 不能写成具名导入
写成 import {Accordion} from 'react-native-collapsible' 的话:
- 运行时拿到的是
undefined(Metro 不做类型检查,不会报错,直到你渲染它才炸); - TypeScript 会报
TS2614: Module has no exported member 'Accordion'。
上游 README 的示例其实是写对的(正是上面这两行),所以这一条要看源码或上游文档,不能只看交付包的 README。而交付包的 README 里只写了 import Library from 'react-native-collapsible';,没提子路径。
2.2 两个组件都没有 propTypes
Collapsible.propTypes === undefined // 实测
Accordion.propTypes === undefined // 实测
两个组件都只有 defaultProps,没有任何运行时属性校验。所以:
- 属性名写错不会有开发期警告,只会静默不生效;
Accordion.activeSections虽然在propTypes里没声明,但render里直接activeSections.includes(...)⇒ 不传会TypeError,属于"事实上的必填"。
公开 API:
| 组件 | 属性 |
|---|---|
Collapsible | align(top/center/bottom,默认 top)、collapsed(默认 true)、collapsedHeight(默认 0)、duration(默认 300)、easing(默认 'easeOutCubic')、enablePointerEvents(默认 false)、renderChildrenCollapsed(默认 true)、style、onAnimationEnd、children |
Accordion | sections、activeSections(事实必填)、renderHeader、renderContent、onChange、expandMultiple、expandFromBottom、underlayColor、touchableComponent、touchableProps、renderSectionTitle、renderFooter、disabled、renderAsFlatList、keyExtractor(默认返回 index)、containerStyle、sectionContainerStyle、onAnimationEnd + 上表里除 children 外的 Collapsible 属性透传 |
三、这个交付包长什么样:实现零改动
与 react-native-collapsible@1.6.2 的 npm tarball 逐文件比对(先去 CR 再逐字符比较):
7 / 8 个文件完全一致(Collapsible.js、Accordion.js、index.d.ts、Accordion.d.ts、babel.config.js、README.md、LICENSE),唯一不同的是 package.json:
| 字段 | 变化 |
|---|---|
description | 追加 " for React Native for OpenHarmony" |
repository.url / homepage | 改为 AtomGit 地址 |
scripts.test | 新增 node --test __tests__/harmony-contract.test.cjs |
实现文件零改动,所以下面所有结论都只涉及使用方式与行为边界,与适配质量无关。
交付包另外多了 6 个文件:双语 README、spec.json、代码检查报告、契约测试、.gitignore。
四、接入宿主与构建运行
纯 JS 库的接法只有三处:
// package.json
"react-native-collapsible": "file:../react-native-collapsible"
// metro.config.js —— file: 装进来是 junction,必须让 Metro 找得到源码
watchFolders: [path.resolve(__dirname, '../react-native-collapsible')],
// index.js
AppRegistry.registerComponent('CollapsibleTestApp', () => CollapsibleTestApp);
不要动 harmony/entry/oh-package.json5(不需要 HAR),不要动 module.json5(不需要权限)。信号是自动链接计数不增加:
info updated 4 file(s), linked 10 libraries, skipped 2 libraries
| 项 | 数值 |
|---|---|
首次 assembleHap | 4 分 22 秒 |
| 二次重编(给测量区加了就地按钮) | 3 分 40 秒 |
| HAP 变化 | 81,725,574 → 81,774,729 字节(+49,155 ≈ 48 KB) |
五、验证设计:动画的是"布局高度",所以可以直接量
5.1 为什么这轮不用像素比对
看这个库的核心实现:
this._animation = Animated.timing(this.state.height, {
useNativeDriver: false, // 高度不可原生驱动
toValue: height ? height : 0,
duration,
easing,
}).start(...)
外层是 Animated.View,样式是 { overflow: 'hidden', height: <animated> }。
⇒ 它动画的是一个普通布局高度,而不是颜色、透明度或不透明度这类"只有像素才能看出来的东西"。所以布局系统给出的矩形就是精确的、定量的证据,不需要截图比对。
5.2 方法:marker 夹逼
页面在每个折叠区前后各放一个文本 marker:
MARK-A-START
〈就地按钮〉
[ Collapsible 折叠区 ]
MARK-A-END
于是:
Y(MARK-A-END) − Y(MARK-A-START) = 折叠区高度 + 固定装饰(marker 自身 + 按钮)
比较同一区域在不同状态下的间距差,就得到折叠区的高度增量 —— 这个差值与滚动位置、marker 高度、按钮高度全都无关,因此极其稳健。
. E:\rnoh-work\collapsible-probe.ps1
Measure-Gap -Top 'MARK-A-START' -Bottom 'MARK-A-END' # 自动滚动到两个 marker 同时可见
一个实现细节:展开后页面会变长,
MARK-A-END可能滑出视口导致读数为空。所以探针里的Scroll-Both会反复滚动直到两个 marker 同时可见才读数。
5.3 内容高度刻意取整
页面里内容高度取整数 vp(120 / 90),设备 density = 3 ⇒ 期望像素高度 360 / 270 / 120。
这样"精确到像素"才有判据。 如果内容高度是 117 这种数,量出来 351 就只能说"差不多"。
5.4 分组口径
| 组 | 进通过率 | 内容 |
|---|---|---|
| A 契约与导出 | ✅ 22 | 两个组件的导出形态、defaultProps、propTypes 缺失、keyExtractor 行为 |
| B 白盒实例方法与错误路径 | ✅ 9 | 实例方法齐备、state.height 是 Animated.Value、非法 easing 抛错 |
| C 回调与受控语义 | ✅ 3 | 回调记录格式、activeSections 类型 |
| 外部协议 | 信息 | marker 夹逼量高度、动画中途采样、手风琴交互 |
六、实测一:折叠与展开高度逐像素精确
设备:Pura X View 模拟器,1320×2232(density 3)。
| 区域 | props | 折叠态间距 | 展开态间距 | 增量 | 换算 |
|---|---|---|---|---|---|
| A | collapsedHeight 默认 0,内容 120vp | 156 | 516 | +360 | = 120 vp ✓ 精确等于内容高度 |
| B | collapsedHeight=40,内容 90vp | 276 | 426 | +150 | 276−156 = 120 = 40vp ✓;426−156 = 270 = 90vp ✓ |
| C | renderChildrenCollapsed=false,内容 120vp | 156 | 515 | +359 | ≈360(1px 取整差) |
(A 区"间距 156"里包含就地按钮的高度,所以不是 0;这只影响绝对值,不影响增量。)
两条被这个测量排除的风险:
measure()的单位是对的。 库展开时走_measureContent()→ref.measure(...),而折叠动画的目标值就来自它。如果 RNOH 的measure()返回物理像素而布局高度是 vp,展开高度会差 3 倍(1080px)。实测 360px = 120vp,排除。collapsedHeight与内容高度是按同一单位比较的。 B 区折叠态 276 = 156 + 120(40vp)、展开态 426 = 156 + 270(90vp),两个数各自都精确落在 vp 网格上。
6.1 动画确实在动
展开 A 后立刻采样(受 devecocli ui layout 自身耗时影响,实际采样点晚于名义时间):
| 采样 | 间距 | 相对基线 |
|---|---|---|
| 基线 | 156 | 0 |
| 展开后不久 | 385 | +331 |
| 动画结束 | 516 | +360 |
+331 严格介于 0 与 360 之间 ⇒ 是动画过渡,不是瞬变 ✓
这也是"一次采样不够"的例子:只测最终值无法区分"动画"和"瞬间跳变",必须在动画过程中取一次中间值。
6.2 renderChildrenCollapsed
| 状态 | CONTENT-C 节点 |
|---|---|
折叠(默认 renderChildrenCollapsed=true) | 会出现(被裁掉但仍在树上) |
折叠(renderChildrenCollapsed={false}) | 不存在 ✓ |
| 展开 | 出现 ✓ |
| 再折叠 | 再次消失 ✓ |
⇒ 折叠时子节点被真正移除,不是单纯视觉隐藏。

七、实测二:两个隐蔽的行为边界
7.1 "挂载时就展开"其实不生效
C 区初始 collapsed={false}(意图是"默认展开")。实测结果出人意料:
C 区间距 = 156 ← 与 collapsedHeight=0 的折叠态完全相同
CONTENT-C 不存在
高度被锁在 0。 看源码就能解释:
const hasKnownHeight = !measuring && (measured || collapsed);
const style = { overflow: 'hidden', height: hasKnownHeight ? height : 0 };
挂载时 measured = false、collapsed = false ⇒ hasKnownHeight = false ⇒ height 恒为 0。
而 measured 只在 _measureContent() 里被置 true,_measureContent 又只由 _toggleCollapsed() 调用,_toggleCollapsed 只在 collapsed 发生变化时由 componentDidUpdate 触发 —— componentDidMount 里没有。
⇒ Collapsible 必须经历一次 collapsed 变化才会展开。
再结合 defaultProps.collapsed = true,可以看出这个库的设计前提是「从折叠状态开始」。实测验证:对 C 执行「折叠 → 展开」后,间距 156 → 515,CONTENT-C 出现 ✓
想做"默认展开"怎么办:不能只写 collapsed={false},得让它经历一次变化,例如挂载后切一次:
const [collapsed, setCollapsed] = useState(true);
useEffect(() => setCollapsed(false), []); // 借一次变化触发测量
7.2 align 有一个隐藏前置条件
align 的实现是给内容层加 translateY,插值区间用的是 inputRange: [0, contentHeight]:
contentStyle.transform = [{ translateY: height.interpolate({
inputRange: [0, contentHeight],
outputRange: [contentHeight / -2, 0], // center
})}];
⇒ contentHeight 为 0 时,插值退化成常量 0,align 完全没有效果。
实验一(没测过高度):三个实例(collapsedHeight=60、内容 120vp、align 分别 top/center/bottom)在"挂载即 collapsed=false"状态(即 7.1 的情况,contentHeight 从未被赋值)下折叠到 60vp:
| 实例 | 内容相对 marker 的偏移(布局 dump) | 像素扫描到的文字位置 |
|---|---|---|
D1 top | 187 | 1562 |
D2 center | 187 | 1796 |
D3 bottom | 187 | 2028 |
三者偏移完全相同,并且像素扫描与布局位置吻合(差 8–10px 是字形上沿)⇒ translateY 恒为 0,align 未生效 ✓
实验二(先真正展开一次):点「展开」(走 _measureContent,contentHeight 被赋值 120vp),再折叠到 60vp:
| 实例 | 未变换偏移 | 变换后偏移 | 实测位移 | 预期位移 | 结论 |
|---|---|---|---|---|---|
D1 top | 187 | 187 | 0 | 0 | ✓ |
D2 center | 187 | 97 | −90px | −30vp×3 = −90px | ✓ 精确 |
D3 bottom | 187 | 54 | −133px | −60vp×3 = −180px | ✗ 差 47px |
center 精确吻合:translateY = (height − contentHeight)/2 = (60 − 120)/2 = −30vp = −90px ✓
bottom 按同一公式应为 height − contentHeight = −60vp = −180px,实测 −133px,差 47px 未能解释(反推该实例的 contentHeight 约为 104vp,而 center 那个是 120vp —— 为什么两个实例的 contentHeight 不同,我没有查到原因,如实记录为未解释项)。
实践结论:要用 align,必须先让组件经历过一次真正的展开;而且 bottom 的实际效果在本环境下与预期不符,不要依赖它。

八、实测三:Accordion 的受控语义
Accordion 是完全受控的:activeSections 由父组件持有,onChange 只负责给出新数组。
页面里放两个章节(keyExtractor 用默认实现,即返回 index),章节内容高度 90vp:
| 操作 | gapACC | 章节标题状态 | onChange 收到 |
|---|---|---|---|
| 初始 | 296 | 第一章(已折叠)/ 第二章(已折叠) | — |
| 点第一章 | 566(+270 = 90vp ✓ 精确) | 第一章(已展开)、第二章(已折叠) | onChange([0]) ✓ |
| 点第二章 | 566(一收一开) | 第一章(已折叠)、第二章(已展开) | onChange([1]) ✓ 替换而非累加 |
| 再点第二章 | 296 | 两章均折叠 | onChange([]) ✓ |
三条结论:
- 章节展开的高度精确等于内容高度(+270px = 90vp);
expandMultiple默认为 false:点第二个会替换第一个,而不是两个都展开([0]→[1]);onChange给的是 key 而不是 index —— 因为默认keyExtractor恰好返回 index,两者在这个用例里数值相同;一旦你自定义了keyExtractor,activeSections里的就必须是你自己的 key。这一点很容易踩。
const [active, setActive] = useState<(string | number)[]>([]);
<Accordion
sections={['第一章', '第二章']}
activeSections={active} // 受控:必须自己保存
renderHeader={(section, key, isActive) => <Text>{`${section}${isActive ? '(已展开)' : '(已折叠)'}`}</Text>}
renderContent={(section, key) => <Text>{`第 ${key} 节正文`}</Text>}
onChange={setActive} // 只给新数组
duration={400}
/>

九、已知限制、交付包质量与一个类型问题
9.1 库本身的
- 挂载即
collapsed={false}不会展开(高度锁 0),必须经历一次collapsed变化(见 7.1)。 align依赖contentHeight,未测过高度时静默无效(见 7.2)。align='bottom'实测位移与预期差 47px,未解释;align='center'精确。Accordion的事实必填属性activeSections没有propTypes声明,不传会TypeError。- 两个组件都没有
propTypes⇒ 属性名写错没有开发期警告,只会静默失效。 - 非法
easing会抛异常(这是好事,但要提前知道):
实测:直接调Error: Invalid easing type "bogus"_transitionToHeight会同步抛出这条错误;合法的'easeOutCubic'/'easeInOutQuad'等名字会被自动映射成Easing.out(Easing.cubic)这类组合。默认值'easeOutCubic'是安全的。 Accordion需要子路径导入(见 2.1)。enablePointerEvents默认 false ⇒ 折叠状态下pointerEvents='none',折叠区不接收触摸。如果你把可点元素放在折叠区内,展开前它是点不到的。
9.2 交付包质量
做得好的:
- 实现零改动(7/8 文件与上游逐字符一致);
- 零运行时依赖,不存在"依赖没被装上"的问题;
LICENSE确为 MIT,与package.json一致;spec.json带license字段;- 契约测试换成了可运行的
node --test。
问题:
① README 没写 Accordion 要走子路径。 两个 README 的示例都是 import Library from 'react-native-collapsible';,既没提子路径,占位名 Library 也与真实导出名(Collapsible)不一致。照着写的人找不到 Accordion。
② README 的「能力一览」只有 2 行,Collapsible 的 9 个属性与 Accordion 的 18 个属性一个都没写。
③ spec.json 无 upstreamCommit(值可从 npm 查到),validation 是裸字符串 "pass",upstream.url 里还带着用户名。
④ 契约测试通过 ≠ 可用。 测试只断言包名、版本、主入口文件存在、中文 README 存在 —— 没有渲染组件、没有测折叠行为,也没测 .d.ts 能否通过类型检查(而后者恰好是坏的,见 9.3)。
⑤ 上游把 husky 的 git hooks 配置留在了发布包里。作为消费者安装时不会执行(没有 prepare/postinstall),所以无害,但发布包里留着它没有意义。
9.3 一个类型问题:.d.ts 与 React 19 不兼容
在本项目的类型环境下(@types/react@19.3.0、RN 0.84.1 自带类型、moduleResolution: bundler),写 <Collapsible /> 或 <Accordion /> 会报:
error TS2607: JSX element class does not support attributes because it does not have a 'props' property.
error TS2786: 'Collapsible' cannot be used as a JSX component.
Its type 'typeof Collapsible' is not a valid JSX element type.
Type 'Collapsible' is missing the following properties from type 'Component<any, any, any>':
context, setState, forceUpdate, render, and 2 more.
我做了两组隔离测试来定位:
| 用例 | 结果 |
|---|---|
本文件里自己声明的 class 组件(空类体 / 带 render / declare class) | 不报错 ✓ |
自己写的 .d.ts,声明形态与库完全一致(export default class … extends React.Component<P> {}),相对路径导入 | 不报错 ✓ |
是否存在重复的 @types/react 副本 | 不存在(全局只有一个 node_modules/@types) |
import Collapsible from 'react-native-collapsible' | 报错 ✗ |
import Accordion from 'react-native-collapsible/Accordion' | 报错 ✗ |
⇒ 可以确定:这个库的 .d.ts 在本项目的类型环境下无法作为 JSX 组件使用,而且问题不在"空类体"这个写法本身,也不在"重复的 React 类型副本"。具体机制我没有定位到,如实记录。
影响与绕法:运行时完全不受影响(Babel 剥掉类型,Metro 打包 exit 0,设备上 34/34 全通过)。要在 TS 项目里带类型使用:
const SafeCollapsible = Collapsible as unknown as React.ComponentType<any>;
或对使用处加 @ts-expect-error。对工程质量敏感的项目,建议把这条写进接入清单。
9.4 本次验证的边界
- 只在一台模拟器上验证(
Pura X View,density 3),没有真机。 align='bottom'那 47px 的偏差未定位原因。enablePointerEvents未验证(折叠时触摸是否真的被吞掉)。expandFromBottom/renderAsFlatList/renderFooter/renderSectionTitle/touchableComponent/disabled/containerStyle/sectionContainerStyle未逐一验证。style属性透传、以及展开状态下改变collapsedHeight的行为未测。- 长列表性能、多个折叠区同时动画的性能未测。
- 没有跨平台对照(未在 iOS/Android 上跑同一份用例)。
未改动库代码。
十、常见问题
Q1:需要 HAR、权限或 ohpm 接线吗?
都不需要。纯 JS 且零运行时依赖,接进宿主只改 package.json、metro.config.js、index.js 三处。判断信号是 link-harmony 计数不增加(本次 linked 10 libraries, skipped 2 libraries)。
Q2:怎么装?
npm install "git+https://atomgit.com/oh-react-native/react-native-collapsible.git#1.6.2-ohos-1.0.0"
# 或本地 file: 方式
npm install ../react-native-collapsible
Q3:Accordion 怎么导入?
import Accordion from 'react-native-collapsible/Accordion'; // 子路径,必须
不能写成 import {Accordion} from 'react-native-collapsible' —— 运行时是 undefined。
Q4:我想让它一开始就是展开的,怎么写?
不能只写 collapsed={false}(挂载时高度会被锁在 0)。要借一次 collapsed 变化来触发测量:
const [collapsed, setCollapsed] = useState(true);
useEffect(() => { setCollapsed(false); }, []);
<Collapsible collapsed={collapsed} duration={300}>…</Collapsible>
Q5:collapsedHeight 是干什么的?
折叠时保留的高度。默认 0(完全收起)。实测精确:collapsedHeight={40} 在 density 3 的设备上折叠态高度正好 120px = 40vp。
常见用法是"折叠起来只露出一行标题":
<Collapsible collapsed={c} collapsedHeight={40} duration={300}>
<View style={{height: 200}}>…</View>
</Collapsible>
Q6:align 怎么用?为什么我看不出效果?
align 控制过渡过程中内容的对齐方向(top 默认 / center / bottom)。两个注意点:
- 内容必须比折叠后的高度高,位移才看得出来(
translateY是按height / contentHeight插值的); - 必须先经历过一次真正的展开 ——
contentHeight为 0 时插值退化成常量 0,align静默失效(见 7.2)。
实测:align='center' 的位移精确等于 −(contentHeight − height)/2;align='bottom' 在本次环境下的实测值与公式不符(差 47px),不建议依赖。
Q7:easing 传字符串还是函数?
都行。字符串走库内置的名称映射(支持 linear、easeInOut*、easeOut*、easeIn* 系列,会自动组合成 Easing.out(Easing.cubic) 这类形式)。传了不认识的名字会抛 Invalid easing type "xxx"。
Q8:Accordion 的 activeSections 里放 index 还是 key?
放 keyExtractor 返回的值。默认 keyExtractor 返回 index,所以看起来像 index;自定义之后就必须放自定义的 key。onChange 给你的也是这组 key。
Q9:折叠状态下里面的按钮点不动?
enablePointerEvents 默认为 false,折叠时外层会被设为 pointerEvents='none'。要让它可点:
<Collapsible collapsed={c} enablePointerEvents>…</Collapsible>
Q10:TypeScript 里 <Collapsible /> 报 TS2786 怎么办?
这是该包 .d.ts 与本项目 React 19 类型不兼容导致的(运行时无影响)。绕法见 9.3。
小结
react-native-collapsible 是个"小但扎实"的库:两个组件、纯 JS、零运行时依赖,接进宿主只要三处改动。
这轮值得留下来的经验是:
-
动画目标是"布局属性"时,优先用布局 dump 而不是像素比对。 这个库动画的是高度,
Y(markerB) − Y(markerA)的差值就是精确的高度增量 —— 比截图比对便宜得多,而且精确到像素(实测 360 / 270 / 120 全部落在 vp 网格上)。 -
measure()的单位必须实测。 库展开时用ref.measure()取内容高度作为动画目标。如果 RNOH 的measure()返回物理像素而布局单位是 vp,展开高度会差 3 倍。本轮实测 360px(正确),但这个风险不测就不知道。 -
要验证"初始状态",不要只验证"切换后的状态"。 "挂载时
collapsed={false}"这个用例专门为它设计,一测就露出来:高度被锁在 0,因为hasKnownHeight = !measuring && (measured || collapsed)在挂载时为 false。"默认展开"是这个库的反直觉点。 -
prop 可能有隐藏前置条件。
align依赖contentHeight,而contentHeight只有测过一次高度才有值 ⇒ 未测过高度时align静默无效。验证一个 prop,要先确认它依赖的状态是否就绪。 -
“测量不到"不等于"没有效果”。 我一度以为布局 dump 不反映
transform,后来用"先展开再折叠"造出真实位移,发现 dump 是反映的。在把"测不到"写成结论之前,先造一个确定有差异的对照组。 -
包可能有多个入口。
main只导出Collapsible,Accordion在子路径 —— 写错导入时只有在类型检查阶段才会报错(TS2614),Metro 不报,直到运行时渲染才炸。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-collapsible(上游 1.6.2 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-collapsible |
| 适配 TAG | 1.6.2-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 运行时依赖 | 无(只有 react / react-native 两个 peerDependency) |
| 公开入口 | 两个:react-native-collapsible(Collapsible)与 react-native-collapsible/Accordion(Accordion) |
| 上游仓库 | https://github.com/oblador/react-native-collapsible(基线 commit c79b7c01ccb80968e9c01d61ec9bf89f2d20d4c0,MIT) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = CollapsibleTestApp) |
接入方式(纯 JS,零原生接线):
// package.json
"react-native-collapsible": "file:../react-native-collapsible"
// 或直接从适配仓库装(替代上面的 file: 方式)
"react-native-collapsible": "git+https://atomgit.com/oh-react-native/react-native-collapsible.git#1.6.2-ohos-1.0.0"
// metro.config.js —— file: 装进来是 junction,必须让 Metro 找得到源码
watchFolders: [path.resolve(__dirname, '../react-native-collapsible')],
import Collapsible from 'react-native-collapsible';
import Accordion from 'react-native-collapsible/Accordion'; // ← 子路径,必须
// 折叠容器:注意三个尺寸相关 props 要一起考虑
<Collapsible
collapsed={collapsed}
collapsedHeight={0} // 折叠时保留的高度(vp)
duration={300}
easing="easeOutCubic"
align="top" // center 已实测精确;bottom 实测与预期不符
enablePointerEvents={false} // false 时折叠区不接收触摸
renderChildrenCollapsed // false 时折叠即移除子节点
onAnimationEnd={() => {}}
>
<View style={{height: 120}}><Text>内容</Text></View>
</Collapsible>
// 手风琴:完全受控
const [active, setActive] = useState<(string | number)[]>([]);
<Accordion
sections={['第一章', '第二章']}
activeSections={active} // 事实必填
onChange={setActive} // 只给新数组,必须自己 setState
renderHeader={(section, key, isActive) => <Text>{`${section}${isActive ? '(展开)' : '(折叠)'}`}</Text>}
renderContent={(section, key) => <Text>{`第 ${key} 节`}</Text>}
duration={400}
/>
# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey CollapsibleTestApp
# 外部测量:marker 夹逼量折叠区高度
devecocli ui layout | Select-String 'MARK-A'
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3(@types/react 19.3.0) |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.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(81.77 MB) |
| 本次增量构建 | assembleHap 4 分 22 秒,HAP +49,155 字节 |
| 验证规模 | 设备侧 34 / 34 断言全部通过;折叠/展开高度外部实测 3 个区域 + 手风琴 4 步(156→516 / 276→426 / 296→566→566→296);动画中途采样 1 次;renderChildrenCollapsed 3 个状态;align 两种前置条件下共 6 组读数 |
欢迎加入 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
更多推荐



所有评论(0)