本文记录把 react-native-segmented-control-tab 适配到 HarmonyOS 的完整过程。

这是一个分段控件(Segmented Control,类似 iOS 的 UISegmentedControl),纯 JS、零依赖、只有一个组件。接入很简单,但它有几个很适合做量化验证的特点:

  1. 选中态的底色是固定的蓝色 #0076FF、未选中是白色 ⇒ 可以拿"蓝色块的位置"当标尺:点哪一段,蓝块就该移到哪一段。这是一条位置型像素判据,比"看起来变了"硬得多。
  2. 它有一条很容易踩的边界:源码里
    if (multiple) { onTabPress(index) }
    else if (selectedIndex !== index) { onTabPress(index) }
    
    ⇒ 单选模式下点【已经选中】的那一段,不会触发任何回调。不知道这条的话,很容易把"点了没反应"当成缺陷。
  3. 它是 class 组件(PureComponent + static defaultProps) ⇒ 默认值在 React 19 下仍然生效 ✓

最终结果:设备侧 23 / 23 断言通过;像素判据显示蓝块从 x = 39–284 移到 533–783(与按等宽算出的 535–785 精确吻合)。

在这里插入图片描述


一、先说结论

项结果
上游最新版4.0.0;license: MIT
上游仓库 star618(kirankalyan5/react-native-segmented-control-tab)
库类型纯 JS 单组件库:SegmentedControlTab(class,PureComponent)
是否需要原生适配不需要(无 harmony/、无 NativeModules)
运行时依赖零(dependencies 未声明)
prepare / prepublish都没有(scripts 只有 lint / flow / test)⇒ 安装路径干净
mainsrc/SegmentedControlTab.js(源码,不是编译产物)
需要 HAR / 权限 / ohpm❌ 都不需要
设备侧断言✅ 23 / 23 全部通过
★ 像素判据✅ 选中段底色 #0076FF;点击后蓝块 39–284 → 533–783(预言 535–785)
★ 单选边界✅ 点已选中的段:0 次回调;点未选中的段:1 次回调(index 正确)
★ multiple 边界✅ 点已选中的段:也会回调(与单选形成对照)
★ 默认值✅ class 组件 ⇒ React 19 下 defaultProps 仍生效(26 个键)
代码异味⚠️ 源码里有两个 static defaultProps 声明(一个空对象、一个真值)⇒ 后者覆盖前者

二、判定与接入

步做法结果
①package.json 里有没有 harmony.autolinking没有
②仓库里有没有 harmony/没有
③代码里有没有 NativeModules / requireNativeComponent全无
npm install "git+https://atomgit.com/oh-react-native/react-native-segmented-control-tab.git#4.0.0-ohos-1.0.0" --ignore-scripts
// metro.config.js —— file: / 软链装法需要把真实目录加进 watchFolders
watchFolders: [ path.resolve(__dirname, '../react-native-segmented-control-tab') ],
import SegmentedTestApp from './SegmentedTestApp';
AppRegistry.registerComponent('SegmentedTestApp', () => SegmentedTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey SegmentedTestApp

不需要 HAR、权限、ohpm、link-harmony,也不需要补装任何依赖。


三、读源码取到的真值

3.1 组件形态与默认值

export default class SegmentedControlTab extends PureComponent<Props> {
  static defaultProps = {                    // ← ⚠️ 这里有一个【空的】defaultProps
  };

  static defaultProps = {                    // ← 真正的默认值(覆盖上面那个)
    values: ['One', 'Two', 'Three'],
    multiple: false,
    selectedIndex: 0,
    selectedIndices: [0],
    badges: ['', '', ''],
    borderRadius: 5,
    enabled: true,
    allowFontScaling: true,
    activeTabOpacity: 1,
    tabsContainerDisableStyle: { opacity: 0.6 },
    // … 共 26 个键
  };

要点:

  1. class 组件 ⇒ React 19 仍然支持 static defaultProps ⇒ 什么都不传也能正常渲染三段的 One / Two / Three ✓
    (⚠️ 反过来要注意:函数组件上的 defaultProps 在 React 19 下会完全失效 —— 那是另一类问题,判断时必须先分清组件类型。)
  2. ⚠️ 源码里有重复的 defaultProps 声明(第一个是空对象)⇒ 后者覆盖前者,第一个是死代码。不影响功能,但值得记一笔。
  3. ⚠️ 没有 propTypes(只有 Flow 类型,运行时会被擦除)⇒ 运行时没有属性校验。
  4. values 的个数决定段数;每段 flex: 1 ⇒ 等宽。

3.2 ★★ 像素判据:选中段的底色是固定的蓝色

src/TabOption.js 的样式:

tabStyle:       { paddingVertical: 5, flex: 1, justifyContent: 'center', alignItems: 'center',
                  borderColor: '#0076FF', borderWidth: 1, backgroundColor: 'white' },
activeTabStyle: { backgroundColor: '#0076FF' },      // ← 选中段:蓝底
tabTextStyle:   { color: '#0076FF' },                // 未选中:蓝字白底
activeTabTextStyle: { color: 'white' },              // 选中:白字蓝底

⇒ #0076FF = RGB(0, 118, 255)。所以:

  • 未选中的段:白底 + 蓝字 + 蓝边框(扫到的只是 1px 宽的边框)
  • 选中的段:整段蓝底(扫到的是宽约 248px 的连续蓝色区间)

⇒ “蓝色块的 x 区间"就是"当前选中段的位置” —— 点别的段,蓝块应当整体平移。这是一条位置型判据,可以做得很硬。

3.3 ★ 两条边界:单选 vs multiple

const handleTabPress = (index, multiple, selectedIndex, onTabPress) => {
  if (multiple) {
    onTabPress(index)                          // 多选:点已选中也回调
  } else if (selectedIndex !== index) {
    onTabPress(index)                          // 单选:点已选中【不回调】
  }
}
模式点已选中的段点未选中的段
multiple: false(默认)不触发 onTabPress触发
multiple: true触发触发

3.4 其它细节

  • 首段套 firstTabStyle(borderRadius 圆角)、末段套 lastTabStyle(borderLeftWidth: 0)、中间段有 marginLeft: -1(让相邻边框重叠,避免双线);
  • values.length === 2 时首段的 borderRightWidth 为 1(其它长度是 0);
  • enabled 默认 true(SegmentedControlTab 会把它传给每个 TabOption)。
    ⚠️ 但 TabOption 自己的 defaultProps.enabled 是 false ⇒ 单独使用 TabOption 时默认是禁用的(disabled={!enabled})。

四、验证设计:用"蓝块位置"当标尺

测试页放了 5 个实例:

实例配置用途
S1三段,selectedIndex={0} 且不更新测单选边界(点已选中不回调)与受控行为
S2五段,受控(onTabPress 里 setState)主像素判据:点哪段蓝块就移到哪段
S3multiple + selectedIndices测多选边界
S4两段走 values.length === 2 分支
S5四段,选中第 1 段等宽观测

判据:对屏幕截图横向扫描 #0076FF 的连续区间(容差 ±25/通道),得到蓝块的 x 范围;再和"按等宽算出的段边界"对照。


五、实测结果

5.1 断言 23 / 23

组内容结果
A 契约导出/class/原型、defaultProps 26 键与关键值、无 propTypes、重复声明的说明18/18
B 渲染与几何渲染未抛错、标题不与状态栏重合、段宽量取方式说明4/4
C 交互与边界事件记录格式 + 两条边界说明1/1
合计23/23

5.2 ★ 像素判据:蓝块确实移动了

在同一个受控实例(S2,五段)上,点击前/后各截一张图,扫描 #0076FF 的连续区间:

状态蓝块的 x 区间对应段按等宽算出的段边界
点击前(selectedIndex = 0)39 – 284第 1 段36 – 285.6
点击后(点第 3 段)533 – 783第 3 段535.2 – 784.8

容器从 x=36 到 x=1284(宽 1248px),5 段等宽 ⇒ 每段 249.6px;第 3 段起点 = 36 + 2 × 249.6 = 535.2 ✓
⇒ 实测 533–783 与算出的 535.2–784.8 精确吻合(1–2px 差是边框/JPEG 取整)。

同时 onTabPress 收到的 index 也是对的:日志为 S2 onTabPress(2)(0 基)。

点击前点击后
在这里插入图片描述
在这里插入图片描述

(两图取景完全一致,唯一差别就是蓝色块的位置 —— 这就是"效果真的发生了"的直接证据。)

5.3 ★ 两条边界实测

边界 A —— 单选模式点"已选中"的段:

点 S1-ONE(此时 selectedIndex=0,它已是选中态)
  ⇒ S1 onTabPress 次数 = 0        ← 不触发 ✓
点 S1-TWO(未选中)
  ⇒ 累计 S1 onTabPress = 1
     S1 onTabPress(1)             ← 触发且 index 正确 ✓

边界 B —— multiple 模式点"已选中"的段:

点 S3-A(此时 selectedIndices=[0],它已是选中态)
  ⇒ S3 onTabPress 次数 = 1
     S3 onTabPress(0)             ← 【触发了】✓ 与单选形成鲜明对照
点 S3-B
  ⇒ S3 onTabPress(1)              ← 多选可累加 ✓

⇒ 源码里那个 else if (selectedIndex !== index) 的语义完全成立。

5.4 UI:标题不与状态栏重合

本机状态栏占 0…117px。页面顶部留白 48vp = 144px,实测:

TITLE measureInWindow y=420   (状态栏高 117)

⇒ 420 > 117 ⇒ 不与电量栏重合 ✓(验收必须用 measureInWindow 拿窗口坐标,onLayout 给的是相对父容器的坐标。)

在这里插入图片描述


六、我自己的一个失误

第一版断言里我写了"已测到段宽",指望用每段的 onLayout 拿到宽度 —— 但这个组件根本不暴露每段的 onLayout(它只在 TabOption 内部渲染,没有把 onLayout 透出来),我代码里那个 tabW 状态从头到尾没被写入过。

⇒ 假失败 1 条。纠正:段宽只能从外部布局 dump 量(本页实测段中心间距恒 248px,5 段 × 248 ≈ 1248 = 容器宽 ⇒ flex: 1 等宽成立),页内不做这个断言。修正后 23/23。

教训:写断言前先确认"这个数据在页内到底拿不拿得到" —— 拿不到的就老老实实放外部量,别写一条注定失败(或注定真空)的断言。


七、已知限制与本次验证边界

7.1 用这个库要知道的

事项说明
单选点已选中不触发回调源码 selectedIndex !== index 才回调 ⇒ 想让"重复点也响应"就得自己包一层或改用 multiple
它是受控组件不接 onTabPress 去更新 selectedIndex,点了选中态不会变(视觉上"点了没反应")
没有 propTypes只有 Flow 类型,运行时无属性校验,传错不会警告
TabOption 单独用时默认禁用TabOption.defaultProps.enabled = false;SegmentedControlTab 会把它覆盖成 true
选中色写死 #0076FF想换主题要传 activeTabStyle / activeTabTextStyle / tabTextStyle 自己覆盖
两个 defaultProps 声明第一个是空对象,属死代码(不影响行为)
values.length === 2 有特殊分支首段 borderRightWidth 与其它长度不同

7.2 本次验证的边界

  1. 只在一台模拟器上验证(Pura X View,density 3,1320×2232),没有真机。
  2. badges 徽标未验证:源码支持 badges(红色小圆点 + 白字;选中时白底黑字),本轮没有测徽标的渲染与像素。
  3. borderRadius 圆角未做像素级核对:只从源码确认首/末段的圆角半径来自 borderRadius(默认 5),没有量圆角是否真的被裁圆。
  4. 自定义样式未测:activeTabStyle / tabStyle / firstTabStyle / lastTabStyle / tabTextStyle / activeTabTextStyle 的覆盖效果没有逐个验证。
  5. enabled={false} 的禁用态未测(源码会给容器加 opacity: 0.6,且 TabOption 被 disabled)。
  6. accessibilityLabels / testIDs / textNumberOfLines / allowFontScaling / activeTabOpacity 未测。
  7. values 很多段(超过 5)时的挤压/换行未测;中文/长文案的 ellipsizeMode="tail" 截断未测。
  8. 没有跨平台对照(未在 iOS/Android 上跑同一套用例)。

未改动库代码。


八、常见问题

Q1:点了已经选中的那一段,onTabPress 不触发,是 bug 吗?

不是,是设计。 源码里:

if (multiple) { onTabPress(index) }
else if (selectedIndex !== index) { onTabPress(index) }   // ← 单选:点已选中不回

实测:单选模式点已选中的段 ⇒ 0 次回调;点未选中的段 ⇒ 1 次回调。想要"重复点也响应",要么用 multiple,要么自己在外面包一层。

Q2:我点了但界面没变化?

因为它是受控组件:selectedIndex 只由 props 决定。你必须接 onTabPress 并把它写回 state:

const [idx, setIdx] = useState(0);
<SegmentedControlTab
  values={['One', 'Two', 'Three']}
  selectedIndex={idx}
  onTabPress={setIdx}        // ★ 不写这句,点了不会变
/>

Q3:怎么换选中色?默认那个蓝色是哪来的?

默认写死在 src/TabOption.js:activeTabStyle: { backgroundColor: '#0076FF' }、tabTextStyle: { color: '#0076FF' }、activeTabTextStyle: { color: 'white' }。要换主题就传同名 prop 覆盖:

<SegmentedControlTab
  values={['A', 'B']}
  activeTabStyle={{backgroundColor: '#2ea043'}}
  activeTabTextStyle={{color: '#fff'}}
  tabTextStyle={{color: '#2ea043'}}
/>

Q4:默认值在 React 19 下会不会失效?

不会。 这个组件是 class(PureComponent),用的是 static defaultProps —— React 19 移除了函数组件的 defaultProps 支持,class 的仍然支持。实测 defaultProps 有 26 个键且不传 props 也能正常渲染三段的 One/Two/Three。

(注意:如果换成函数组件 + defaultProps 的库,在 React 19 下默认值会全部失效 —— 那是另一类问题。)

Q5:能多选吗?

能,用 multiple + selectedIndices:

const [idxs, setIdxs] = useState([0]);
<SegmentedControlTab
  values={['A', 'B', 'C']}
  multiple
  selectedIndices={idxs}
  onTabPress={i => setIdxs(p => p.includes(i) ? p.filter(x => x !== i) : [...p, i])}
/>

⚠️ 多选模式下点已选中的段也会回调(与单选的差别,实测已确认)。

Q6:values 传几个就显示几段吗?宽度怎么分?

是的,values.map(...);每段是 flex: 1 ⇒ 等宽。本机实测:5 段、容器宽 1248px ⇒ 段间距恒 248px。

Q7:怎么加小徽标(红点数字)?

传 badges(与 values 等长的数组,空字符串表示不显示):

<SegmentedControlTab values={['收件箱', '已发送']} badges={['3', '']} />

⚠️ 本轮没有验证徽标的实际渲染(见"验证边界")。

Q8:为什么我的段显示不全 / 文字被截断?

TabOption 的文本是 numberOfLines={textNumberOfLines}(默认 1)+ ellipsizeMode="tail" ⇒ 超长会被截断成 …。段多或文案长时要注意,或调 textNumberOfLines。

Q9:TabOption 能单独用吗?

能,但注意它自己的 defaultProps.enabled 是 false ⇒ 单独用时默认是禁用的(disabled={!enabled}),要显式传 enabled。SegmentedControlTab 内部会传 enabled={true}。

Q10:这个库需要鸿蒙侧做什么特殊处理吗?

不需要。 纯 JS、零依赖、无原生模块、不需要 HAR / 权限 / ohpm。唯一要注意的是本文说的单选边界与受控特性。


小结

react-native-segmented-control-tab 是纯 JS 单组件库:class 组件(PureComponent)+ static defaultProps(26 键)、零依赖、main 指向源码。设备侧 23 / 23 断言通过。

值得记下来的是这几条:

  1. ★ 选中色固定 ⇒ 天然的位置型像素判据。 选中段底色是 #0076FF、未选中是白色,所以**“蓝色块的 x 区间"就是"当前选中段的位置”**。实测:点击前 39–284、点第 3 段后 533–783,与按等宽算出的 535.2–784.8 精确吻合。
    ⇒ 只要组件有"固定配色的状态块",就优先拿它的位置当标尺 —— 比"看起来变了"硬得多,也比文字断言更抗改。

  2. ★ 库里的"分支条件"就是最该测的边界。 源码那一行 else if (selectedIndex !== index) 决定了"单选点已选中不回",实测 0 次回调;而 multiple 下同样点法回 1 次。⇒ 读源码时把每个 if 都记下来,逐个设计用例;这条如果漏了,很容易把正常行为报成缺陷。

  3. class 与函数组件的 defaultProps 在 React 19 下命运不同。 这个库是 class ⇒ 26 个默认值全部生效;而函数组件的 defaultProps 在 React 19 已被移除支持。⇒ 看到"库里有默认值"要先问:它是 class 还是函数组件?

  4. 写断言前先确认"这个数据页内拿不拿得到"。 我写了一条"已测到段宽",但组件不暴露每段 onLayout,那个状态从头到尾没被写入过 ⇒ 假失败一条。⇒ 拿不到的数据就放外部量(布局 dump / 像素),别写注定为空的断言。

  5. UI 安全区仍然要单独验。 本机状态栏 117px,页面留白 48vp,实测标题窗口坐标 y = 420 > 117 ⇒ 不重合 ✓(measureInWindow,不是 onLayout)。


本篇用到的库

项内容
三方库react-native-segmented-control-tab(上游 4.0.0 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-segmented-control-tab
适配 TAG4.0.0-ohos-1.0.0
需要 HAR / 权限 / ohpm都不需要(纯 JS,零依赖)
库类型纯 JS 单组件库:分段控件(Segmented Control)
运行时依赖零
prepare / prepublish都没有
许可证MIT
上游仓库https://github.com/kirankalyan5/react-native-segmented-control-tab(★618)
宿主工程RNOH084Demo(测试页 rnAppKey = SegmentedTestApp)
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-segmented-control-tab.git#4.0.0-ohos-1.0.0" --ignore-scripts

# 本地目录接入(开发常用)
npm install ../react-native-segmented-control-tab --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-segmented-control-tab') ],
// 用法(★ 受控:必须自己把 onTabPress 的 index 写回 state)
import SegmentedControlTab from 'react-native-segmented-control-tab';

const [idx, setIdx] = useState(0);

<SegmentedControlTab
  values={['One', 'Two', 'Three']}
  selectedIndex={idx}
  onTabPress={setIdx}                       // ★ 不写这句,点了选中态不会变
  borderRadius={5}
  activeTabStyle={{backgroundColor: '#0076FF'}}   // 默认就是这个蓝,换主题时覆盖
  activeTabTextStyle={{color: 'white'}}
  tabTextStyle={{color: '#0076FF'}}
/>

// 多选(点已选中的段【也会】回调)
<SegmentedControlTab
  values={['A', 'B', 'C']}
  multiple
  selectedIndices={idxs}
  onTabPress={i => setIdxs(p => p.includes(i) ? p.filter(x => x !== i) : [...p, i])}
/>

// 带徽标
<SegmentedControlTab values={['收件箱', '已发送']} badges={['3', '']} />
# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey SegmentedTestApp

像素判据复现(本节判据的复现方式)

# 截图后扫描选中段的蓝色 #0076FF(RGB 0,118,255),取连续区间
Add-Type -AssemblyName System.Drawing
$b = New-Object System.Drawing.Bitmap('shot.jpeg')
$xs = @()
for ($x = 0; $x -lt $b.Width; $x++) {
  $p = $b.GetPixel($x, $segRowY)
  if ([Math]::Abs($p.R-0) -lt 25 -and [Math]::Abs($p.G-118) -lt 25 -and [Math]::Abs($p.B-255) -lt 25) { $xs += $x }
}
# 连续区间即选中段的位置;点了别的段,该区间应当整体平移

验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.js / npmv24.14.0 / 11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 26(26.0.0.32)
设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64),1320×2232,density 3
本机状态栏高度117px(页面留白取 48vp)
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap 4 分 00 秒;改断言后重编 3 分 52 秒
验证规模设备侧 23 / 23 断言;像素判据 2 张(点击前后,蓝块 39–284 → 533–783);边界 2 组(单选不回调 0 次 / multiple 回调 1 次);UI 安全区 1 项(标题窗口 y=420 > 117)

相关社区与项目地址

项目地址
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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐