本文记录把react-native-progress-bar-animated(带动画的进度条)适配到 HarmonyOS 的完整过程。
它做的事很专一:画一根进度条—— 外层是带边框的容器,内层是一条Animated驱动的填充条,宽度按百分比增长;可选在"走满"时把底色渐变过去、并回调一个onComplete。
结论:可以用,而且几何能做到像素级精确核对(7 组预言全部命中)。但它有两处**"现在能跑、但有隐患"的写法,还有一个契约层面的运行时事实与源码不一致**,这三点比"能不能跑"更值得写下来。
★★★★★ 一、几何:7 组像素级预言全部精确命中
核心公式只有一行(src/AnimatedProgressBar.js的animateWidth):
animateWidth(){consttoValue=((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.1componentWillReceiveProps—— 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){constcallback=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.2Animated没有传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.1defaultProps(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上)
constPROTO=['constructor','componentDidMount','componentWillReceiveProps','componentDidUpdate','animateWidth','animateBackground','render'];⇒NOTE|原型方法命中数(源码7个)=7PASS⇒这个库全用原型方法、没有箭头类字段⇒ 这种库必须用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,宿主通常已有)npminstall"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是配 RN0.84.x的),且既不是当前宿主(0.84.1/0.84.3)也不是权威 0.88 基线 |
| ⚠️ 许可证 | 上游 GitHub 的license字段为空;交付包与 npm 均记ISC |
| 交付包自带契约测试 | 只校验"包名/版本/入口文件存在/有中文鸿蒙说明"四件事(不渲染组件) |
关于基线:本次验证在RN0.84.1+ RNOH0.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),defaultProps12 项、原型方法 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)npminstall"git+https://atomgit.com/oh-react-native/react-native-progress-bar-animated.git#1.0.6-ohos-1.0.0"--ignore-scriptsimport 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-bcom.rnoh084.demo-aEntryAbility--psrnAppKey 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-openharmony0.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 |
| 本次构建 | assembleHap3 分 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