React Native for OpenHarmony 实战:三方库 react-native-simple-radio-button 的鸿蒙化适配指南
本文记录把react-native-simple-radio-button简单单选按钮组适配到 HarmonyOS 的完整过程。它做的事很直接一组单选按钮一个圆点 一行文字标签点哪一项哪一项被选中并把选中结果回调出去还支持多选问卷场景下的clearSelection()清空选择。结论这属于最省事的一类库—— 纯 JS、零依赖、没有一处已移除的 API而且RadioForm/RadioButton/RadioButtonInput/RadioButtonLabel四个类全是简写原型方法不是箭头类字段断言写起来毫不含糊。装完直接用。★★★★ 它的几何可以精确预言实测逐像素命中RadioButtonInput的尺寸完全由几行算术决定varinnerSize{width:20,height:20,borderRadius:10}// 内点选中时才渲染varouterSize{width:30,height:30,borderRadius:15}// 外圈if(this.props.buttonSize){// 传了 buttonSizeinnerSize.widththis.props.buttonSize;innerSize.heightthis.props.buttonSize;outerSize.widththis.props.buttonSize10;outerSize.heightthis.props.buttonSize10;}if(this.props.buttonOuterSize){outerSize.widththis.props.buttonOuterSize;…}varborderWidththis.props.borderWidth||3而选中只是多渲染一个实心内点varcView style{[Style.radioNormal,this.props.isSelectedStyle.radioActive,// ← 只有选中时才加内点样式this.props.isSelectedinnerSize,this.props.isSelected{backgroundColor:innerColor}]}/View⇒所以哪一项被选中在布局 dump 里是结构可见的选中项会多一个嵌套节点。实测initial{1}、density 3节点预言实测第 1 个外圈30vp 90pxCustom#radioButtonInputradioButton|0⇒w90 h90✓第 2 个initial1外 90px 内 60px外w90 h90 内Custom#View w60 h60✓✓第 3 个外圈90pxw90 h90无内点✓⇒outerSize默认 30vp、innerSize默认 20vp两个都被逐像素验证✓✓★★ 而且节点名自带索引源码里testIDradioButton | iCustom#radioButtonInputradioButton|0 ← 第 1 项 Custom#radioButtonInputradioButton|1 ← 第 2 项 Custom#radioButtonInputradioButton|2 ← 第 3 项⇒ 在 dump 里可以按索引精确定位每个圆点 ✓★★★ 交互也精确验证了点第 1 项的标签「选项甲」PRESS R1 value0 index0⇒ 源码里是这么串起来的// RadioButtonLabelonPress{(){if(!this.props.disabled){this.props.onPress(this.props.obj.value,this.props.index)}}}// RadioForm._renderButton 传下去的 onPressonPress{(value,index){this.props.onPress(value,index);this.setState({is_active_index:index})}}⇒回调拿到的是obj.value与索引注意是value不是label—— 实测value0 index0完全吻合✓clearSelection()是RadioForm上的原型方法可以用ref调实测调用成功CLEAR r4 clearSelection() 调用成功★★ 一处预扫抓到的平台分支值得留意预扫时命中了一行Platform.OS androidconstructor(props){super(props);this.state{is_active_index:props.initial};if(Platform.OSandroid){UIManager.setLayoutAnimationEnabledExperimentalUIManager.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.4license: MIT上游 star446库类型纯 JS 组件库index.js251 B 只做导出 lib/SimpleRadioButton.js8831 B lib/Style.js978 B源码直发是否需要原生适配不需要无harmony/、无NativeModules运行时依赖✅零prepare/postinstall✅ 都没有mainindex.js需要 HAR / 权限 / ohpm❌ 都不需要★ 是否可用✅可以直接用★ 导出默认RadioForm具名RadioButton/RadioButtonInput/RadioButtonLabel4 个类★ 成员写法4 个类全是简写原型方法clearSelection/shouldComponentUpdate/componentDidUpdate… 都在原型上设备侧断言✅纯断言 24 / 24A 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 个类全是原型方法⇒ 断言可以照源码写 ✓npminstallgithttps://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_indexbuttonColor:#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/20B 渲染与几何5 个实例渲染未抛错、标题安全区vp ≥39、两条几何预言记录4/4C 交互与边界回调 /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 仍存在 ★ R1initial1的几何density 3 Custom#radioButtonInputradioButton|0 w90 h90 ← 外圈 30vp Custom#radioButtonInputradioButton|1 w90 h90 ← 外圈 30vp └─ Custom#View w60 h60 ← ★★ 选中内点 20vp唯一一个 Custom#radioButtonInputradioButton|2 w90 h90 ← 外圈 30vp无内点 ★ 交互PRESS R1 value0 index0 ← onPress(obj.value, index) ✓ ★ 交互CLEAR r4 clearSelection() 调用成功 UI 安全区TITLE y420pxvp ≥39 ✓五、已知限制与本次验证边界5.1 用这个库要知道的事项说明★ 回调是(value, index)不是label也不是事件对象★ 尺寸规则外圈默认 30vp / 内点默认 20vpbuttonSize⇒ 内点 它、外圈 它 10buttonOuterSize⇒ 单独指定外圈★borderWidth默认 3外圈边框宽度★★Platform.OS android才启用 LayoutAnimation鸿蒙走不到 ⇒动画可能不生效本轮未实测★animation默认 truecomponentDidUpdate里每次更新都会调LayoutAnimation.spring()★disabled时不渲染可点元素圆点变普通View标签 onPress 短路★radio_props为空 ⇒ 渲染children想自己拼布局时可以用★clearSelection()要拿 ref多选问卷场景常用⚠️ 一处无害拼写错误Style.js定义radioFormrender()里用Style.radioFrom⇒undefined数组样式里无影响⚠️UIManager.setLayoutAnimationEnabledExperimental用法用了保护所以即使该 API 不存在也不会抛错5.2 本次验证的边界★★ 动画LayoutAnimation是否真的不生效本轮未实测两帧像素差分没做成clearSelection()之后内点消失未做视觉确认R4 区在 dump 里定位失败量到的是别的实例disabled的对照没有真正点到禁用实例R3 在屏外⇒ 那一条断言是空过的buttonOuterSize未验证只用了默认与buttonSizeborderWidth/buttonInnerColor/buttonOuterColor未逐项验证labelHorizontalfalse的竖排布局未验证idSeparator/accessibilityLabel/testID的拼接规则未逐项核对只观察到节点名含radioButton|Nchildren分支只做了渲染观察R5RadioButton/RadioButtonInput/RadioButtonLabel单独使用未验证只作为RadioForm的内部件多选问卷的完整流程连续点选 清空 再选未跑只在一台模拟器上验证Pura X Viewdensity 31320×2232没有真机未做跨平台对照。未改动库代码。六、★ 两条断言失败的原因如实记录 一条流程教训C 组两条失败✗ [C] ★★ 点单选项触发了 onPress带 value 与 index 期望 true 实际 false ✗ [C] ★ 已通过 ref 调过 clearSelection 期望 true 实际 false原因不是库而是我这一侧的执行顺序我是先点了跑全部那一刻还没有任何交互之后才去点单选项、才去调clearSelection()⇒ 断言读的是点击之前的计数 ✓而这两个行为本身都已经被独立证实PRESS R1 value0 index0 ← 点「选项甲」的回调value 与 index 都对 CLEAR r4 clearSelection() 调用成功 ← 用 ref 调 clearSelection★ 教训交互类断言必须在交互【之后】再跑一次。更稳的做法是把跑断言分成两阶段先跑静态/契约断言做完交互后再跑交互断言或者让页面把断言结果实时刷新本轮是手动触发的快照。七、常见问题Q1它在 RN 0.84 上能跑吗能。纯 JS、零依赖、没有一处已移除的 API四个类都是简写原型方法。本工程实测打包与构建都成功、页面正常渲染、选中内点几何逐像素吻合、点击回调与clearSelection()都正常 ✓Q2怎么知道当前选中了哪一项两种办法// ① 受控自己记住 indexRadioForm initial{0}onPress{(value,index)setSel(index)}…/// ② 从布局上判断选中项会多一个内点默认 20vpdump 里可数Q3onPress的第一个参数是什么是obj.valueradio_props里那一项的value不是label。第二个参数是索引。实测点第一项得到value0 index0✓Q4想清空选择怎么办用 ref 调clearSelection()原型方法const ref useRef(null); RadioForm ref{ref} radio_props{props} onPress{…} / // 之后 ref.current.clearSelection(); // ⇒ is_active_index -1Q5怎么改圆点大小RadioForm buttonSize{24} … / // 内点 24vp外圈自动 34vp RadioForm buttonOuterSize{40} … / // 只改外圈 RadioForm borderWidth{2} … / // 外圈边框默认 3Q6为什么点禁用项没反应因为disabled为真时圆点被渲染成普通View不是TouchableOpacity标签的onPress里也直接短路 ⇒不会回调。Q7为什么我看不到切换动画源码里LayoutAnimation的启用开关只在Platform.OS android时才会调if(Platform.OSandroid){UIManager.setLayoutAnimationEnabledExperimentalUIManager.setLayoutAnimationEnabledExperimental(true);}⇒鸿蒙上这一支走不到。如果你的平台也需要这个 opt-in就应当在应用侧自己启用一次或者把animation关掉避免无意义调用。Q8这个库需要鸿蒙侧做什么特殊处理吗不需要。纯 JS、零依赖、无原生代码 —— 直接装、直接用 ✓唯一值得留意的是上面那条LayoutAnimation的启用开关小结react-native-simple-radio-button是纯 JS 的单选按钮组零依赖、源码直发。设备侧纯断言 24 / 24 通过几何逐像素命中。最值得记下来的是这几条★★★★ 选中状态在布局上是【结构可见】的 —— 这让几何判据非常硬。源码里选中时只是多渲染一个innerSize的实心圆this.props.isSelectedStyle.radioActive,this.props.isSelectedinnerSize,⇒ 实测initial{1}时只有第 2 项有嵌套节点且w60 h60px 20vp三个外圈都是w90 h90px 30vp✓✓⇒outerSize30 /innerSize20 两个默认值都被逐像素验证。★★ 节点名里带索引radioButton|N⇒ 在 dump 里可以按索引精确定位每个圆点做A/B 两态对比时特别方便。★★★ 回调签名可以直接预言。源码是onPress(this.props.obj.value, this.props.index)⇒ 实测PRESS R1 value0 index0✓⇒注意是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适配 TAG2.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# 从适配仓库安装零依赖npminstallgithttps://atomgit.com/oh-react-native/react-native-simple-radio-button.git#2.7.4-ohos-1.0.0--ignore-scripts# 本地目录接入npminstall../react-native-simple-radio-button --ignore-scriptsimport 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 定义 radioFormrender 里用 Style.radioFromundefined# 换页启动测试页 —— force-stop 不能省hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey SimpleRadioButtonTestApp判据的复现方式# ① 几何外圈 30vp90px、选中内点 20vp60px —— 直接数 dump 里的节点尺寸devecocli ui layout|Select-StringradioButtonInput# ⇒ w90 h90每项一个devecocli ui layout|Select-StringCustom#View# ⇒ 只有一个 w60 h60选中项的内点# ② 选中项节点名自带索引 radioButton|N ⇒ 可按索引定位# ③ 交互点标签或圆点读回调日志hdc shelluitest uiInput click x yhdc shellhilog -x | grep sradio-test# ⇒ PRESS R1 value0 index0# ④ clearSelection拿 ref 调读日志# ⇒ CLEAR r4 clearSelection() 调用成功# ⑤ 平台分支确认 Platform.OS 不是 android ⇒ LayoutAnimation 启用分支走不到验证环境项版本React Native0.84.1React19.2.3RNOHnpm / ohpmreact-native-oh/react-native-harmony/rnoh/react-native-openharmony0.84.3Node.js / npmv24.14.0/11.9.0DevEco Studio26.0.0.621HarmonyOS SDKAPI 2626.0.0.32设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器Pura X Viewohos-x641320×2232density 3本机状态栏高度117px 39vp宿主依赖✅无需补装库零依赖宿主 HAP 产物entry-default-signed.hap本次构建assembleHap4 分 04 秒 ⇒A 20/20 B 4/4打包与构建退出码都检查了验证规模设备侧纯断言 24 / 24几何 4 个节点三个外圈 90×90px 一个内点 60×60px逐像素命中交互 2 项PRESS … value0 index0/CLEAR … 调用成功4 张截图互异相关社区与项目地址项目地址React Native 鸿蒙官方社区CPF-RNhttps://atomgit.com/CPF-RNRNOH 框架仓库https://atomgit.com/openharmony-RN/ohos_react_nativeRNOH 三方库使用文档https://atomgit.com/OpenHarmony-RN/usage-docsReact Native for OpenHarmony 组织https://atomgit.com/oh-react-nativeRN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview欢迎加入 CPF-RN 鸿蒙社区https://atomgit.com/CPF-RNReact Native for OpenHarmony 组织https://atomgit.com/oh-react-nativeRN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview