React Native for OpenHarmony 实战:三方库 react-native-progress-bar-animated 的鸿蒙化适配指南
本文记录把 react-native-progress-bar-animated(带动画的进度条)适配到 HarmonyOS 的完整过程。
它做的事很专一:画一根进度条 —— 外层是带边框的容器,内层是一条 Animated 驱动的填充条,宽度按百分比增长;可选在"走满"时把底色渐变过去、并回调一个 onComplete。
结论:可以用,而且几何能做到像素级精确核对(7 组预言全部命中)。 但它有两处**"现在能跑、但有隐患"的写法,还有一个契约层面的运行时事实与源码不一致**,这三点比"能不能跑"更值得写下来。
★★★★★ 一、几何:7 组像素级预言全部精确命中
核心公式只有一行(src/AnimatedProgressBar.js 的 animateWidth):
animateWidth() {
const toValue = ((this.props.width * this.state.progress) / 100) - this.props.borderWidth * 2;
Animated.timing(this.widthAnimation, {
easing: Easing[this.props.barEasing],
toValue: toValue > 0 ? toValue : 0,
duration: this.props.barAnimationDuration,
}).start();
}
⇒ 内层填充条的宽度 = width × 百分比 − 边框×2(单位 vp) ⇒ 可以精确预言 ✓
实测(density 3;我把三个实例放在同一屏,用布局 dump 逐条核对):
| 实例 | 参数 | 公式 | 预言 | 实测(布局 dump) |
|---|---|---|---|---|
| E1 外层容器 | width=300、height=15、borderWidth=1 | 300vp / 15vp | 900 × 45px | ✅ y=1257 w=900 h=45 |
| E1 内层填充 | value=100(= maxValue) | 300×100/100 − 2 = 298vp | 894px | ✅ y=1260 **w=894** h=39 |
| E1 内层填充 | value=50 | 300×50/100 − 2 = 148vp | 444px | ✅ w=444 |
| E3 内层填充 | value=75、height=10 | 300×75/100 − 2 = 223vp | 669px | ✅ y=1715 **w=669** h=24 |
| E2 外层容器 | height=24 | 24vp | 72px | ✅ y=1471 w=900 h=72 |
| E3 外层容器 | height=10 | 10vp | 30px | ✅ y=1712 w=900 h=30 |
| 内层填充高度 | 任意 | height − borderWidth×2 = 15−2 = 13vp | 39px | ✅ h=39 |
⇒ 七组全部命中:900/894/444/669/72/30/39 这七个像素数与公式逐一对上 ✓✓
⇒ 顺带验证了 toValue > 0 ? toValue : 0 那个保护:value=0 时内层宽度为 0,它在 dump 里根本不会出现(见第三节)。
★★★★ 二、两处"现在能跑、但有隐患"的写法
2.1 componentWillReceiveProps —— React 已废弃的旧生命周期,但它【仍然被调用】
// L29:没有 UNSAFE_ 前缀
componentWillReceiveProps(props) {
if (props.value !== this.state.progress) {
if (props.value >= 0 && props.value <= this.props.maxValue) {
this.setState({ progress: props.value }, () => {
if (this.state.progress === this.props.maxValue) {
const callback = this.props.onComplete;
if (callback) { setTimeout(callback, this.props.barAnimationDuration); }
}
});
}
}
}
这段代码承担了关键职责:state.progress 只在这里更新,而 animateWidth()(在 componentDidUpdate 里调用)读的正是 this.state.progress。
⇒ 所以它一旦不再被调用,就会连锁出现**“宽度不跟着变 + onComplete 永不触发”**。
实测(页面周期性读实例的 state,并与 props.value 对比):
PROBE | instance.state.progress = 0 / props.value = 0
PROBE | instance.state.progress = 50 / props.value = 50 ← ★ 跟随了
PROBE | instance.state.progress = 100 / props.value = 100
EVENT | onComplete 触发(第 1 次) ← ★ onComplete 也触发了
PROBE | instance.state.progress = 0 / props.value = 0
⇒ 在这套 React / RNOH 上,旧生命周期仍然被调用,state 正常跟随、onComplete 正常触发 ✓✓
⇒ 但它已被 React 废弃 ⇒ 记作迁移风险(升级 React 大版本时要重点回归这条链路)✓
2.2 Animated 没有传 useNativeDriver —— 必然告警
Animated.timing(this.widthAnimation, {
easing: Easing[this.props.barEasing],
toValue: …,
duration: this.props.barAnimationDuration,
}).start(); // ★ 没有 useNativeDriver
⇒ 而它驱动的是 width(布局属性)⇒ 本来就应该显式写 useNativeDriver: false ✓
实测(LogBox 通知原文):
"Animated: `useNativeDriver` was not specified. This is a required option and must be explicitly set to `true` or `false`"
⇒ 与"useNativeDriver 是必填项"这条规则完全一致 ✓
⇒ 实践含义:这类库在当前 RN 上会出现一条常驻通知;如果你要自己封装进度条,记得显式写上 useNativeDriver: false ✓
⚠️ 有一点和预期不同:没有出现"旧生命周期已废弃"的告警(只有
useNativeDriver这一条)。
所以"用componentWillReceiveProps会被警告"这个印象,在当前 React 版本上并不成立 —— 以实测为准 ✓
★★★ 三、一个"dump 盲区"的补充:宽度为 0 的节点不出现
value=0 时 toValue 被保护成 0:
toValue: toValue > 0 ? toValue : 0
实测:value=0 的两个相位(P0 与 P3)里,"高 39px 的条"一个都找不到:
---- P0:value=0 → 高 39px 的条共 0 个 ----
---- P3:value=0 → 高 39px 的条共 0 个 ----
---- P1:value=50 → w=444(命中) ----
---- P2:value=100 → w=894(命中) ----
⇒ "宽度 0 的节点"和"opacity: 0 的节点"一样,都不会出现在布局 dump 里 ✓
⇒ ⇒ 结论扩充:凡是用"尺寸归零"或"透明度归零"来隐藏的组件,布局 dump 对它是盲区 ——
判定"到底渲染了没有"必须用 JS 侧证据(实例 state、日志),不能只看 dump ✓
★★★★ 四、契约反射:12 项 defaultProps + 7 个原型方法全部命中
4.1 defaultProps(12 项,逐项精确)
NOTE | defaultProps = {"value":0,"maxValue":100,"barEasing":"linear",
"barAnimationDuration":500,"backgroundAnimationDuration":2500,"height":15,
"backgroundColor":"#148cF0","backgroundColorOnComplete":null,"borderWidth":1,
"borderColor":"#C8CCCE","borderRadius":6,"onComplete":null}
PASS × 12(每一项都用 JSON 比对)
⇒ 注意 backgroundColor 是 #148cF0(源码里大小写就是这个样子,F 是大写)✓
4.2 原型方法(7 个,全在 prototype 上)
const PROTO = ['constructor','componentDidMount','componentWillReceiveProps',
'componentDidUpdate','animateWidth','animateBackground','render'];
⇒ NOTE | 原型方法命中数(源码 7 个) = 7 PASS
⇒ 这个库全用原型方法、没有箭头类字段 ⇒ 这种库必须用 ref 才能调内部方法;
而箭头类字段的库(方法挂在实例上)又不一样 —— 写断言前先看源码是哪种写法 ✓
4.3 ⚠️ 一个"源码与运行时不一致"的事实:isRequired 标记读不到
源码里明明写着:
width: PropTypes.number.isRequired,
但运行时读到的是:
NOTE | propTypes 的键数 = 13
NOTE | propTypes 的键 = value, maxValue, barEasing, barAnimationDuration,
backgroundAnimationDuration, width, height, backgroundColor,
backgroundColorOnComplete, borderWidth, borderColor, borderRadius, onComplete
FAIL | ★ width 是 isRequired | 期望=true | 实际=false
⇒ propTypes 的 13 个键都在(和源码一致 ✓),但 width.isRequired 读出来是 false ✓
⇒ ⇒ 记作实测事实:不要用"运行时读 propTypes"来判断某个 prop 是否必填(它取决于 prop-types 的实现与构建模式);
要判断必填性,看源码或 .d.ts ✓
⚠️ 这一条也顺手纠正了我自己的一个错误:我最初把
propTypes的项数写成 15(实际 13)——
断言里的"数量"最好从源码里数清楚,而不是凭印象写。




五、接入与预检
| 步 | 做法 | 结果 |
|---|---|---|
| ① | harmony.autolinking / harmony/ / ios/ / android/ | 都没有 |
| ② | 原生痕迹预扫 | ✅ 无原生源文件(NativeModules/requireNativeComponent 全空) |
| ③ | ★ 老写法九项预扫 | ⚠️ 命中 1 项:componentWillReceiveProps(不带 UNSAFE_ 前缀);其余 8 项全空 |
| ④ | measure() / measureInWindow / onLayout / findNodeHandle | ✅ 全空(没有用"量容器宽度"那套 ⇒ 少了一类环境坑) |
| ⑤ | 平台分支 | ✅ Platform.OS / Platform.select 全空 |
| ⑥ | useNativeDriver | ⚠️ 一处都没写(⇒ 必然出现 useNativeDriver was not specified 通知) |
| ⑦ | 依赖 | ✅ 只有 prop-types ^15.6.0(宿主已有)⇒ 零外部原生依赖 |
| ⑧ | scripts 扫 prepare 家族 | ✅ 无(只有 test + lint)⇒ 无脚本阻塞 |
老写法九项逐项结果:
| 项 | 结果 |
|---|---|
React.createClass | ✅ 空 |
React.PropTypes | ✅ 空 |
从 react 具名导入 PropTypes | ✅ 空 |
ViewPropTypes / TextPropTypes | ✅ 空 |
字符串 ref="x" / this.refs.x | ✅ 空 |
mixins: | ✅ 空 |
componentWillReceiveProps | ⚠️ 命中 1 处(L29,无 UNSAFE_ 前缀) |
componentWillMount | ✅ 空 |
measure( / measureInWindow | ✅ 空 |
它还用了正确的现代写法:class + componentDidMount + componentDidUpdate ✓(只有那一个旧生命周期)
# 从适配仓库安装(唯一依赖 prop-types,宿主通常已有)
npm install "git+https://atomgit.com/oh-react-native/react-native-progress-bar-animated.git#1.0.6-ohos-1.0.0" --ignore-scripts
六、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | index.js(101 B,re-export)+ src/AnimatedProgressBar.js(3879 B) ⇒ 单体组件、源码可读 ✓ |
spec.json | ⚠️ 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4"(verification: evidence/batch50/…)⇒ 两字段自相矛盾(rnoh 0.84.4 是配 RN 0.84.x 的),且既不是当前宿主(0.84.1/0.84.3)也不是权威 0.88 基线 |
| ⚠️ 许可证 | 上游 GitHub 的 license 字段为空;交付包与 npm 均记 ISC |
| 交付包自带契约测试 | 只校验"包名/版本/入口文件存在/有中文鸿蒙说明"四件事(不渲染组件) |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。契约与几何结论跨基线成立;
但第二节那条"旧生命周期是否还被调用"必须随 React 大版本复验(这是本轮唯一带时效性的结论)。
七、已知限制与本次验证边界
7.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★★★ componentWillReceiveProps 已被 React 废弃 | 当前仍被调用(实测 state.progress 跟随、onComplete 触发)⇒ 升级 React 时要重点回归这条链路 |
★★★ Animated 未传 useNativeDriver | 会常驻一条 Animated: 'useNativeDriver' was not specified… 通知;自己封装时应显式写 useNativeDriver: false |
| ★★★★ 内层宽度公式 | width × 百分比 − borderWidth × 2(vp)⇒ density 3 下乘 3 得像素 |
★★ width 是必填 prop | 没有默认值;不传会导致 NaN 参与计算 |
★ height 默认 15、borderWidth 默认 1 | ⇒ 内层高度 = height − 2(默认 13vp = 39px) |
★ onComplete 只在"值达到 maxValue"时触发 | 且是 setTimeout(callback, barAnimationDuration) 之后 |
★ backgroundColorOnComplete 为空时不构造插值 | 走 this.props.backgroundColor 直传 |
| ★ 全用原型方法(无箭头类字段) | 要调内部方法必须用 ref |
★ barEasing 有 6 个白名单值 | bounce / cubic / ease / sin / linear / quad(取自 Easing[...]) |
| ⚠️ 宽度为 0 的节点不出现在布局 dump | 用 dump 判断"有没有渲染"会漏判 |
7.2 本次验证的边界
- 未做真机验证(只在模拟器上;未做跨平台对照);
- 未逐帧采集动画曲线(只核对了各相位的终点宽度:0 / 444 / 669 / 894);
- 未验证
barEasing各取值的实际曲线(E3 用了bounce,只核对了终点宽度); - 未验证
barAnimationDuration/backgroundAnimationDuration的时序(只知onComplete会在动画后触发); - 未验证
backgroundColorOnComplete的颜色插值结果(dump 不反映颜色;只验证了它不崩、几何正确); - 未验证
borderRadius/borderColor的视觉效果; - 未验证
maxValue取非 100 值时的行为; - 未验证 传入超出
[0, maxValue]的value(源码里有范围判断,但只从源码推断); - 未验证 反复快速改
value时的动画行为; - 未在
RN 0.88.0/RNOH 0.84.4上复验(交付包声明的基线); - 未测其它屏幕宽度。
未改动库代码。
八、常见问题
Q1:它在 RN 0.84 上能跑吗?
能。 几何七组像素级预言全部命中(900/894/444/669/72/30/39),defaultProps 12 项、原型方法 7 个全部核对一致,
state.progress 正常跟随 props.value,onComplete 正常触发。
Q2:为什么控制台一直有一条 useNativeDriver 通知?
因为库的 Animated.timing(...) 没有传 useNativeDriver。而它驱动的 width 是布局属性,本来就该显式写 false。
⇒ 这条通知不影响功能;自己封装进度条时记得写上。
Q3:我改 value 为什么宽度没变?
先确认两点:① width 是必填(不传会算出 NaN);② 这个库把 state.progress 放在 componentWillReceiveProps 里更新 ——
若将来 React 版本移除了该生命周期,state 就不会跟随(本次实测仍可用)。
Q4:怎么算内层条有多宽?
内层宽度(vp) = (width * value / 100) - borderWidth * 2
内层高度(vp) = height - borderWidth * 2
默认 borderWidth=1、height=15 ⇒ 内层高 13vp(density 3 ⇒ 39px)。
例:width=300, value=50 ⇒ 148vp ⇒ 444px(实测一致 ✓)。
Q5:onComplete 什么时候触发?
只有 value 达到 maxValue 时(且是 setTimeout(callback, barAnimationDuration) 之后)。
⚠️ 它写在 componentWillReceiveProps 的 setState 回调里 ⇒ 与 Q3 是同一处依赖。
Q6:可以只画一根静态条吗?
可以,传 value 即可(组件挂载时 componentDidMount 会 animateWidth())。
但注意只有 value > 0 才会开跑,value=0 时内层宽度为 0(dump 里看不到它)。
Q7:为什么我在布局 dump 里找不到进度条?
宽度为 0 的节点不会被列进布局 dump(和 opacity: 0 的节点一样)。
要用 JS 侧证据(实例 state、日志)判断它是否渲染。
小结
react-native-progress-bar-animated 是单体组件、只依赖 prop-types、零平台分支的进度条库,
几何可以做到像素级核对(本轮 7 组预言全部命中),设备侧表现正常。
最值得记下来的是这几条:
-
★★★★★ 一行公式、七组像素级命中。
内层宽度 = width × value / 100 − borderWidth × 2
⇒ 实测900 / 894 / 444 / 669(外层、value=100、value=50、value=75)与72 / 30 / 39(两个外层高度、内层高度)全部对上 ✓✓
⇒ 只要组件是"按数值算尺寸",几何就是最硬的判据。 -
★★★★
componentWillReceiveProps在 React 19 上仍被调用(但已废弃)。
实测state.progress跟随props.value(0→50→100→0)、onComplete触发 ✓
⇒ 我原本担心"旧生命周期失效 ⇒ 宽度不变 +onComplete永不触发",实测推翻了这个担心 ✓
⇒ 但它承担的是关键链路(state.progress只在这里更新)⇒ 升级 React 时必须回归。 -
★★★★
Animated未传useNativeDriver⇒ 必有通知。
Animated: 'useNativeDriver' was not specified. This is a required option…
⇒ 自己封装时要显式写useNativeDriver: false(驱动的是width,不能用原生驱动)✓ -
★★★ 又一处"dump 盲区":宽度为 0 的节点不出现。
value=0的两个相位里"高 39px 的条"一个都找不到 ⇒ 与opacity: 0同类 ✓
⇒ 用尺寸归零/透明度归零来隐藏的组件,布局 dump 都是盲区;判定渲染要用 JS 侧证据。 -
★★★ 契约反射很便宜:
defaultProps12 项 + 原型方法 7 个,全用JSON比对。
且要注意这个库全用原型方法(无箭头类字段) ⇒ 与"方法挂在实例上"的库写法相反,
断言前先看源码是哪种 ✓ -
★★
propTypes的isRequired标记运行时读不到。
源码是width: PropTypes.number.isRequired,但运行时pt.width.isRequired === false(13 个键却都在)
⇒ 判断必填性请看源码或.d.ts,别靠运行时反射 ✓ -
★ 一条自我纠正:断言的"数量"要从源码数清楚。
我一开始把propTypes写成 15 项,实际 13 项 ⇒ 自造的 FAIL ✓ -
★ 判据逐条直写日志(
PASS|描述|期望|实际)+ 分相位驱动value⇒
采集端grep即得全证据,且相位号打屏让采样时机可事后对齐 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-progress-bar-animated(上游 1.0.6 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-progress-bar-animated |
| 适配 TAG | 1.0.6-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 库类型 | 带动画的进度条 |
| 运行时依赖 | ✅ 只有 prop-types ^15.6.0(宿主通常已有) |
prepare / postinstall | ✅ 都没有 |
| 许可证 | ISC(上游 GitHub license 字段为空) |
| 上游仓库 | https://github.com/rafaelmotta/react-native-progress-bar-animated(★52) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = ProgressBarTestApp) |
# 从适配仓库安装(唯一依赖 prop-types)
npm install "git+https://atomgit.com/oh-react-native/react-native-progress-bar-animated.git#1.0.6-ohos-1.0.0" --ignore-scripts
import AnimatedProgressBar from 'react-native-progress-bar-animated';
<AnimatedProgressBar
width={300} // ★ 必填(无默认值)
value={50} // 0..maxValue(默认 maxValue=100)
height={15} // 默认 15 ⇒ 内层高 = 15-2 = 13vp = 39px
borderWidth={1} // 默认 1 ⇒ 参与宽度计算
backgroundColor="#148cF0" // 默认值(注意源码里 F 是大写)
backgroundColorOnComplete="#0A7D2C" // 走满时把底色渐变过去
barEasing="linear" // bounce | cubic | ease | sin | linear | quad
barAnimationDuration={500}
backgroundAnimationDuration={2500}
onComplete={() => console.log('走满了')} // 仅在 value 达到 maxValue 时,动画后触发
/>
// 内层宽度(vp) = width * value / 100 - borderWidth * 2
// 内层高度(vp) = height - borderWidth * 2
// ⚠️ 组件用的是【已废弃的 componentWillReceiveProps】(无 UNSAFE_ 前缀),
// 且 state.progress 只在那里更新 ⇒ 升级 React 时重点回归
// ⚠️ 库的 Animated.timing 没写 useNativeDriver ⇒ 会有常驻通知;自己封装请写 false
// ⚠️ 宽度为 0 的节点不会出现在布局 dump 里(和 opacity:0 同类)
// ⚠️ propTypes 的 isRequired 标记运行时读不到(判断必填请看源码/.d.ts)
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey ProgressBarTestApp
判据的复现方式
# ① 契约 / 几何预言:逐条 PASS/FAIL 日志
hdc shell "hilog -x | grep pbar-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE|PROBE|EVENT'
# ⇒ defaultProps 12 项、原型方法 7 个、colSize 类比公式、state.progress 是否跟随
# ② 几何:内层条高 39px(13vp×3),按宽度核对
devecocli ui layout | Select-String '\[(\d+),(\d+),(\d+),(\d+)\]'
# ⇒ value=100 → w=894;value=50 → w=444;value=75(E3)→ w=669;外层 h=45/72/30
# ③ LogBox:useNativeDriver 未指定的通知
devecocli ui layout | Select-String 'logbox_notification_message_text'
# ④ 注意:value=0 时内层宽度 0 ⇒ 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 = 39vp |
| 宿主依赖 | ✅ 无需补装(只有 prop-types,宿主已有) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 51 秒 ⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 几何 7 组像素级预言全部命中;契约 12 项 defaultProps + 7 个原型方法;旧生命周期链路取证(state.progress 跟随 4 个相位 + onComplete 触发);LogBox 1 条与预言一致;4 张截图互异;判据 2 条 FAIL 均为我自己断言写错(已如实记录) |
⚠️ 基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
契约与几何结论跨基线成立;"旧生命周期是否仍被调用"这一条带时效性,需随 React 大版本复验。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)