云计算百科
云计算领域专业知识百科平台

React Native for OpenHarmony 实战:三方库 react-native-circular-action-menu 的鸿蒙化适配指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

本文记录把 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.defaultProps 3 键、ActionButton.defaultProps 16 键、propTypes 15 键 —— 我一开始把后两个各多算了 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)
    未经允许不得转载:网硕互联帮助中心 » React Native for OpenHarmony 实战:三方库 react-native-circular-action-menu 的鸿蒙化适配指南
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!