本文记录把 react-native-step-indicator(步骤条 / 进度指示器)适配到 HarmonyOS 的完整过程。

这是一个纯 JS 组件库,源码 475 行,功能是"横向或纵向的步骤条",带可点击的步骤、可自定义的标签渲染,以及一条随进度增长的已完成色带。它不需要任何原生接线。

它有两个很适合做量化验证的特点,我把它们都做成了硬判据:

  1. 已完成色带的宽度可以直接算出来:

    const animateToPosition = (progressBarSize / (stepCount - 1)) * position;
    // progressBarSize = width * (stepCount - 1) / stepCount
    // ⇒ 绿条宽度 = width * k / N
    
  2. 当前步骤的圆圈更大(默认 40 vs 30)且底色是白色,而其它步骤是实心绿 / 浅绿 ⇒ 白圆的圆心 x 就是 currentPosition 的位置:

    圆心 x = width/(2N) + k * width/N        (styles.stepContainer 是 flex:1 ⇒ 等分)
    

实测(容器宽 1248px、N=5)三个实例全部精确命中:

实例k绿条宽度 预言 / 实测当前步圆心 x 预言 / 实测当前步圆尺寸
S100 / 无绿条 ✓160.8 / 161 ✓120px
S22499.2 / 499 ✓660.0 / 660 ✓120px
S34998.4 / 998(满格) ✓1159.2 / 1159 ✓120px

设备侧断言 13 / 13 全部通过,一次构建就过。

在这里插入图片描述


一、先说结论

项结果
上游最新版1.0.3;license: MIT;上游 star 1459
库类型纯 JS 单组件库:StepIndicator(步骤条)
是否需要原生适配不需要(无 harmony/、无 NativeModules;import 图只有 react / react-native / ./types)
运行时依赖零(dependencies 未声明);peerDependencies 只有 react / react-native
prepare⚠️ 保留了上游的 "prepare": "bob build"(bob 是 devDependency)⇒ 目录/file: 接入必须 --ignore-scripts
mainlib/commonjs/index(编译产物)⇒ 必须单独核对产物
导出形态只有 default 一个导出,且它是 React.memo(...) 返回的对象(typeof 'object'、无可用 prototype)
需要 HAR / 权限 / ohpm❌ 都不需要
设备侧断言✅ 13 / 13 全部通过(A 5 + B 7 + C 1)
★ 两个像素判据✅ 绿条宽度 = W·k/N、当前步圆心 = W/(2N)+k·W/N —— 三个实例全部逐像素吻合
★ 当前步更大更白✅ 当前步圆 120px(40vp),其余 90px(30vp)
★ 交互✅ 点步骤圆 ⇒ onPress(0) 触发且 index 正确

二、判定与接入

步做法结果
①package.json 里有没有 harmony.autolinking没有
②仓库里有没有 harmony/没有
③代码里有没有 NativeModules / requireNativeComponent全无
④import 图体检(防 @env 那类坑)只有 react / react-native / ./types ✓
npm install "git+https://atomgit.com/oh-react-native/react-native-step-indicator.git#1.0.3-ohos-1.0.0" --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-step-indicator') ],
import StepIndicatorTestApp from './StepIndicatorTestApp';
AppRegistry.registerComponent('StepIndicatorTestApp', () => StepIndicatorTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey StepIndicatorTestApp

不需要 HAR、权限、ohpm、link-harmony。


三、读源码取到的真值(src/index.tsx,475 行)

3.1 组件形态与默认值

const StepIndicator = ({
  currentPosition = 0,
  stepCount = 5,
  direction = 'horizontal',
  customStyles: customStylesFromProps = defaultStyles,
  labels = [],
  onPress,
  renderStepIndicator: renderCustomStepIndicator,
  renderLabel,
}: StepIndicatorProps) => { … };

export default React.memo(StepIndicator);

要点:

  1. 函数组件 + 参数解构默认值 ⇒ React 19 下没问题(不是 defaultProps 那类坑);
  2. 默认导出是 React.memo(...) 的返回值 ⇒ typeof 'object'、没有可用的 prototype;实测模块运行时只有 default 一个导出(Object.keys(...) = ['default']);
  3. customStyles 会与 defaultStyles 合并({...defaultStyles, ...customStylesFromProps})。

3.2 ★ 进度条的几何与公式

// 背景条(浅绿)
progressBarBackgroundStyle = {
  backgroundColor: customStyles.separatorUnFinishedColor,   // '#a4d4a5'
  position: 'absolute',
  top: (height - separatorStrokeWidth) / 2,
  left:  width / (2 * stepCount),
  right: width / (2 * stepCount),
  height: separatorStrokeWidth,                              // 默认 2vp
};
// ⇒ progressBarSize = width * (N-1) / N

// 已完成条(绿):同样的 left/right,但宽度受动画控制
progressBarStyle = { …, width: progressAnim };

// 动画目标
const animateToPosition = (progressBarSize / (stepCount - 1)) * position;
Animated.timing(progressAnim, { toValue: animateToPosition, duration: 200, useNativeDriver: false });

⇒ 绿条宽度 = width · k / N(useNativeDriver: false ⇒ JS 驱动)。

3.3 ★ 当前步骤为什么"更大更白"

const getStepStatus = (p) =>
  p === currentPosition ? CURRENT : p < currentPosition ? FINISHED : UNFINISHED;

// CURRENT:白底 + 粗绿边 + sizeAnim(动画到 currentStepIndicatorSize=40)
// FINISHED:实心 stepIndicatorFinishedColor '#4aae4f',尺寸 stepIndicatorSize=30
// UNFINISHED:stepIndicatorUnFinishedColor '#a4d4a5',尺寸 30
// 且 stepContainer 是 flex:1 ⇒ 每步等分 ⇒ 圆心 x = width/(2N) + k*width/N

默认配色(正是像素判据的依据):

用途颜色
当前步底色#ffffff(白)
已完成步 / 已完成条#4aae4f(绿)
未完成步 / 背景条#a4d4a5(浅绿)

3.4 交互

const stepPressed = (position) => { if (onPress) { onPress(position); } };
// 每个步骤与标签都包在 TouchableWithoutFeedback 里,点击 ⇒ stepPressed(position)

⚠️ 注意:源码里还有一个 onCurrentPositionChanged,但它是组件内部的 const 函数(src/index.tsx:389),不是 prop(src/types.ts 里也没有声明它)⇒ 它无法从外部监听(见第六节我的失误)。


四、验证设计:两个独立的像素判据

判据内容为什么可靠
① 绿条宽度已完成色带(#4aae4f)的宽度应为 W·k/N与"动画跑到哪"直接挂钩
② 当前步圆心白色圆圈(#ffffff,40vp)的圆心 x 应为 W/(2N)+k·W/N,且尺寸比其它大位置 + 尺寸双重特征,不易误判

两个判据互相独立(一个看色带长度、一个看圆圈位置),能交叉验证。

测量方式:以布局 dump 的绝对坐标为准(见第七节的教训:截图有 JPEG 色晕,几何量不准)。


五、实测结果

5.1 断言 13 / 13

组内容结果
A 契约默认导出存在、是 memo 对象、typeof object、有 $$typeof、无可用 prototype、枚举全部具名导出5/5
B 渲染与几何4 个实例渲染未抛错、标题不与状态栏重合、4 个实例尺寸、两个判据的预言值7/7
C 回调事件记录格式1/1
合计13/13

实例尺寸:{"s1":"416x79","s2":"416x79","s3":"416x79","s4":"416x79"} ⇒ 容器宽 416vp = 1248px ✓

5.2 ★★ 几何逐像素复核(布局 dump 绝对坐标)

S2(k=2 / N=5):

y=2034..2043 (h=9)  x=161..1159 (w=998)   ← 背景条(浅绿)
y=2034..2043 (h=9)  x=161..660  (w=499)   ← ★ 已完成条(绿)
y=1978..2098 (h=120) x=600..720 (w=120)   ← ★ 当前步(k=2)40vp,圆心 660
y=1993..2083 (h=90)  x=116..206 / 366..456 / 865..955 / 1114..1204   ← 其余 30vp,圆心 161/411/910/1159

S3(k=4 / N=5):

y=711..720 (h=9)  x=161..1159 (w=998)   ← ★ 已完成条【满格】
圆心:161 / 411 / 660 / 910 / ★1159(当前步 120px,其余 90px)

S1(k=0):只有背景条(浅绿),没有已完成绿条 ✓

汇总:

实例绿条宽度 预言实测当前步圆心 预言实测当前步尺寸
S1 (k=0)00(无) ✓160.8161 ✓120px
S2 (k=2)499.2499 ✓660.0660 ✓120px
S3 (k=4)998.4998 ✓1159.21159 ✓120px

⇒ 两个判据、三个实例、六组数字全部逐像素吻合 ✓✓

在这里插入图片描述

在这里插入图片描述

5.3 ★ 交互实测

S4 是 3 步(stepCount=3)的受控实例,圆心预言为 36+1248/6+k·416 ⇒ 244 / 660 / 1076;实测:

圆: x=199..289  w=90   圆心 x=244   ✓
圆: x=615..705  w=90   圆心 x=660   ✓
圆: x=1016..1136 w=120 圆心 x=1076  ✓(当前步,更大)

点击第 1 个圆(244, 1680) ⇒ 日志:

PRESS onPress(0)

⇒ onPress 触发,且 index 正确(0 基)✓

在这里插入图片描述

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

TITLE measureInWindow y=420   (状态栏高 117)   ⇒ 420 > 117,不重合 ✓

六、我自己的一个失误

我把 onCurrentPositionChanged 当成了回调 prop,在测试页里这样写:

<StepIndicator
  currentPosition={pos}
  onPress={...}
  onCurrentPositionChanged={(p) => logEvent(`POS-CHANGED onCurrentPositionChanged(${p})`)}   // ← 无效
/>

实测结果:点击只打出 PRESS onPress(0),没有任何 POS-CHANGED。

读源码后确认:onCurrentPositionChanged 是组件内部定义的 const 函数(src/index.tsx:389,在 getStepStatus 之后),由内部的 useEffect([currentPosition, progressBarSize]) 调用;它不在 props 解构里,src/types.ts 的 StepIndicatorProps 里也没有它 ⇒ 它不是对外 API,外部无法监听。

纠正:把 C 组的设计改为"只断言真实存在的 prop(onPress)",并在文章里写明 onCurrentPositionChanged 不是 prop。

⚠️ 教训:回调名不等于回调 prop。看到组件内部有个 onXxx(...) 调用,就去 props 解构里核一遍——只有当它出现在解构参数(或 propTypes/类型声明)里,才是对外 API。


七、方法上的一个教训:几何量必须用 dump,不能信截图

本轮我先用截图扫色来量进度条,得到的是 x=117..1203;而布局 dump给出的绝对坐标是 x=161..1159。

差异来自 JPEG 压缩的色晕(边缘像素被混色,落到我的颜色容差之外/之内)。dump 的数值是精确的(161 与预言 160.8 只差 0.2px)。

⇒ 判据分工:

  • 几何(位置/尺寸)⇒ 用布局 dump 的绝对坐标;
  • 颜色/状态/视觉对照 ⇒ 用截图(比如"绿条到哪结束"这种颜色跳变,dump 里没有颜色信息)。

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

8.1 用这个库要知道的

事项说明
prepare: "bob build" 仍在目录/file: 接入必须加 --ignore-scripts(否则会因为找不到 bob 而安装失败)
main 指向编译产物lib/commonjs/index(源码在 src/)⇒ 核对改动时要连产物一起比
默认导出是 memo 对象typeof 'object'、import 后拿不到 prototype
onCurrentPositionChanged 不是 prop它是内部函数,外部无法监听;要感知变化只能靠自己的 currentPosition 状态
进度条是动画的(200ms)useNativeDriver: false(JS 驱动);截图时机太早可能拍到中间态
当前步比其它步大currentStepIndicatorSize(40) vs stepIndicatorSize(30) ⇒ 布局高度按 40 算

8.2 本次验证的边界

  1. 只验证了 direction='horizontal'(默认)⇒ direction='vertical' 完全未测(源码里竖屏分支用的是 height/(2*stepCount) / top/bottom,几何另有公式);
  2. renderLabel / renderStepIndicator 两个自定义渲染回调未验证;
  3. customStyles 自定义未逐项验证(只用了默认配色);
  4. labels 的选中态颜色未做像素核对(源码里 currentStepLabelColor vs labelColor 不同,但文字颜色不好扫);
  5. 步骤数 ≠ 5 的横向几何只验了 stepCount=3(S4);其它数量未穷举;
  6. 进度条动画的中间态未验证(只在动画结束后测量);
  7. stepCount=1 的边界未测(源码里 progressBarSize/(stepCount-1) 会除以 0 ⇒ animateToPosition = Infinity,然后被 isNaN 兜住 ⇒ 但这只是代码级推断,未实测);
  8. 只在一台模拟器上验证(Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。

未改动库代码。


九、常见问题

Q1:为什么 import StepIndicator from 'react-native-step-indicator' 拿不到 prototype?

因为它导出的是 React.memo(StepIndicator) 的返回值,是个对象(typeof 'object'),不是 class/函数。要看它的 props 请查 src/types.ts 的 StepIndicatorProps。

Q2:npm install 报找不到 bob?

因为包里保留了 "prepare": "bob build",而 bob(react-native-builder-bob)只是 devDependency。目录/file: 接入时 prepare 会执行 ⇒ 安装失败。

解法:

npm install ../react-native-step-indicator --ignore-scripts

(走 .tgz 安装不会触发 prepare。)

Q3:进度条(已完成色带)的长度是怎么算的?

绿条宽度 = width * currentPosition / stepCount

推导:背景条从 left/right = width/(2N) 起 ⇒ 自身宽 width*(N-1)/N;动画目标是
(progressBarSize/(stepCount-1)) * position ⇒ 乘起来正好是 width*k/N。
实测(W=1248, N=5):k=2 ⇒ 499px(算 499.2)、k=4 ⇒ 998px(算 998.4)✓

Q4:怎么把当前步骤做得更显眼?

默认已经做了两件事:当前步圆更大(currentStepIndicatorSize 40 vs stepIndicatorSize 30)、底色变白(stepIndicatorCurrentColor: '#ffffff')且带粗绿边(currentStepStrokeWidth: 5)。都能通过 customStyles 改。

Q5:onCurrentPositionChanged 怎么监听?

监听不了 —— 它是组件内部的函数,不是 prop(StepIndicatorProps 里没有它)。要感知位置变化,请自己持有 currentPosition 状态:

const [pos, setPos] = useState(0);
<StepIndicator currentPosition={pos} onPress={setPos} />   // 位置变化由你自己掌握

Q6:点击步骤能拿到下标吗?

能。onPress(position) 给的就是0 基下标(实测点第 1 个圆得到 onPress(0))。

Q7:stepCount 传 1 会怎样?

按源码推,progressBarSize / (stepCount - 1) 会除以 0 ⇒ 目标值变成 Infinity/NaN,而代码里有 isNaN(animateToPosition) ? 0 : animateToPosition 兜底 ⇒ 进度条宽度会退回 0。⚠️ 这一条属代码级推断,本轮未实测。

Q8:颜色怎么改?

统一走 customStyles:

<StepIndicator
  customStyles={{
    stepIndicatorFinishedColor: '#4aae4f',
    stepIndicatorUnFinishedColor: '#a4d4a5',
    stepIndicatorCurrentColor: '#ffffff',
    separatorFinishedColor: '#4aae4f',
    separatorUnFinishedColor: '#a4d4a5',
    currentStepLabelColor: '#4aae4f',
  }}
/>

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

不需要。 纯 JS、零依赖、import 图干净(只有 react / react-native)、不需要 HAR / 权限 / ohpm。唯一要注意的是安装时的 prepare 和上面几条 API 细节。


小结

react-native-step-indicator 是纯 JS 单组件库(源码 475 行),零依赖、无需原生接线。设备侧 13 / 13 断言通过,一次构建就过。

最值得记下来的是这几条:

  1. ★★ 能从源码推出"确定的像素值"时,就把它当判据。 这个库的进度条宽度有闭式公式(W·k/N),当前步的圆心也有(W/(2N)+k·W/N)⇒ 我据此做了两个互相独立的判据,三个实例 六组数字全部逐像素吻合(499/998 与 660/1159)。⇒ 比"看起来对"硬得多,而且一旦公式写错会立刻暴露。

  2. ★ 几何量必须用布局 dump 的绝对坐标,不要信截图。 我用截图扫色量出 117..1203,而 dump 是 161..1159 —— 差的正是 JPEG 色晕。⇒ 分工:几何用 dump,颜色/视觉用截图。

  3. ★ 回调名 ≠ 回调 prop。 onCurrentPositionChanged 名字看着像回调 prop,实际是组件内部的 const 函数 ⇒ 我从外部传它完全没有效果。⇒ 判断一个回调是否对外,要去 props 解构 / propTypes / 类型声明里核一遍(这与"引用一个子路径/可选入口前先看它的 import 图"是同一个思路,只是发生在 API 层面)。

  4. prepare 是跨库反复出现的坑。 这个库保留了上游的 "prepare": "bob build" ⇒ 目录/file: 接入会失败。⇒ 接库时把 scripts 扫一遍(prepare / prepublish / postinstall),一律用 --ignore-scripts 兜底。

  5. main 指向编译产物时要连产物一起核。 本库 main = lib/commonjs/index,所以"逐文件比对"里必须包含 lib/** —— 否则"源码没改"不能推出"跑起来的产物没改"。


本篇用到的库

项内容
三方库react-native-step-indicator(上游 1.0.3 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-step-indicator
适配 TAG1.0.3-ohos-1.0.0
需要 HAR / 权限 / ohpm都不需要(纯 JS,零依赖)
库类型纯 JS 单组件库:步骤条 / 进度指示器
运行时依赖零;peerDependencies 只有 react / react-native
prepare⚠️ 有 "prepare": "bob build" ⇒ 目录/file: 接入必须 --ignore-scripts
许可证MIT
上游仓库https://github.com/24ark/react-native-step-indicator(★1459)
宿主工程RNOH084Demo(测试页 rnAppKey = StepIndicatorTestApp)
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-step-indicator.git#1.0.3-ohos-1.0.0" --ignore-scripts

# 本地目录接入(开发常用)—— prepare: bob build 会让不带 --ignore-scripts 的安装失败
npm install ../react-native-step-indicator --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-step-indicator') ],
import StepIndicator from 'react-native-step-indicator';

const [pos, setPos] = useState(0);
const labels = ['已下单', '已付款', '已发货', '运输中', '已签收'];

// ① 基本用法(横向,5 步)
<StepIndicator
  currentPosition={pos}
  stepCount={5}
  labels={labels}
  onPress={setPos}                  // ★ 真 prop:点击步骤/标签会给 0 基下标
/>

// ② 自定义配色
<StepIndicator
  currentPosition={2}
  stepCount={5}
  customStyles={{
    stepIndicatorFinishedColor: '#4aae4f',
    stepIndicatorUnFinishedColor: '#a4d4a5',
    stepIndicatorCurrentColor: '#ffffff',
    separatorFinishedColor: '#4aae4f',
    separatorUnFinishedColor: '#a4d4a5',
    currentStepLabelColor: '#4aae4f',
  }}
/>

// ③ 自定义标签渲染
<StepIndicator
  currentPosition={1} stepCount={3}
  labels={['A', 'B', 'C']}
  renderLabel={({position, stepStatus, label, currentPosition}) => (
    <Text>{`${position}:${label}(${stepStatus})`}</Text>
  )}
/>

// ⚠️ onCurrentPositionChanged 不是 prop(内部函数),传了也没用
# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey StepIndicatorTestApp

几何判据的复现方式

# ★ 几何量用布局 dump 的【绝对坐标】,不要用截图扫色(JPEG 色晕会让边缘偏几十像素)
devecocli ui layout | Select-String 'MARK-S2'
#  期望(W=1248, N=5, k=2):
#    背景条 x=161..1159 (w=998)      已完成绿条 x=161..660 (w=499)
#    当前步圆 w=120 圆心 x=660        其余圆 w=90 圆心 x=161/411/910/1159

验证环境

项版本
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 3 分 41 秒(一轮就过,无重编)
验证规模设备侧 13 / 13 断言;几何复核 3 个实例 × 2 个判据 = 6 组(绿条 0/499/998、圆心 161/660/1159、当前步圆 120px);交互 1 次(onPress(0));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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐