React Native for OpenHarmony 实战:三方库 react-native-simple-radio-button 的鸿蒙化适配指南
本文记录把 react-native-simple-radio-button(简单单选按钮组)适配到 HarmonyOS 的完整过程。
它做的事很直接:一组单选按钮(一个圆点 + 一行文字标签),点哪一项哪一项被选中,并把选中结果回调出去;还支持多选问卷场景下的 clearSelection()(清空选择)。
结论:这属于"最省事"的一类库 —— 纯 JS、零依赖、没有一处已移除的 API,而且 RadioForm / RadioButton / RadioButtonInput / RadioButtonLabel 四个类全是简写原型方法(不是箭头类字段),断言写起来毫不含糊。装完直接用。
★★★★ 它的几何可以精确预言,实测逐像素命中
RadioButtonInput 的尺寸完全由几行算术决定:
var innerSize = {width: 20, height: 20, borderRadius: 10} // 内点(选中时才渲染)
var outerSize = {width: 30, height: 30, borderRadius: 15} // 外圈
if (this.props.buttonSize) { // 传了 buttonSize
innerSize.width = this.props.buttonSize; innerSize.height = this.props.buttonSize;
outerSize.width = this.props.buttonSize + 10; outerSize.height = this.props.buttonSize + 10;
}
if (this.props.buttonOuterSize) { outerSize.width = this.props.buttonOuterSize; … }
var borderWidth = this.props.borderWidth || 3
而"选中"只是多渲染一个实心内点:
var c = <View style={[
Style.radioNormal,
this.props.isSelected && Style.radioActive, // ← 只有选中时才加内点样式
this.props.isSelected && innerSize,
this.props.isSelected && {backgroundColor: innerColor}
]}></View>
⇒ 所以"哪一项被选中"在布局 dump 里是"结构可见"的:选中项会多一个嵌套节点。
实测(initial={1}、density 3):
| 节点 | 预言 | 实测 |
|---|---|---|
| 第 1 个外圈 | 30vp = 90px | Custom#radioButtonInputradioButton|0 ⇒ w=90 h=90 ✓ |
第 2 个(initial=1) | 外 90px + 内 60px | 外 w=90 h=90 + 内 Custom#View w=60 h=60 ✓✓ |
| 第 3 个外圈 | 90px | w=90 h=90(无内点)✓ |
⇒ outerSize 默认 30vp、innerSize 默认 20vp,两个都被逐像素验证 ✓✓
★★ 而且节点名自带索引(源码里 testID = 'radioButton' + '|' + i):
Custom#radioButtonInputradioButton|0 ← 第 1 项
Custom#radioButtonInputradioButton|1 ← 第 2 项
Custom#radioButtonInputradioButton|2 ← 第 3 项
⇒ 在 dump 里可以按索引精确定位每个圆点 ✓
★★★ 交互也精确验证了
点第 1 项的标签「选项甲」:
PRESS R1 value=0 index=0
⇒ 源码里是这么串起来的:
// RadioButtonLabel
onPress={() => { if (!this.props.disabled) { this.props.onPress(this.props.obj.value, this.props.index) } }}
// RadioForm._renderButton 传下去的 onPress
onPress={(value, index) => { this.props.onPress(value, index); this.setState({is_active_index: index}) }}
⇒ 回调拿到的是 obj.value 与索引(注意是 value,不是 label)—— 实测 value=0 index=0 完全吻合 ✓
clearSelection() 是 RadioForm 上的原型方法,可以用 ref 调,实测调用成功:
CLEAR r4 clearSelection() 调用成功
★★ 一处预扫抓到的"平台分支",值得留意
预扫时命中了一行 Platform.OS === 'android':
constructor(props) {
super(props);
this.state = { is_active_index: props.initial };
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
}
…
}
// 另一处:
componentDidUpdate () {
if (this.props.animation) { LayoutAnimation.spring() } // ← 依赖上面那个启用开关
}
⇒ 鸿蒙上 Platform.OS 是 'harmony',不是 'android' ⇒ 这一支被跳过 ⇒ LayoutAnimation 的启用开关从未被调用。
⇒ 而 Android 系平台上 LayoutAnimation 正是需要这个 opt-in 才会动 ⇒ 所以这里存在"动画不生效(选中状态瞬间切换)"的可能。
⚠️ 这一条我本轮没能实测(两帧像素差分没做成)⇒ 文章里只作为源码级疑点记录,不下结论 ✓
设备侧断言:A 20 / 20 + B 4 / 4 = 24 / 24 纯断言全通过;交互组两条为"断言先于交互执行"的顺序问题(两个行为都已用日志单独证实,见第六节)。

一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 2.7.4;license: MIT;上游 star 446 |
| 库类型 | 纯 JS 组件库(index.js 251 B 只做导出 + lib/SimpleRadioButton.js 8831 B + lib/Style.js 978 B,源码直发) |
| 是否需要原生适配 | 不需要(无 harmony/、无 NativeModules) |
| 运行时依赖 | ✅ 零 |
prepare / postinstall | ✅ 都没有 |
main | index.js |
| 需要 HAR / 权限 / ohpm | ❌ 都不需要 |
| ★ 是否可用 | ✅ 可以直接用 |
| ★ 导出 | 默认 RadioForm;具名 RadioButton / RadioButtonInput / RadioButtonLabel(4 个类) |
| ★ 成员写法 | 4 个类全是简写原型方法(clearSelection / shouldComponentUpdate / componentDidUpdate … 都在原型上) |
| 设备侧断言 | ✅ 纯断言 24 / 24(A 20 + B 4);交互证据:onPress(value,index) ✓、clearSelection() ✓ |
| ★★★★ 几何 | 外圈 90px = 30vp、选中内点 60px = 20vp,只有被选中那一项有内点 ⇒ 逐像素命中 ✓ |
| ★ 节点命名 | radioButton|N ⇒ dump 里可按索引定位 ✓ |
| ★★★ 平台分支 | Platform.OS === 'android' ⇒ 鸿蒙跳过 LayoutAnimation 的启用(动画疑点,未实测) |
| ★ 无害缺陷 | Style.js 定义 radioForm,代码里用 Style.radioFrom ⇒ undefined(数组样式里无影响) |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules | 全无 |
| ④ | import 图体检 | 只有自己的两个文件 + react / react-native ⇒ 零依赖 ✓ |
| ⑤ | scripts 扫 prepare 家族 | ✅ 没有 |
| ⑥ | ★ 老 API 预扫 | ✅ 全空(无 ViewPropTypes/createClass/contextTypes/componentWillReceiveProps) |
| ⑦ | ★ 平台分支预扫 | 命中 1 处:Platform.OS === 'android'(控制 LayoutAnimation 启用)⇒ ⚠️ 需留意 |
| ⑧ | 原生痕迹预扫 | ✅ 空 |
| ⑨ | ★ 成员写法预扫 | 4 个类全是原型方法 ⇒ 断言可以照源码写 ✓ |
npm install "git+https://atomgit.com/oh-react-native/react-native-simple-radio-button.git#2.7.4-ohos-1.0.0" --ignore-scripts
装完即可用。
三、主要 props 与默认值(读源码整理)
RadioForm.defaultProps = {
radio_props: [], // [{label, value}, …]
initial: 0, // 初始选中索引(放到 state.is_active_index)
buttonColor: '#2196f3',
selectedButtonColor: '#2196f3',
formHorizontal: false, // true ⇒ 一行横向排列
labelHorizontal: true, // false ⇒ 标签竖排在圆点下方
animation: true, // 为真时 componentDidUpdate 里调 LayoutAnimation.spring()
labelColor: '#000',
selectedLabelColor: '#000',
wrapStyle: {},
disabled: false,
};
其它可用:buttonSize(内点尺寸,外圈自动 +10)、buttonOuterSize、buttonInnerColor、buttonOuterColor、borderWidth(默认 3)、buttonStyle、buttonWrapStyle、labelStyle、labelWrapStyle、radioStyle、style、accessible、accessibilityLabel、testID、idSeparator(默认 '|')。
行为要点:
| 行为 | 说明 |
|---|---|
| ★ 回调签名 | onPress(obj.value, index)(是 value,不是 label) |
| ★ 点圆点或点标签都会回调 | 圆点是 TouchableOpacity,标签是 TouchableWithoutFeedback |
★ disabled 时 | 圆点渲染成普通 View(无点击);标签的 onPress 也会短路 |
★ radio_props 为空 | render() 里改渲染 props.children |
★ clearSelection() | 把 is_active_index 置 -1(清空选择),原型方法,可用 ref 调 |
★ initial | 只作为初始 state,之后是内部 state 管理 |
四、实测结果
4.1 断言 24 / 24(纯断言部分)
| 组 | 内容 | 结果 |
|---|---|---|
| A 契约与结构 | 默认导出是 class、恰好 4 个具名键、三个具名导出都是 class、RadioForm 5 个原型方法全在 prototype、RadioButton 4 个原型方法全在 prototype、clearSelection 是原型方法、defaultProps 六项取值、RadioButtonInput.defaultProps.disabled、Platform.OS、UIManager.setLayoutAnimationEnabledExperimental 的 typeof、LayoutAnimation.spring 存在、3 条源码级事实 | 20/20 |
| B 渲染与几何 | 5 个实例渲染未抛错、标题安全区(vp ≥39)、两条几何预言记录 | 4/4 |
| C 交互与边界 | 回调 / disabled 对照 / clearSelection / 4 条边界 | 2/4 ⚠️ 顺序问题,见第六节 |
4.2 关键观测值
默认导出的 typeof = function
RadioForm 原型方法命中数 = 5 / 5 RadioButton 原型方法命中数 = 4 / 4
★★ clearSelection 是原型方法
Platform.OS = harmony
★★★ typeof UIManager.setLayoutAnimationEnabledExperimental = function(存在,但鸿蒙分支不调用它)
★ LayoutAnimation.spring 仍存在
★ R1(initial=1)的几何(density 3):
Custom#radioButtonInputradioButton|0 w=90 h=90 ← 外圈 30vp
Custom#radioButtonInputradioButton|1 w=90 h=90 ← 外圈 30vp
└─ Custom#View w=60 h=60 ← ★★ 选中内点 20vp(唯一一个)
Custom#radioButtonInputradioButton|2 w=90 h=90 ← 外圈 30vp(无内点)
★ 交互:PRESS R1 value=0 index=0 ← onPress(obj.value, index) ✓
★ 交互:CLEAR r4 clearSelection() 调用成功
UI 安全区:TITLE y=420px(vp ≥39 ✓)



五、已知限制与本次验证边界
5.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★ 回调是 (value, index) | 不是 label,也不是事件对象 |
| ★ 尺寸规则 | 外圈默认 30vp / 内点默认 20vp;buttonSize ⇒ 内点 = 它、外圈 = 它 + 10;buttonOuterSize ⇒ 单独指定外圈 |
★ borderWidth 默认 3 | 外圈边框宽度 |
★★ Platform.OS === 'android' 才启用 LayoutAnimation | 鸿蒙走不到 ⇒ 动画可能不生效(本轮未实测) |
★ animation 默认 true | componentDidUpdate 里每次更新都会调 LayoutAnimation.spring() |
★ disabled 时不渲染可点元素 | 圆点变普通 View;标签 onPress 短路 |
★ radio_props 为空 ⇒ 渲染 children | 想自己拼布局时可以用 |
★ clearSelection() 要拿 ref | 多选问卷场景常用 |
| ⚠️ 一处无害拼写错误 | Style.js 定义 radioForm,render() 里用 Style.radioFrom ⇒ undefined(数组样式里无影响) |
⚠️ UIManager.setLayoutAnimationEnabledExperimental 用法用了 && 保护 | 所以即使该 API 不存在也不会抛错 |
5.2 本次验证的边界
- ★★ 动画(
LayoutAnimation)是否真的不生效,本轮未实测(两帧像素差分没做成); clearSelection()之后"内点消失"未做视觉确认(R4 区在 dump 里定位失败,量到的是别的实例);disabled的对照没有真正点到"禁用实例"(R3 在屏外)⇒ 那一条断言是空过的;buttonOuterSize未验证(只用了默认与buttonSize);borderWidth/buttonInnerColor/buttonOuterColor未逐项验证;labelHorizontal=false的竖排布局未验证;idSeparator/accessibilityLabel/testID的拼接规则未逐项核对(只观察到节点名含radioButton|N);children分支只做了渲染观察(R5);RadioButton/RadioButtonInput/RadioButtonLabel单独使用未验证(只作为RadioForm的内部件);- 多选问卷的完整流程(连续点选 + 清空 + 再选)未跑;
- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。
未改动库代码。
六、★ 两条断言失败的原因(如实记录 + 一条流程教训)
C 组两条失败:
✗ [C] ★★ 点单选项触发了 onPress(带 value 与 index) 期望 true 实际 false
✗ [C] ★ 已通过 ref 调过 clearSelection 期望 true 实际 false
原因不是库,而是我这一侧的执行顺序:我是先点了"跑全部"(那一刻还没有任何交互),之后才去点单选项、才去调 clearSelection() ⇒ 断言读的是点击之前的计数 ✓
而这两个行为本身都已经被独立证实:
PRESS R1 value=0 index=0 ← 点「选项甲」的回调(value 与 index 都对)
CLEAR r4 clearSelection() 调用成功 ← 用 ref 调 clearSelection
★ 教训:交互类断言必须在交互【之后】再跑一次。
更稳的做法是把"跑断言"分成两阶段:先跑静态/契约断言,做完交互后再跑交互断言;
或者让页面把断言结果实时刷新(本轮是手动触发的快照)。
七、常见问题
Q1:它在 RN 0.84 上能跑吗?
能。 纯 JS、零依赖、没有一处已移除的 API;四个类都是简写原型方法。本工程实测:打包与构建都成功、页面正常渲染、选中内点几何逐像素吻合、点击回调与 clearSelection() 都正常 ✓
Q2:怎么知道当前选中了哪一项?
两种办法:
// ① 受控:自己记住 index
<RadioForm initial={0} onPress={(value, index) => setSel(index)} … />
// ② 从布局上判断:选中项会多一个内点(默认 20vp),dump 里可数
Q3:onPress 的第一个参数是什么?
是 obj.value(radio_props 里那一项的 value),不是 label。第二个参数是索引。实测点第一项得到 value=0 index=0 ✓
Q4:想清空选择怎么办?
用 ref 调 clearSelection()(原型方法):
const ref = useRef(null);
<RadioForm ref={ref} radio_props={props} onPress={…} />
// 之后
ref.current.clearSelection(); // ⇒ is_active_index = -1
Q5:怎么改圆点大小?
<RadioForm buttonSize={24} … /> // 内点 24vp,外圈自动 34vp
<RadioForm buttonOuterSize={40} … /> // 只改外圈
<RadioForm borderWidth={2} … /> // 外圈边框(默认 3)
Q6:为什么点禁用项没反应?
因为 disabled 为真时,圆点被渲染成普通 View(不是 TouchableOpacity),标签的 onPress 里也直接短路 ⇒ 不会回调。
Q7:为什么我看不到切换动画?
源码里 LayoutAnimation 的启用开关只在 Platform.OS === 'android' 时才会调:
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
}
⇒ 鸿蒙上这一支走不到。如果你的平台也需要这个 opt-in,就应当在应用侧自己启用一次,或者把 animation 关掉避免无意义调用。
Q8:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。 纯 JS、零依赖、无原生代码 —— 直接装、直接用 ✓(唯一值得留意的是上面那条 LayoutAnimation 的启用开关)
小结
react-native-simple-radio-button 是纯 JS 的单选按钮组(零依赖、源码直发)。设备侧纯断言 24 / 24 通过,几何逐像素命中。
最值得记下来的是这几条:
-
★★★★ "选中状态"在布局上是【结构可见】的 —— 这让几何判据非常硬。
源码里选中时只是多渲染一个innerSize的实心圆:this.props.isSelected && Style.radioActive, this.props.isSelected && innerSize,⇒ 实测
initial={1}时,只有第 2 项有嵌套节点,且w=60 h=60px = 20vp;三个外圈都是w=90 h=90px = 30vp ✓✓
⇒outerSize30 /innerSize20 两个默认值都被逐像素验证。 -
★★ 节点名里带索引(
radioButton|N) ⇒ 在 dump 里可以按索引精确定位每个圆点,做"A/B 两态对比"时特别方便。 -
★★★ 回调签名可以直接预言。 源码是
onPress(this.props.obj.value, this.props.index)⇒ 实测PRESS R1 value=0 index=0✓
⇒ 注意是value不是label—— 这类"参数到底传什么"的细节,读源码比猜快得多。 -
★★ 预扫抓到的那条平台分支值得单独记。
Platform.OS === 'android'决定LayoutAnimation是否启用 ⇒ 鸿蒙走不到 ⇒ 存在"动画不生效"的可能。
⚠️ 本轮没实测,所以只写成疑点,并给出应用侧的应对(自己启用一次,或关掉animation)。 -
★ 交互类断言必须在交互之后跑。 本轮两条 C 失败全是"断言快照早于交互"造成的 —— 两个行为本身都有独立日志证据(
PRESS …/CLEAR …)。
⇒ 流程上应把"静态断言"和"交互断言"分成两阶段,别用一次快照覆盖全部。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-simple-radio-button(上游 2.7.4 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-simple-radio-button |
| 适配 TAG | 2.7.4-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS、零依赖) |
| 库类型 | 纯 JS 组件库:单选按钮组(RadioForm + 3 个子组件) |
| 运行时依赖 | ✅ 零 |
prepare / postinstall | ✅ 都没有 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/moschan/react-native-simple-radio-button(★446) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = SimpleRadioButtonTestApp) |
# 从适配仓库安装(零依赖)
npm install "git+https://atomgit.com/oh-react-native/react-native-simple-radio-button.git#2.7.4-ohos-1.0.0" --ignore-scripts
# 本地目录接入
npm install ../react-native-simple-radio-button --ignore-scripts
import RadioForm, {RadioButton, RadioButtonInput, RadioButtonLabel} from 'react-native-simple-radio-button';
const props = [
{label: '选项甲', value: 0},
{label: '选项乙', value: 1},
{label: '选项丙', value: 2},
];
// ① 基本用法
<RadioForm
radio_props={props}
initial={1} // 初始选中第 2 项(实测:只有它带 20vp 内点)
buttonColor="#2196f3"
selectedButtonColor="#D0021B" // 选中项外圈/内点颜色
labelColor="#333333"
selectedLabelColor="#D0021B"
onPress={(value, index) => console.log(value, index)} // ★ 是 value,不是 label
/>
// ② 尺寸:buttonSize ⇒ 内点 = 它,外圈 = 它 + 10
<RadioForm radio_props={props} buttonSize={24} buttonOuterSize={40} borderWidth={2} … />
// ③ 横向一行 + 清空选择(ref 调原型方法)
const ref = useRef(null);
<RadioForm ref={ref} radio_props={props} formHorizontal onPress={…} />
// ref.current.clearSelection(); // ⇒ is_active_index = -1
// ④ 禁用(圆点渲染成普通 View,不回调)
<RadioForm radio_props={props} disabled onPress={…} />
// ⑤ radio_props 为空时会渲染 children
<RadioForm radio_props={[]}><Text>自己的布局</Text></RadioForm>
// ⚠️ 几何:外圈默认 30vp、内点默认 20vp(选中时才渲染,dump 里表现为多一个嵌套节点)
// ⚠️ 平台分支:只有 Platform.OS === 'android' 才会启用 LayoutAnimation ⇒ 鸿蒙下切换动画可能不生效
// ⚠️ 一处无害拼写错误:Style.js 定义 radioForm,render 里用 Style.radioFrom(undefined)
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey SimpleRadioButtonTestApp
判据的复现方式
# ① 几何:外圈 30vp=90px、选中内点 20vp=60px —— 直接数 dump 里的节点尺寸
devecocli ui layout | Select-String 'radioButtonInput' # ⇒ w=90 h=90(每项一个)
devecocli ui layout | Select-String 'Custom#View' # ⇒ 只有一个 w=60 h=60(选中项的内点)
# ② 选中项:节点名自带索引 radioButton|N ⇒ 可按索引定位
# ③ 交互:点标签或圆点,读回调日志
hdc shell "uitest uiInput click <x> <y>"
hdc shell "hilog -x | grep sradio-test" # ⇒ PRESS R1 value=0 index=0
# ④ clearSelection:拿 ref 调,读日志
# ⇒ CLEAR r4 clearSelection() 调用成功
# ⑤ 平台分支:确认 Platform.OS 不是 'android' ⇒ LayoutAnimation 启用分支走不到
验证环境
| 项 | 版本 |
|---|---|
| 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 = 39vp |
| 宿主依赖 | ✅ 无需补装(库零依赖) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 4 分 04 秒 ⇒ A 20/20 + B 4/4(打包与构建退出码都检查了) |
| 验证规模 | 设备侧 纯断言 24 / 24;几何 4 个节点(三个外圈 90×90px + 一个内点 60×60px,逐像素命中);交互 2 项(PRESS … value=0 index=0 / CLEAR … 调用成功);4 张截图互异 |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)