React Native for OpenHarmony 实战:三方库 react-native-animated-ellipsis 的鸿蒙化适配指南
本文记录把 react-native-animated-ellipsis(加载中的动画省略号)适配到 HarmonyOS 的完整过程。
它做的事很专一:渲染 N 个点,让它们逐个渐亮,形成"加载中……"的循环动画。
结论:可用,而且是"判据最漂亮"的一轮 —— 我用"点文本节点计数"和"字号比"两把尺子,把它的行为精确核对到了小数点后两位。
另外它有一个值得记下来的前置条件:它引用了 RN 已移除的 Text.propTypes,本来会 import 即崩 —— 而工程里已有的一个 polyfill 正好把它也救了。
★★★★★ 一、2179 B 的全部实现
dist/AnimatedEllipsis.js,去掉注释后就是这么多:
export default class AnimatedEllipsis extends Component {
constructor(props) {
super(props);
this._animation_state = {
dot_opacities: this.initializeDots(), // ★ 只在 constructor 跑一次
target_opacity: 1,
should_animate: true,
};
}
initializeDots() {
const opacities = [];
for (let i = 0; i < this.props.numberOfDots; i++) {
opacities.push(new Animated.Value(this.props.minOpacity));
}
return opacities;
}
componentDidMount() { this.animate_dots.bind(this)(0); }
componentWillUnmount() { this._animation_state.should_animate = false; }
animate_dots(which_dot) {
if (!this._animation_state.should_animate) return;
if (which_dot >= this._animation_state.dot_opacities.length) {
which_dot = 0;
const min = this.props.minOpacity;
this._animation_state.target_opacity = this._animation_state.target_opacity == min ? 1 : min; // ★ 来回翻转
}
const next_dot = which_dot + 1;
Animated.timing(this._animation_state.dot_opacities[which_dot], {
toValue: this._animation_state.target_opacity,
duration: this.props.animationDelay,
}).start(this.animate_dots.bind(this, next_dot)); // ★ 链式:一个结束再动下一个
}
render() {
const dots = this._animation_state.dot_opacities.map((o, i) =>
<Animated.Text key={i} style={[this.props.style, {opacity: o}]}>{' '}{'.'}</Animated.Text>
);
return <View style={styles.container}>{dots}</View>; // container: {flexDirection: 'row'}
}
}
两个要点:
- 每个点是一个
Animated.Text,文本内容是' .'(一个空格 + 一个点) ⇒ 在布局 dump 里可以数出来 ✓ initializeDots()只在构造函数里调用一次 ⇒ 运行中改numberOfDots不会重建点 ⚠️(这是使用限制,所以我用三个不同实例做对照)
defaultProps / propTypes(整块读出来的原文):
AnimatedEllipsis.propTypes = {
numberOfDots: PropTypes.number,
animationDelay: PropTypes.number,
minOpacity: PropTypes.number,
style: Text.propTypes.style, // ★ L74 —— 就是这一行会崩
};
AnimatedEllipsis.defaultProps = {
numberOfDots: 3,
animationDelay: 300,
minOpacity: 0,
style: {color: '#aaa', fontSize: 32},
};
★★★★★ 二、前置条件:它引用了已移除的 Text.propTypes
AnimatedEllipsis.js L74:
style: Text.propTypes.style,
RN 从 0.62 起移除了 Text.propTypes(我在 RN 0.84 上确证过:Libraries/Text/Text.js 里 propTypes 命中 = 0),
所以这一行会在模块加载期抛 Cannot read property 'style' of undefined ⇒ 库 import 即崩 ✗
但这次不用再写一遍补丁 —— 工程里已有的 polyfills/text-prop-types.js 已经在入口文件的第 28 行引入了:
// index.js(第 28 行,在所有页面 import 之前)
import './polyfills/text-prop-types';
实测(页面判据确认它还生效):
NOTE | Text.propTypes 是否存在(应为 object) = object
PASS | ★★★ polyfill 生效:Text.propTypes 存在(否则本库 import 即崩)
⇒ ⇒ 一个 20 行的兼容补丁,修好了两个库 ✓✓ ——
这类"只在 propTypes 声明里引用已移除 API"的老库,补丁是通用的。
★★★★★ 三、判据一:点文本的计数精确等于 numberOfDots
我放了三个实例(numberOfDots = 3 / 5 / 2),然后数布局 dump 里"内容是 ' .' 的 Text 节点":
" ." w=49 h=113 x=296 y=1331 ← E1 第 1 个点
" ." w=49 h=113 x=344 y=1331 ← E1 第 2 个点
" ." w=49 h=113 x=392 y=1331 ← E1 第 3 个点
" ." w=49 h=113 x=197 y=1601 ← E2 第 1 个点
" ." w=49 h=113 x=245 y=1601 ← E2 第 2 个点
" ." w=49 h=113 x=293 y=1601 ← E2 第 3 个点
" ." w=49 h=113 x=341 y=1601 ← E2 第 4 个点
" ." w=49 h=113 x=389 y=1601 ← E2 第 5 个点
" ." w=37 h=84 x=145 y=1871 ← E3 第 1 个点
" ." w=37 h=84 x=181 y=1871 ← E3 第 2 个点
⇒ 点文本节点数 = 10(预期 10 = 3+5+2) ✓✓
三组各自的数量完全对上:
| 实例 | numberOfDots | 实测点数 | y 坐标 | 每个点的宽度 |
|---|---|---|---|---|
| E1 | 3 | ✅ 3 | 1331 | 49 |
| E2 | 5 | ✅ 5 | 1601 | 49 |
| E3 | 2 | ✅ 2 | 1871 | 37 |
而且同一组内的 x 间隔恰好等于点宽度(344-296=48≈49、245-197=48≈49)⇒ 点是一个紧挨一个排的 ✓
★★★★★ 四、判据二:字号比的三重一致(这一条最有说服力)
E3 传了 style={{fontSize: 24}},其余两个用默认 fontSize: 32。于是可以做一个三路交叉验证:
| 量 | E1/E2(fontSize 32) | E3(fontSize 24) | 比值 |
|---|---|---|---|
| 点的宽度 | 49 | 37 | 0.755 |
| 点的行高 | 113 | 84 | 0.743 |
| 字号本身 | 32 | 24 | 0.750 |
⇒ 三个比值全部落在 0.74~0.76 之间,与 24/32 = 0.75 吻合 ✓✓✓
这说明三件事同时成立:
styleprop 里的fontSize确实覆盖了defaultProps的 32 ✓- 这个字号确实影响了字形度量(宽度与行高都按比例缩小)✓
- RNOH 的文本测量是准确的(不是"看起来差不多",而是比例能对上)✓
⇒ 这种"用一个已知比值去核对多个独立测量值"的方法,比单点断言强得多 ✓
★★★ 五、动画部分:只能证明"结构对 + 不崩"
它用 Animated.timing(...) 逐个改变 opacity:
Animated.timing(this._animation_state.dot_opacities[which_dot], {
toValue: this._animation_state.target_opacity,
duration: this.props.animationDelay,
}).start(this.animate_dots.bind(this, next_dot));
⚠️ 两点必须写清:
opacity不进布局 ⇒ 布局 dump 里看不到"哪个点更亮" ⇒ 动画过程本轮无法用 dump 验证,只能看截图(四张截图互异,说明画面在变)✓Animated.timing没传useNativeDriver⇒ 按经验会有
Animated: 'useNativeDriver' was not specified警告 —— 但实测屏上无 LogBox ✓
⇒ 所以只能记"未观察到该警告",不能写"没有这个警告" ✓
LogBox 实测:屏上无通知 ✓




★★★ 六、其余预检结论
| 预检项 | 结果 |
|---|---|
ListView / createClass / React.PropTypes / ViewPropTypes / 字符串 ref / this.refs. / mixins / componentWillReceiveProps / componentWillMount / getDefaultProps | ✅✅ 全部为空(唯一的老写法就是那行 Text.propTypes) |
isRequired | ✅ 空(无必填 prop 陷阱) |
peerDependencies | ✅ 空(连 react/react-native 都没声明,但实际需要) |
Platform.OS / measure() / onLayout / NativeModules | ✅ 全空 |
dependencies | ⚠️ prop-types(宿主已有)+ babel-preset-stage-2(Babel 6 时代包,只用于构建) |
scripts.prepare | ❌ npm run build(babel) ⇒ 安装必须 --ignore-scripts |
| 交付包形态 | ⚠️ 只有 dist/(已编译的 ESM),没有 src/ |
七、接入与预检(要点)
# ★ 必须 --ignore-scripts:它的 scripts.prepare 是 "npm run build"(babel)
npm install "git+https://atomgit.com/oh-react-native/react-native-animated-ellipsis.git#2.0.0-ohos-1.0.0" --ignore-scripts
# ★ 还需要在入口最前面引入 Text.propTypes 的 polyfill(否则 import 即崩)
八、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | ⚠️ 只有 dist/AnimatedEllipsis.js(2179 B,已编译的 ESM)+ .babelrc ⇒ 没有 src/ |
spec.json | ⚠️ 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4" ⇒ 两字段自相矛盾 |
| 备注 | 库代码零改动(靠已有的 polyfill 兼容) |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
⚠️ 它的 Text.propTypes 是版本相关问题 ⇒ 换基线时需要 polyfill 才可用;其余结论跨基线成立。
九、已知限制与本次验证边界
9.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★★★★★ 必须补 Text.propTypes | L74 引用了 RN ≥0.62 已移除的 API ⇒ 不补就 import 即崩 |
★★★★★ 每个点是内容为 ' .' 的 Animated.Text | 所以"点数"完全可以数出来验证 |
★★★★ style.fontSize 会影响点的宽度与行高 | 实测 32→24 时,宽度/行高按 0.75 比例缩小 ✓ |
★★★★ initializeDots() 只在 constructor 跑 | 运行中改 numberOfDots 不会重建点 ⇒ 要变点数请换实例(或用 key 重挂载) |
★★★ Animated.timing 没传 useNativeDriver | 实测未观察到警告;opacity 不进布局 ⇒ 动画只能看截图 |
★★ scripts.prepare 会跑 babel | 安装必须 --ignore-scripts |
★★ 只有 dist/,没有 src/ | 想看源码得读已编译的 ESM |
★ dependencies 里有 babel-preset-stage-2 | Babel 6 时代包,只用于构建,运行时不需要 |
9.2 本次验证的边界
- ★★★ 动画过程(逐个渐亮)没有数值级验证(
opacity不进布局,只能靠截图"四张互异"间接说明画面在变); - ★★★ 未验证"运行中修改
numberOfDots"(源码显示不会重建,但没实测换 prop 的表现); - ★★ 未验证
animationDelay不同值的效果(Animated.timing的时长无法从 dump 读出); - ★★ 未验证
minOpacity的实际视觉下限; - 未验证 在
<Text>内部嵌套使用(README 的推荐用法是放在Loading文字后面); - 未验证 多个实例并存时的动画同步/性能;
- 未验证
numberOfDots很大的情况(如 20 个点)的排布; - 未在
RN 0.88.0/RNOH 0.84.4上复验; - 未测其它屏幕宽度;未做跨平台对照。
库代码未改动。
十、常见问题
Q1:它在 RN 0.84 上能跑吗?
加了 Text.propTypes 的 polyfill 之后能跑。 三个实例的点数(3/5/2)与总点数(10)精确对上;
字号覆盖引起的宽度/行高变化比例(0.755 / 0.743)与字号比(0.75)吻合;LogBox 无通知。
Q2:一装就报 Cannot read property 'style' of undefined?
因为 AnimatedEllipsis.js L74 用了 Text.propTypes.style,而 RN ≥0.62 已移除 Text.propTypes。加 polyfill:
// polyfills/text-prop-types.js
const {Text} = require('react-native');
if (!Text.propTypes) Text.propTypes = {style: {}};
// 入口文件最前面(所有业务 import 之前)
import './polyfills/text-prop-types';
Q3:为什么改了 numberOfDots 没反应?
因为它们在 constructor 里一次性创建(initializeDots()),之后不会重建。
要动态改点数:换一个实例,或给组件加 key 强制重挂载。
Q4:点的颜色/大小怎么改?
传 style:
<AnimatedEllipsis style={{color: '#D0021B', fontSize: 24}} />
实测 fontSize 从 32 改成 24 后,点的宽度从 49 变成 37、行高从 113 变成 84(比例都约 0.75)✓
Q5:为什么布局 dump 里看不到"哪个点更亮"?
动画改的是 opacity,而 opacity 不参与布局 ⇒ dump 里只有结构没有亮度。
要看动画效果 只能截图。
Q6:能放在一行文字中间吗?
可以(README 的推荐用法就是 Loading 后面接它),它外层是 flexDirection: 'row' 的 View。
⚠️ 本轮没验证这种内联布局,记为未验证。
Q7:npm install 报 babel 相关错误?
它的 scripts.prepare 是 npm run build(babel src/ -d dist),而包里没有 src/(只有 dist/)⇒ 安装时执行必然失败。
加 --ignore-scripts 即可。
小结
react-native-animated-ellipsis 是2179 B 的加载动画省略号:
渲染 N 个内容为 ' .' 的 Animated.Text,用链式 Animated.timing 让它们逐个渐亮。
老写法九项全空,唯一的历史包袱是引用了已移除的 Text.propTypes。
最值得记下来的是这几条:
-
★★★★★ 一个 polyfill 修好了两个库。
它栽在Text.propTypes.style上 —— 而这次不用再写一遍补丁,
之前那份polyfills/text-prop-types.js已经在入口第 28 行生效 ⇒ 兼容层是可复用的 ✓✓ -
★★★★★ 点文本计数:3 + 5 + 2 = 10,三组各自精确。
因为每个点都是独立的一个Text,所以"点数"这个抽象属性变成了可直接数的节点数 ✓
⇒ 挑"能被 dump 直接数出来的最小单元"做判据,是最省事的路子。 -
★★★★★ 字号比的三重一致(0.755 / 0.743 / 0.75)。
宽度比、行高比、字号比三者吻合 ⇒ 同时证明了"style 覆盖生效"、“字号确实影响字形度量”、“RNOH 文本测量准确”。
⇒ 用"一个已知比值"去核对"多个独立测量值",比单点断言强得多 ✓ -
★★★★ 又一次确认"改 prop 不生效"这类设计限制要提前读出来。
initializeDots()只在constructor跑 ⇒ 改numberOfDots不重建 ⇒ 所以 A/B 必须用不同实例 ✓
(读源码 2179 B 的成本几乎为零,收益是避免了"改 prop 没反应"的误判) -
★★★
opacity动画的验证边界要说清。
opacity不进布局 ⇒ dump 看不到 ⇒ 只能用截图间接说明画面在变,不能声称"验证了渐亮顺序" ✓ -
★★ 别把"预期会出现的警告"写成结论。
Animated.timing确实没传useNativeDriver,但实测屏上无 LogBox ⇒ 只写"未观察到" ✓ -
★
scripts.prepare与包结构不匹配会阻塞安装:
它的prepare是babel src/ -d dist,而包内只有dist/⇒ 执行必失败 ⇒--ignore-scripts是唯一正确姿势 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-animated-ellipsis(上游 2.0.0 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-animated-ellipsis |
| 适配 TAG | 2.0.0-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | ✅ 都不需要 |
| 库类型 | 加载动画省略号(N 个点逐个渐亮) |
| 导出形态 | 默认导出 |
| 运行时依赖 | prop-types(宿主已有) |
| ⚠️ 必须的额外步骤 | 入口最前面引入 Text.propTypes polyfill(否则 import 即崩) |
⚠️ scripts.prepare | npm run build(babel)⇒ 安装需 --ignore-scripts |
| 许可证 | MIT |
| 上游仓库 | https://github.com/adorableio/react-native-animated-ellipsis |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = AnimatedEllipsisTestApp) |
npm install "git+https://atomgit.com/oh-react-native/react-native-animated-ellipsis.git#2.0.0-ohos-1.0.0" --ignore-scripts
import AnimatedEllipsis from 'react-native-animated-ellipsis';
// 基本用法(默认 3 个点、delay 300、minOpacity 0、fontSize 32)
<Text>Loading<AnimatedEllipsis /></Text>
// 自定义点数 / 速度 / 样式
<AnimatedEllipsis numberOfDots={5} animationDelay={500} style={{color: '#D0021B', fontSize: 24}} />
// ⚠️ 必须先在入口最前面引入 Text.propTypes 的 polyfill(RN ≥0.62 已移除该 API)
// ⚠️ initializeDots 只在 constructor 跑 ⇒ 改 numberOfDots 不会重建点(要变点数请换实例或加 key)
// ⚠️ Animated.timing 未传 useNativeDriver;opacity 不进布局 ⇒ 动画只能看截图
// ⚠️ 安装必须 --ignore-scripts(prepare = babel build,而包里没有 src/)
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey AnimatedEllipsisTestApp
判据的复现方式
# ① 契约:4 项 defaultProps + polyfill 是否生效
hdc shell "hilog -x | grep aev-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
# ② ★ 数点文本:内容是 ' .' 的 Text 节点
devecocli ui layout | Select-String 'Text#Paragraph'
# ⇒ 三个实例的点数应分别为 3 / 5 / 2,总数 10
# ③ ★ 字号比:fontSize 24 的实例,宽度与行高应约为 fontSize 32 的 0.75 倍
# 实测 宽度 37 vs 49(0.755)、行高 84 vs 113(0.743)
验证环境
| 项 | 版本 |
|---|---|
| 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(已有)+ Text.propTypes polyfill |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 32 秒 ⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 契约 11/11 全 PASS;点文本计数精确 10(3+5+2);字号比三重一致(0.755 / 0.743 / 0.75);LogBox 无通知;4 张截图互异 |
⚠️ 基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
⚠️ 它的Text.propTypes是版本相关问题(需 polyfill);其余结论跨基线成立。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)