本文记录把 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=1300vp / 15vp900 × 45px✅ y=1257 w=900 h=45
E1 内层填充value=100(= maxValue)300×100/100 − 2 = 298vp894px✅ y=1260 **w=894** h=39
E1 内层填充value=50300×50/100 − 2 = 148vp444px✅ w=444
E3 内层填充value=75、height=10300×75/100 − 2 = 223vp669px✅ y=1715 **w=669** h=24
E2 外层容器height=2424vp72px✅ y=1471 w=900 h=72
E3 外层容器height=1010vp30px✅ y=1712 w=900 h=30
内层填充高度任意height − borderWidth×2 = 15−2 = 13vp39px✅ 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 本次验证的边界

  1. 未做真机验证(只在模拟器上;未做跨平台对照);
  2. 未逐帧采集动画曲线(只核对了各相位的终点宽度:0 / 444 / 669 / 894);
  3. 未验证 barEasing 各取值的实际曲线(E3 用了 bounce,只核对了终点宽度);
  4. 未验证 barAnimationDuration / backgroundAnimationDuration 的时序(只知 onComplete 会在动画后触发);
  5. 未验证 backgroundColorOnComplete 的颜色插值结果(dump 不反映颜色;只验证了它不崩、几何正确);
  6. 未验证 borderRadius / borderColor 的视觉效果;
  7. 未验证 maxValue 取非 100 值时的行为;
  8. 未验证 传入超出 [0, maxValue] 的 value(源码里有范围判断,但只从源码推断);
  9. 未验证 反复快速改 value 时的动画行为;
  10. 未在 RN 0.88.0 / RNOH 0.84.4 上复验(交付包声明的基线);
  11. 未测其它屏幕宽度。

未改动库代码。


八、常见问题

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 组预言全部命中),设备侧表现正常。

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

  1. ★★★★★ 一行公式、七组像素级命中。
    内层宽度 = width × value / 100 − borderWidth × 2
    ⇒ 实测 900 / 894 / 444 / 669(外层、value=100、value=50、value=75)与 72 / 30 / 39(两个外层高度、内层高度)全部对上 ✓✓
    ⇒ 只要组件是"按数值算尺寸",几何就是最硬的判据。

  2. ★★★★ componentWillReceiveProps 在 React 19 上仍被调用(但已废弃)。
    实测 state.progress 跟随 props.value(0→50→100→0)、onComplete 触发 ✓
    ⇒ 我原本担心"旧生命周期失效 ⇒ 宽度不变 + onComplete 永不触发",实测推翻了这个担心 ✓
    ⇒ 但它承担的是关键链路(state.progress 只在这里更新)⇒ 升级 React 时必须回归。

  3. ★★★★ Animated 未传 useNativeDriver ⇒ 必有通知。
    Animated: 'useNativeDriver' was not specified. This is a required option…
    ⇒ 自己封装时要显式写 useNativeDriver: false(驱动的是 width,不能用原生驱动)✓

  4. ★★★ 又一处"dump 盲区":宽度为 0 的节点不出现。
    value=0 的两个相位里"高 39px 的条"一个都找不到 ⇒ 与 opacity: 0 同类 ✓
    ⇒ 用尺寸归零/透明度归零来隐藏的组件,布局 dump 都是盲区;判定渲染要用 JS 侧证据。

  5. ★★★ 契约反射很便宜:defaultProps 12 项 + 原型方法 7 个,全用 JSON 比对。
    且要注意这个库全用原型方法(无箭头类字段) ⇒ 与"方法挂在实例上"的库写法相反,
    断言前先看源码是哪种 ✓

  6. ★★ propTypes 的 isRequired 标记运行时读不到。
    源码是 width: PropTypes.number.isRequired,但运行时 pt.width.isRequired === false(13 个键却都在)
    ⇒ 判断必填性请看源码或 .d.ts,别靠运行时反射 ✓

  7. ★ 一条自我纠正:断言的"数量"要从源码数清楚。
    我一开始把 propTypes 写成 15 项,实际 13 项 ⇒ 自造的 FAIL ✓

  8. ★ 判据逐条直写日志(PASS|描述|期望|实际)+ 分相位驱动 value ⇒
    采集端 grep 即得全证据,且相位号打屏让采样时机可事后对齐 ✓


本篇用到的库

项内容
三方库react-native-progress-bar-animated(上游 1.0.6 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-progress-bar-animated
适配 TAG1.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 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 = 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

Logo

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

更多推荐