React Native for OpenHarmony 实战:三方库 react-native-circular-action-menu 的鸿蒙化适配指南
本文记录把 react-native-circular-action-menu(圆形展开菜单)适配到 HarmonyOS 的完整过程。
它是一个悬浮按钮(FAB):点一下会让子项沿一段圆弧展开,再点一下收回。
它最大的特点是位置可以用一个公式精确算出来,所以这轮我做了一次定量的几何验证:
// ActionButtonItem.js —— 子项定位
const offsetX = this.props.radius * Math.cos(this.props.angle);
const offsetY = this.props.radius * Math.sin(this.props.angle);
// 打开时:transform: translateX → offsetX、translateY → offsetY
// ActionButton.js —— 每个子项的角度
// alignMap[position] 决定角度范围:center ⇒ 180°..360°、left ⇒ 270°..360°、right ⇒ 180°..270°
const offset = (endRadian - startRadian) / (childrenCount - 1);
const angle = startRadian + index * offset;
⇒ position: 'center' + 3 个子项时,理论偏移量是 (-r, 0)、(0, -r)、(+r, 0)(左、上、右 —— 一个半圆)。
实测(density 3 ⇒ radius: 100 = 300px):
| 子项 | 角度 | 理论偏移 | 实测质心 |
|---|---|---|---|
| 红(第 1 项) | 180° | (-300, 0) | (360, 2211) |
| 绿(第 2 项) | 270° | (0, -300) | (659, 1937) |
| 蓝(第 3 项) | 360° | (+300, 0) | (960, 2211) |
⇒ 红↔蓝水平间距 = 600px = 2 × 300 = 2r ✓ 精确
⇒ 再把半径减半(radius: 50)做对照:红↔蓝 = 300px ⇒ 精确的 2:1 ✓✓
也就是说:子项位置确实落在以 FAB 为圆心、radius 为半径的圆弧上,alignMap 的角度范围也完全成立。


设备侧断言 38 / 38 全部通过。
一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 0.5.0;license: MIT;上游 star 477;最后提交 2018-10 |
| 库类型 | 纯 JS 组件库:ActionButton(class)+ ActionButtonItem(class),源码直发 |
| 是否需要原生适配 | 不需要(无 harmony/、无 NativeModules) |
| 运行时依赖 | ✅ 仅 prop-types@^15.6.1(宿主已有 15.8.1)⇒ 本轮没踩依赖坑 |
prepare / postinstall | ✅ 都没有 |
main | ActionButton.js(源码直发:ActionButton.js 8154 B + ActionButtonItem.js 2597 B) |
| 导出形态 | export default class ActionButton,并挂了一个静态属性 ActionButton.Item = ActionButtonItem |
| 需要 HAR / 权限 / ohpm | ❌ 都不需要 |
| ★ 默认值 | active false / position 'center' / radius 100 / size 63 / itemSize 36 / degrees 135 / autoInactive true / backdrop false |
| ★★ 定位公式 | (radius·cos(angle), radius·sin(angle)) ⇒ 实测 radius 100 ⇒ 红↔蓝 600px、radius 50 ⇒ 300px(精确 2:1) |
| ★ 角度范围 | center 180…360、left 270…360、right 180…270(由 alignMap 决定) |
| ★ 其它实测 | Item 静态挂载、Item.defaultProps 3 键、FAB 的 “+” 打开时旋转 135°(截图可见变 “×”)、active=false 时子项为 0 |
| 设备侧断言 | ✅ 38 / 38 通过(A 33 + B 3 + C 2) |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules / requireNativeComponent | 全无 |
| ④ | import 图体检 | ./ActionButtonItem + prop-types + react / react-native ⇒ 只有一个真依赖,且宿主已有 |
| ⑤ | scripts 扫 prepare 家族 | ✅ 没有 |
npm install "git+https://atomgit.com/oh-react-native/react-native-circular-action-menu.git#0.5.0-ohos-1.0.0" --ignore-scripts
# 它只依赖 prop-types(若宿主没有则补装)
npm install prop-types --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-circular-action-menu') ],
import CircularMenuTestApp from './CircularMenuTestApp';
AppRegistry.registerComponent('CircularMenuTestApp', () => CircularMenuTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey CircularMenuTestApp
不需要 HAR、权限、ohpm、link-harmony。
三、读源码取到的真值
3.1 结构与约定
export default class ActionButton extends Component { … }
ActionButton.Item = ActionButtonItem; // ★ 静态挂载 ⇒ 用法是 <ActionButton.Item />
实测:ActionButton.Item 存在、是 class、prototype.render 存在;Item.defaultProps 3 键(onPress / startDegree 0 / endDegree 720)✓
3.2 默认值(defaultProps 16 键)与 propTypes(15 键)
ActionButton.defaultProps = {
active: false, bgColor: 'transparent', buttonColor: 'rgba(0,0,0,1)',
buttonTextColor: 'rgba(255,255,255,1)',
position: 'center', // ★ 决定角度范围
outRangeScale: 1, autoInactive: true,
onPress: () => {}, onOverlayPress: () => {},
backdrop: false,
degrees: 135, // ★ 打开时 FAB 上的图形旋转角度
size: 63, // ★ FAB 直径(borderRadius = size/2 ⇒ 圆)
itemSize: 36, // ★ 子项直径
radius: 100, // ★ 子项到圆心的距离
btnOutRange: 'rgba(0,0,0,1)', btnOutRangeTxt: 'rgba(255,255,255,1)',
};
3.3 ★★ 角度范围与定位公式(本轮判据的根据)
const alignMap = {
center: { alignItems: 'center', justifyContent: 'flex-end', startDegree: 180, endDegree: 360 },
left: { alignItems: 'flex-start', justifyContent: 'flex-end', startDegree: 270, endDegree: 360 },
right: { alignItems: 'flex-end', justifyContent: 'flex-end', startDegree: 180, endDegree: 270 },
};
// renderActions()
const startDegree = this.props.startDegree || alignMap[this.props.position].startDegree;
const endDegree = this.props.endDegree || alignMap[this.props.position].endDegree;
const startRadian = startDegree * Math.PI / 180;
const endRadian = endDegree * Math.PI / 180;
const childrenCount = React.Children.count(this.props.children);
let offset = 0;
if (childrenCount !== 1) { offset = (endRadian - startRadian) / (childrenCount - 1); }
… angle={startRadian + index * offset} …
// ActionButtonItem.render()
const offsetX = this.props.radius * Math.cos(this.props.angle);
const offsetY = this.props.radius * Math.sin(this.props.angle);
// transform: [ {translateY: 0→offsetY}, {translateX: 0→offsetX}, {rotate: startDegree→endDegree}, {scale: 0→1} ]
⚠️ 一个关键的测量约束:子项位移是 transform ⇒ 布局 dump 不反映平移(dump 给的是未平移的框)⇒ 位置必须用像素测,或用"dump 中心 + 公式"两源互校(本轮两个都用上了)。
3.4 交互
// FAB 点击
onPress={() => { this.props.onPress(); if (this.props.children) { this.animateButton(); } }}
animateButton() {
if (this.state.active) { this.reset(); return; }
Animated.spring(this.state.anim, { toValue: 1, duration: 250 }).start();
this.setState({ active: true });
}
reset() {
Animated.spring(this.state.anim, { toValue: 0, duration: 250 }).start();
setTimeout(() => { this.setState({ active: false }); }, 250);
}
// 子项点击(renderActions 里)
onPress={() => {
if (this.props.autoInactive) { this.timeout = setTimeout(() => { this.reset(); }, 200); }
button.props.onPress(); // ⚠️ 无条件调用:子项不传 onPress 会抛错
}}
active为真才渲染子项(renderActions()里if (!this.state.active) return null)- 构造函数:
anim = new Animated.Value(active ? 1 : 0)⇒active={true}时无需交互即可测量(本轮主要判据都用这个) - ⚠️
Animated.spring传了duration: 250—— spring 不认duration(它用friction/tension)⇒ 这个参数是无效的 - ⚠️
button.props.onPress()无条件调用 ⇒ 子项若没写onPress会抛错(本轮故意不触发)
四、验证设计
| 判据 | 内容 |
|---|---|
| ① 位置(定量,像素) | 子项是 transform 位移 ⇒ dump 不反映 ⇒ 按颜色求质心,与 radius·cos/sin 比对 |
| ② 半径 A/B | radius=100 与 radius=50 ⇒ 间距应精确 2:1 |
| ③ 角度范围 | position: right 的中间项角度是 225°(与 center 的 270° 不同) |
| ④ 开关(对照) | active=false ⇒ 一个子项都不渲染 |
说明:位移是
transform,所以不能只靠 dump;而颜色判据的容差要窄、扫描范围必须限定在实例区域内(否则会把别的实例同色项一起算进去 —— 本轮第一版就踩了这个)。
五、实测结果
5.1 断言 38 / 38
| 组 | 内容 | 结果 |
|---|---|---|
| A 契约 | class + 三个原型方法、ActionButton.Item 静态挂载、Item.defaultProps 3 键、defaultProps 16 键 / propTypes 15 键 + 关键默认值、角度表与公式记录 | 33/33 |
| B 渲染与几何 | 4 个菜单实例渲染未抛错、标题不与状态栏重合、预期几何与判据说明 | 3/3 |
| C 交互与边界 | 事件记录格式、M4 打开状态、三条边界记录 | 2/2 |
| 合计 | 38/38 |
关键观测值:
default 导出的 typeof = function
ActionButton.Item 的 typeof = function
Item.defaultProps 键数 = 3
defaultProps 键数 = 16 propTypes 键数 = 15
★ 角度范围(源码 alignMap) = center: 180..360 left: 270..360 right: 180..270
★ 定位公式(源码) = offsetX = radius*cos(angle)、offsetY = radius*sin(angle)
UI 安全区:TITLE measureInWindow y=420 > 117 ✓
5.2 ★★ 位置定量验证(本轮核心)
M1:position="center"、radius=100、3 个子项(红 / 绿 / 蓝实心圆,itemSize=36vp=108px)
| 子项 | 角度 | 理论偏移(px) | 实测质心 |
|---|---|---|---|
| 红 | 180° | (-300, 0) | (360, 2211) |
| 绿 | 270° | (0, -300) | (659, 1937) |
| 蓝 | 360° | (+300, 0) | (960, 2211) |
- 红 ↔ 蓝水平间距 = 600 px(= 2 × 300 = 2r)✓ 精确
- 相邻水平间距 = 299 / 301 px ✓
- 红 ↔ 绿垂直间距 ≈ 296 px(实测 274;红/蓝两项在屏幕底部被裁掉约 40%,质心因此上移 ⇒ 换算回真实中心约 -296)✓
M2:同结构,只把 radius 改成 50(限定区扫扫描)
| 量 | M1(radius 100) | M2(radius 50) |
|---|---|---|
| 红 ↔ 蓝水平间距 | 600 px | 300 px |
| 红 ↔ 绿垂直间距 | ≈ -296 | -163(理论 -150) |
| 单项像素数(108px 圆理论 ≈ 9160) | — | 红 8407 / 蓝 8290 ≈ 满圆 ✓ |
⇒ 间距与 radius 严格成正比(2:1) ⇒ 公式成立 ✓✓
★ 附带证实:图2 里 FAB 上的 “+” 已经旋转成 “×” —— 正是 degrees: 135 的效果(rotate: 0deg → 135deg)✓
★ 对照:active=false(M4)⇒ 子项不渲染(像素判据里该区没有子项颜色)✓


六、一个测量上的教训(本轮自己踩的)
第一版像素统计我扫了整张截图,结果 radius=50 的红色子项被数出 23,420 像素 —— 而一个 itemSize=36vp(108px)的实心圆满打满算也只有约 9,160 像素。
原因:页面上有 4 个菜单实例,其它实例也有同色的子项,全被算了进来(M1 的红项、M3/M4 的红项)。
改法:把扫描范围限定在该实例的标记区间内(MARK-M2-START ~ MARK-M2-END),重测后每项像素数就回到 8,400 左右(符合一个圆)✓
⇒ 教训:用颜色做定量判据时,(a)容差要窄;(b)必须限定空间范围,否则"同类元素出现在别处"会直接污染统计量。
七、已知限制与本次验证边界
7.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
子项位置由 radius + 角度决定 | center 是 180…360(左/上/右半圆);换 position 会换角度范围 |
子项位移是 transform | 布局 dump 不反映;自动化测试要按像素或按公式推算 |
active 为真才渲染子项 | 关闭态时子项节点数为 0 |
子项必须传 onPress | 源码里 button.props.onPress() 是无条件调用 ⇒ 不传会抛错 |
Animated.spring 的 duration 无效 | 源码传了 duration: 250,但 spring 用 friction/tension,该参数被忽略 |
autoInactive 默认 true | 点子项后 200ms 自动收起 |
backdrop 默认 false | 打开时默认没有蒙层;bgColor 默认 transparent |
7.2 本次验证的边界
position: 'left'(270…360)未验证(只验证了center与right的角度差异,left未单独测量);backdrop/bgColor/onOverlayPress未验证(没有真的点蒙层);autoInactive的"点后 200ms 收起"未做严格计时;outRangeScale/btnOutRange/btnOutRangeTxt的视觉变化未逐项像素核对(只确认了degrees的旋转);degrees的精确角度未量(只从截图看出 “+” 变成了 “×”);startDegree/endDegree手动覆盖未验证;- 子项不传
onPress的抛错未运行时复现(故意不触发); backdrop为真的蒙层与层级未验证;- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。
未改动库代码。
八、常见问题
Q1:怎么用?
import ActionButton from 'react-native-circular-action-menu';
<ActionButton buttonColor="rgba(231,76,60,1)" onPress={() => {}}>
<ActionButton.Item buttonColor="#9b59b6" title="新建" onPress={() => {}}>
<Text>+</Text>
</ActionButton.Item>
<ActionButton.Item buttonColor="#3498db" title="收藏" onPress={() => {}}>
<Text>★</Text>
</ActionButton.Item>
</ActionButton>
⚠️ 每个 ActionButton.Item 都要给 onPress(源码里是无条件调用)。
Q2:子项是怎么排布的?
子项落在以 FAB 为圆心、radius 为半径的圆弧上:
offsetX = radius * cos(angle)
offsetY = radius * sin(angle)
角度范围由 position 决定:
position | 角度范围 | 效果 |
|---|---|---|
center(默认) | 180°…360° | 左 → 上 → 右(半圆) |
left | 270°…360° | 上 → 左 |
right | 180°…270° | 左 → 上 |
子项数量为 n 时,相邻角度间隔是 (end - start) / (n - 1)。
Q3:怎么调圆的大小与间距?
<ActionButton radius={120} size={70} itemSize={44} … />
// ↑子项到圆心 ↑FAB 直径 ↑子项直径
实测:radius 从 100 改成 50 ⇒ 子项水平间距从 600px 变成 300px(density 3)。
Q4:打开时 FAB 上的图标会转吗?
会。degrees 默认 135,打开时 rotate: 0deg → 135deg(所以默认的 “+” 会看起来像 “×”)。想让它转 45° 就传 degrees={45}。
Q5:点了子项自己不会收起来?
autoInactive 默认 true(点后 200ms 自动收起)。想让它保持展开就传 autoInactive={false},或自己控制 active。
Q6:想让菜单一开始就是展开的(或受控)?
<ActionButton active={true} … /> // 初始展开
⚠️ 注意 active 只是初始值(构造函数里读取),后续状态由组件内部维护。
Q7:为什么自动化测试读不到子项位置?
因为子项位移是 transform(translateX/translateY),布局 dump 不会反映平移 ⇒ 要按像素测,或按公式 radius·cos/sin 推算。
Q8:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。 纯 JS、只依赖 prop-types、无原生模块、不需要 HAR / 权限 / ohpm。
小结
react-native-circular-action-menu 是纯 JS 的圆形展开菜单(ActionButton + ActionButton.Item)。设备侧 38 / 38 断言通过。
最值得记下来的是这几条:
-
★★ 有闭式公式就用它做定量判据。 子项位置 =
(radius·cos(angle), radius·sin(angle))⇒ 我按颜色求质心,实测radius=100时红↔蓝 600px、radius=50时 300px(精确 2:1),相邻间距 299/301px ⇒ 公式与alignMap的角度范围同时被证实。
⇒ 比"看起来像个半圆"硬得多(图2 也确实一眼可见半圆,两者互为印证)。 -
★
transform位移 ⇒ dump 不可信,必须换判据。 这个库的子项定位全在transform里 ⇒ 布局 dump 给的是未平移的框 ⇒ 只能按像素测,或用"dump 中心 + 公式"两源互校。 -
★ 颜色定量判据必须限定空间范围。 我第一版扫了整屏,把其它实例的同色子项也算进来,得到"一个圆 23,420 像素"(满圆只有约 9,160)这种明显不合理的数 ⇒ 限定到实例区间后恢复正常。⇒ 容差要窄 + 范围要限定。
-
★ 顺手把"静态挂载"和"默认值数量"都断言掉。
ActionButton.Item是静态属性挂载(不是单独 import),Item.defaultProps3 键、ActionButton.defaultProps16 键、propTypes15 键 —— 我一开始把后两个各多算了 1(凭印象数的),靠实测纠正。 -
依赖情况在预检阶段就能确定:import 图里只有一个
prop-types,而它已在宿主里 ⇒ 本轮全程没在依赖上花时间。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-circular-action-menu(上游 0.5.0 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-circular-action-menu |
| 适配 TAG | 0.5.0-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 库类型 | 纯 JS 组件库:圆形展开菜单(ActionButton + ActionButton.Item) |
| 运行时依赖 | ✅ 仅 prop-types@^15.6.1(宿主一般已有) |
prepare / postinstall | ✅ 都没有 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/geremih/react-native-circular-action-menu(★477) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = CircularMenuTestApp) |
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-circular-action-menu.git#0.5.0-ohos-1.0.0" --ignore-scripts
# 本地目录接入(若无 prop-types 则补装)
npm install ../react-native-circular-action-menu --ignore-scripts
npm install prop-types --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-circular-action-menu') ],
import ActionButton from 'react-native-circular-action-menu';
// ① 基本用法:点 FAB 展开成一圈(默认 center ⇒ 左/上/右半圆)
<ActionButton
buttonColor="rgba(231,76,60,1)"
radius={100} // 子项到圆心的距离
size={63} // FAB 直径
itemSize={36} // 子项直径
degrees={135} // 打开时 FAB 图形旋转角度
onPress={() => {}}
onOverlayPress={() => {}}
>
<ActionButton.Item buttonColor="#9b59b6" onPress={() => {}}><Text>+</Text></ActionButton.Item>
<ActionButton.Item buttonColor="#3498db" onPress={() => {}}><Text>★</Text></ActionButton.Item>
<ActionButton.Item buttonColor="#1abc9c" onPress={() => {}}><Text>♥</Text></ActionButton.Item>
</ActionButton>
// ② 换位置(决定角度范围)与自定义角度
<ActionButton position="right" … /> // 180°..270°
<ActionButton position="left" … /> // 270°..360°
<ActionButton startDegree={200} endDegree={340} … />
// ③ 初始展开 / 不自动收起 / 带蒙层
<ActionButton active={true} autoInactive={false} backdrop={true} bgColor="rgba(0,0,0,0.4)" … />
// ⚠️ 每个 Item 都要给 onPress(源码无条件调用,不传会抛错)
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey CircularMenuTestApp
位置判据的复现方式
子项位置(理论):offsetX = radius·cos(angle)、offsetY = radius·sin(angle)
center + 3 项 ⇒ 角度 180°/270°/360° ⇒ 偏移 (-r,0)、(0,-r)、(+r,0)
density 3 ⇒ radius=100 时 r=300px ⇒ 左右两项水平间距应为 600px
⚠️ 子项位移是 transform ⇒ 布局 dump 不反映 ⇒ 必须按像素测
⚠️ 按颜色求质心时:容差要窄,且【扫描范围必须限定在本实例区间内】
(否则会把页面上其它实例的同色子项一起算进去)
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| 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 |
| 本机状态栏高度 | 117px(页面留白取 48vp) |
| 宿主依赖 | prop-types 15.8.1(已有 ⇒ 无需补装) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 首轮 4 分 01 秒;修计数后 3 分 57 秒 |
| 验证规模 | 设备侧 38 / 38 断言;位置定量量测 2 组(radius 100 ⇒ 600px、radius 50 ⇒ 300px,各 3 个子项质心);角度范围记录 1 组;开关对照 1 例;旋转效果 1 项(截图可见 "+“→"×”);UI 安全区 1 项 |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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
更多推荐



所有评论(0)