本文记录把 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'}
  }
}

两个要点:

  1. 每个点是一个 Animated.Text,文本内容是 ' .'(一个空格 + 一个点) ⇒ 在布局 dump 里可以数出来 ✓
  2. 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 坐标每个点的宽度
E13✅ 3133149
E25✅ 5160149
E32✅ 2187137

而且同一组内的 x 间隔恰好等于点宽度(344-296=48≈49、245-197=48≈49)⇒ 点是一个紧挨一个排的 ✓

★★★★★ 四、判据二:字号比的三重一致(这一条最有说服力)

E3 传了 style={{fontSize: 24}},其余两个用默认 fontSize: 32。于是可以做一个三路交叉验证:

量E1/E2(fontSize 32)E3(fontSize 24)比值
点的宽度49370.755
点的行高113840.743
字号本身32240.750

⇒ 三个比值全部落在 0.74~0.76 之间,与 24/32 = 0.75 吻合 ✓✓✓

这说明三件事同时成立:

  1. style prop 里的 fontSize 确实覆盖了 defaultProps 的 32 ✓
  2. 这个字号确实影响了字形度量(宽度与行高都按比例缩小)✓
  3. 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));

⚠️ 两点必须写清:

  1. opacity 不进布局 ⇒ 布局 dump 里看不到"哪个点更亮" ⇒ 动画过程本轮无法用 dump 验证,只能看截图(四张截图互异,说明画面在变)✓
  2. 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.propTypesL74 引用了 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-2Babel 6 时代包,只用于构建,运行时不需要

9.2 本次验证的边界

  1. ★★★ 动画过程(逐个渐亮)没有数值级验证(opacity 不进布局,只能靠截图"四张互异"间接说明画面在变);
  2. ★★★ 未验证"运行中修改 numberOfDots"(源码显示不会重建,但没实测换 prop 的表现);
  3. ★★ 未验证 animationDelay 不同值的效果(Animated.timing 的时长无法从 dump 读出);
  4. ★★ 未验证 minOpacity 的实际视觉下限;
  5. 未验证 在 <Text> 内部嵌套使用(README 的推荐用法是放在 Loading 文字后面);
  6. 未验证 多个实例并存时的动画同步/性能;
  7. 未验证 numberOfDots 很大的情况(如 20 个点)的排布;
  8. 未在 RN 0.88.0 / RNOH 0.84.4 上复验;
  9. 未测其它屏幕宽度;未做跨平台对照。

库代码未改动。


十、常见问题

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。

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

  1. ★★★★★ 一个 polyfill 修好了两个库。
    它栽在 Text.propTypes.style 上 —— 而这次不用再写一遍补丁,
    之前那份 polyfills/text-prop-types.js 已经在入口第 28 行生效 ⇒ 兼容层是可复用的 ✓✓

  2. ★★★★★ 点文本计数:3 + 5 + 2 = 10,三组各自精确。
    因为每个点都是独立的一个 Text,所以"点数"这个抽象属性变成了可直接数的节点数 ✓
    ⇒ 挑"能被 dump 直接数出来的最小单元"做判据,是最省事的路子。

  3. ★★★★★ 字号比的三重一致(0.755 / 0.743 / 0.75)。
    宽度比、行高比、字号比三者吻合 ⇒ 同时证明了"style 覆盖生效"、“字号确实影响字形度量”、“RNOH 文本测量准确”。
    ⇒ 用"一个已知比值"去核对"多个独立测量值",比单点断言强得多 ✓

  4. ★★★★ 又一次确认"改 prop 不生效"这类设计限制要提前读出来。
    initializeDots() 只在 constructor 跑 ⇒ 改 numberOfDots 不重建 ⇒ 所以 A/B 必须用不同实例 ✓
    (读源码 2179 B 的成本几乎为零,收益是避免了"改 prop 没反应"的误判)

  5. ★★★ opacity 动画的验证边界要说清。
    opacity 不进布局 ⇒ dump 看不到 ⇒ 只能用截图间接说明画面在变,不能声称"验证了渐亮顺序" ✓

  6. ★★ 别把"预期会出现的警告"写成结论。
    Animated.timing 确实没传 useNativeDriver,但实测屏上无 LogBox ⇒ 只写"未观察到" ✓

  7. ★ 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
适配 TAG2.0.0-ohos-1.0.0
需要 HAR / 权限 / ohpm✅ 都不需要
库类型加载动画省略号(N 个点逐个渐亮)
导出形态默认导出
运行时依赖prop-types(宿主已有)
⚠️ 必须的额外步骤入口最前面引入 Text.propTypes polyfill(否则 import 即崩)
⚠️ scripts.preparenpm 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 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(已有)+ 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

Logo

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

更多推荐