React Native for OpenHarmony 实战:三方库 react-native-ticker 的鸿蒙化适配指南
本文记录把 react-native-ticker(数字滚动/翻牌效果)适配到 HarmonyOS 的完整过程。
它做的事很专一:把一段文字(通常是数字)拆成单个字符,每个字符位做成一个"竖向滚轮" —— 用 translateY 把目标字符滚到视口里,于是数字变化时看起来像"数字在滚动"。
结论:数字场景(纯数字/含小数点)在鸿蒙上工作正常;但它在"字符集发生变化"时会崩 —— 这是一个可以用源码解释、也用对照实验证实了的真实缺陷。
★★★★★ 一、先说机制:它是怎么"量字"又怎么"滚"的
去掉编译产物后,核心逻辑非常清楚(index.js 137 行):
// ① 把 children 拆成单字符
const measureStrings = Children.map(children, child => splitText(`${child}`)).reduce(...)
// ② 是否含数字 ⇒ 决定"轮子"的内容
const hasNumbers = measureStrings.find(v => isNumber(v)) !== undefined;
const numberRange = range(10).map(p => p + '');
const numAdditional = [',', '.'];
const numberItems = [...numberRange, ...numAdditional]; // ['0'..'9', ',', '.']
const rotateItems = uniq([...(hasNumbers ? numberItems : []), ...measureStrings]);
// ③ 为 rotateItems 里每个元素渲染一个【隐藏 Text】(absolute + opacity:0),靠 onLayout 量宽高
rotateItems.map(v => <Text style={[textStyle, styles.hide]} onLayout={e => handleMeasure(e, v)}>{v}</Text>)
// ④ 全部量完 ⇒ measured = true
if (Object.keys(measureMap.current).length === rotateItems.length) setMeasured(true);
// ⑤ 只有 measured 才渲染 TickItem
{measured === true && Children.map(children, ...)}
// ⑥ TickItem:把 rotateItems 全渲染成一列竖排 Text,用 translateY 选一个露出来
const position = getPosition({text: children, height: measurement.height, items: rotateItems});
const stylePos = useSharedValue(position);
const animatedStyles = useAnimatedStyle(() => ({
height: measurement.height,
width: widthAnim.value,
overflow: 'hidden',
transform: [{translateY: stylePos.value}],
}));
⇒ ⇒ 这是一个"先测量、再滚动"的两段式状态机 ✓
⇒ measured 这个状态,就是本文最重要的伏笔。
★★★★★ 二、核心发现:字符集一变就崩(Cannot read property 'height' of undefined)
我的测试页用时间轴把值依次改成 123 → 456 → 12.5 → abc,前三个阶段都正常,第四阶段整页崩:
LogBox: "Cannot read property 'height' of undefined"
(此后页面上的单字符 Text 全部消失)
根因(读源码就能推出来):
const measurement = measureMap[children]; // ★ children 变了,但 measureMap 里没有它
const position = getPosition({
text: children,
height: measurement.height, // ★ 这里崩
items: rotateItems,
});
把过程走一遍:
| 阶段 | 值 | hasNumbers | rotateItems | measureMap 里有什么 |
|---|---|---|---|---|
| 1 | 123 | true | ['0'..'9', ',', '.'](12 项) | 量完这 12 项 ⇒ measured = true |
| 2 | 456 | true | 同上(12 项) | 还是那 12 项 |
| 3 | 12.5 | true | 同上(12 项) | 还是那 12 项 |
| 4 | abc | false | ['a','b','c'](3 项) | 仍只有那 12 项 —— 没有 a! |
⇒ measured 一旦变成 true,就再也不会回到 false(源码里没有任何复位逻辑)
⇒ 于是第四阶段立刻去渲染 TickItem,而 measureMap['a'] 是 undefined
⇒ measurement.height 崩 ✓✓
★★★★★ 用对照实验证实:纯数字路径不崩
为了排除"是别的原因导致崩",我把时间轴改成只走纯数字(123 → 456 → 12.5,不切字符集)再跑一遍:
@5s(值 123): 单字符 Text 数 = 0 LogBox: 无 ✓
@13s(值 456): 单字符 Text 数 = 18 LogBox: 无 ✓
@21s(值 12.5): LogBox: 无 ✓
⇒ ✓✓ 纯数字路径三个阶段都没有 LogBox ⇒ 崩溃确实由【字符集变化】触发
⇒ ⇒ 结论可靠:崩因是"字符集变化导致新字符没有量测结果",不是随机问题 ✓✓
顺带得到的状态机时序证据
上面第一行 @5s 单字符 Text 数 = 0 很有价值 —— 它说明:
- 页面挂载后,
measured仍然是false(还在等 12 个onLayout回调) - 到
@13s才变成18个单字符 Text ⇒measured已经true、TickItem开始渲染
⇒ onLayout 在 RNOH 上确实是异步但可达的 ✓(这个库正是靠它工作的)
对使用者的直接含义
| 场景 | 是否可用 |
|---|---|
纯数字(123 → 456、金额、计数器) | ✅ 正常 |
数字 + 小数点/逗号(12.5、1,234) | ✅ 正常(, 与 . 本来就在 numberItems 里) |
数字 ⇄ 字母 混用(123 → abc) | ❌ 崩(本文发现的缺陷) |
纯字母之间变化(abc → abd) | ⚠️ 字符集不变(a,b,c → a,b,d 里 d 是新的)⇒ 同样会崩 |
规避办法:让 ticker 的字符集保持稳定(例如始终用等宽数字、需要切字母时给 Ticker 换一个 key 强制重挂载):
<Ticker key={String(value)} …>{value}</Ticker> {/* 换 key ⇒ 重挂载 ⇒ 重新量测 */}
★★★★ 三、react-native-reanimated 的处理:功能桩(附适配版信息)
它声明的 peer 是 react-native-reanimated: >= 2.4.1(原生库,worklets + C++)。
但它的用法面非常小 —— 只有 5 个 API:
import Animated, {useSharedValue, useAnimatedStyle, withTiming, Easing} from 'react-native-reanimated';
鸿蒙适配版是存在的:
| 包 | 版本 |
|---|---|
@react-native-ohos/react-native-reanimated | 4.0.2 |
@react-native-oh-tpl/react-native-reanimated | 3.6.4-rc.1 |
但我这一轮选择打桩,理由:
- reanimated 需要 babel plugin + worklets,接入不只是加 HAR;
- HAR 接入此前实测失败过一次;
- 更关键:本库的核心逻辑(拆字 →
onLayout量宽高 →measured状态机 → 渲染TickItem)全是纯 JS,
把 reanimated 换成"瞬间到位"的功能桩后,渲染结构与几何仍然真实,只有"滚动过渡"变成瞬移。
桩做成了"功能可用"而不是空壳:
const useSharedValue = (initial) => /* 返回稳定的 {value} 对象 */;
const useAnimatedStyle = (updater) => updater() || {}; // 直接执行 worklet
const withTiming = (toValue) => toValue; // 直接到位
const Easing = { linear: t => t, ease: t => t, /* … */ };
const Animated = { View, Text, ScrollView, /* … */ }; // 直接映射 RN 组件
⚠️ 代价必须写清楚:滚动动画的"过程"完全没看到(只有终值正确)。
"接了真 reanimated 之后动画是否顺畅"本轮未验证。
★★★★★ 四、桩统计给出了一条漂亮的"行为计数"证据链
桩会统计三个计数,我在每个相位打一次 ⇒ 数字与组件的内部结构精确对应:
挂载初期 {sharedValueCount: 0, animatedStyleCount: 0, withTimingCount: 0}
P0(值 123) {sharedValueCount: 12, animatedStyleCount: 6, withTimingCount: 12}
P1(值 456) {sharedValueCount: 12, animatedStyleCount: 12, withTimingCount: 18}
P2(值 12.5) {sharedValueCount: 12, animatedStyleCount: 18, withTimingCount: 24}
P3(值 abc) {sharedValueCount: 14, animatedStyleCount: 25, withTimingCount: 32}
怎么读这些数字:
sharedValueCount = 12⇒ 页面上一共有 6 个TickItem(E1 的 3 个字符位 + E2 固定的abc3 个字符位),
每个TickItem里有两处useSharedValue(widthAnim与stylePos)⇒ 6 × 2 = 12 ✓✓withTimingCount每次改值 +6 ⇒ 3 个字符位 × 2 个withTiming(位置 + 宽度)= 6 ✓✓animatedStyleCount每次 +6 ⇒ 3 个字符位 × 1 个useAnimatedStyle= 6 ✓✓
⇒ ⇒ 这些计数不是"跑起来了"的模糊证据,而是能和源码结构一一对上的整数 ✓✓
⇒ 这条经验值得复用:给桩加"按 API 分类的计数",就能反推组件内部渲染了多少个实例、每实例调了几次 hook ✓
★★★★ 五、文本级判据:单字符 Text 的数量与内容
TickItem 会把 rotateItems 全部渲染成一列竖排 Text,靠 translateY 只让目标字符露出来。
所以我直接数"布局 dump 里内容是单个字符的 Text":
单字符 Text 的种类数 = 15
内容 = ,×1 .×1 0×1 1×1 2×1 3×1 4×1 5×1 6×1 7×1 8×1 9×1 a×2 b×2 c×2
总数 = 18
与 rotateItems 的构造完全吻合:
,.0–9各 1 个(共 12 个)⇒ 来自 E1(数字路径,numberItems12 项)✓abc各 2 个(共 6 个)⇒ 来自 E2 固定渲染的abc(rotateItems = ['a','b','c'])✓- 12 + 6 = 18 ✓✓
⚠️ 为什么每种只有 1–2 个而不是"全部 12 个都可见":
TickItem用的是overflow: 'hidden'+translateY,被移出视口的 Text 不会被布局 dump 列出。
这又一次印证了一条反复出现的盲区:“用视觉手段隐藏/位移的节点,dump 看不见” ✓




★★★ 六、其余预检结论
| 预检项 | 结果 |
|---|---|
| 老写法九项 | ✅✅ 全部为空 |
propTypes | ✅✅ 完全没有 ⇒ 不存在"必填 prop 无默认值"的陷阱(这一项被特意放在预检最前面) |
defaultProps | 只有 {duration: 250} |
Platform.OS | ✅ 空(只有 I18nManager.isRTL 决定 flexDirection,实测 isRTL = false) |
measure() | ✅ 空(量尺寸用 onLayout,2 处) |
useNativeDriver | ✅ 空(reanimated 不用这个参数) |
NativeModules | ✅ 空 |
dependencies | ✅ 空(零运行时依赖) |
prepublishOnly | ✅ npm install 不跑 |
契约(反射实测,全 PASS):
NOTE | 默认导出的 typeof = function
PASS | ★ 默认导出 Ticker 是函数组件
PASS | ★ 具名导出 Tick 是函数组件
NOTE | Ticker.defaultProps 的键 = duration
PASS | ★ defaultProps.duration = 250
NOTE | Ticker.propTypes 的键 = (无 propTypes)
PASS | ★★ 库没有 propTypes ⇒ 不存在"必填 prop 无默认值"陷阱
PASS | ★★ reanimated 可解析(功能桩)
七、接入与预检(要点)
# ① 装库(file: 接入;它没有 dependencies,所以不用补装)
npm install "git+https://atomgit.com/oh-react-native/react-native-ticker.git#6.0.1-ohos-1.0.0" --ignore-scripts
# ② 它的 peer 是 react-native-reanimated —— 二选一:
# (A) 接真库:@react-native-ohos/react-native-reanimated@4.0.2 + babel plugin + HAR
# (B) 打桩:metro 的 resolver.extraNodeModules 指向功能桩(本文采用)
八、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | index.js(5406 B,编译产物)+ index.tsx(5431 B,源码) ⇒ 都完整可读 ✓(12 文件) |
spec.json | ⚠️ 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4" ⇒ 两字段自相矛盾 |
| 备注 | 实现零改动(本轮未修改库任何文件) |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。库无已移除写法 ⇒ 结论跨基线成立 ✓
九、已知限制与本次验证边界
9.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| ★★★★★ 字符集变化时会崩 | measureMap 只累积不清理、measured 不复位 ⇒ 从数字切字母(或字母集变化)⇒ Cannot read property 'height' of undefined |
★★★★ 规避:保持字符集稳定,或换 key 强制重挂载 | <Ticker key={String(value)}> |
| ★★★★ reanimated 只用 5 个 API | useSharedValue / useAnimatedStyle / withTiming / Easing / Animated.View ⇒ 功能桩可行 |
★★★ 靠 onLayout 量每个字符的宽高 | 量完全部 rotateItems 才 measured = true ⇒ 首帧没有内容(实测 @5s 还是 0 个单字符 Text) |
★★★ TickItem 会把 rotateItems 全渲染 | 靠 overflow:hidden + translateY 只露一个 ⇒ dump 里只看得到 1–2 个 |
| ★★ 无 propTypes | 没有"必填 prop"陷阱,但也没有参数校验 |
| ★ 零运行时依赖 | dependencies 为空 |
9.2 本次验证的边界
- ★★★ 滚动动画的"过程"完全没看到(reanimated 用功能桩,
withTiming直接到位); - ★★★ 没有接真的 reanimated(适配版
@react-native-ohos/react-native-reanimated@4.0.2存在但未接入)⇒ "接了真库后动画是否顺畅"未知; - ★★
Tick具名导出只做了"可导入/是函数"的断言,没做实际渲染(它需要外部提供measureMap与rotateItems); - ★★ 未验证
duration取不同值的效果(桩里withTiming直接返回目标值,时长无意义); - 未验证
containerStyle/textStyle/textProps的更多组合; - 未验证 多字符位数(如 6 位金额)时的宽度补偿行为;
- 未验证
I18nManager.isRTL = true时的表现; - 未在
RN 0.88.0/RNOH 0.84.4上复验; - 未测其它屏幕宽度;未做跨平台对照。
未改动库代码。
十、常见问题
Q1:它在 RN 0.84 上能跑吗?
数字场景能跑。 值 123 → 456 → 12.5 三个阶段都正常(LogBox 无通知),
单字符 Text 的数量与内容与 rotateItems 的构造精确吻合。
但值里出现字母时会崩(见第二节)。
Q2:报 Cannot read property 'height' of undefined?
你让 ticker 的字符集变了(例如从 123 变成 abc)。
内部 measureMap 只累积不清理、measured 不复位 ⇒ 新字符没被量过 ⇒ measurement.height 崩。
规避:字符集保持稳定,或 <Ticker key={String(value)}>…</Ticker> 强制重挂载。
Q3:为什么首帧看不到内容?
因为它要先用隐藏的 Text + onLayout 把 rotateItems 里每个字符的宽高都量一遍,
全部量完才 setMeasured(true) 才渲染真正的滚动列。
实测挂载 5 秒时单字符 Text 还是 0 个,13 秒时是 18 个 —— 说明这个"量字"过程是异步的。
Q4:一定要装 react-native-reanimated 吗?
理论上要(它是 peer)。但你也可以像我这样打一个功能桩:
withTiming 直接返回目标值、useAnimatedStyle 直接执行——结构能跑、只是没有滚动过程。
如果要真动画,鸿蒙适配版是 @react-native-ohos/react-native-reanimated@4.0.2。
Q5:能不能只显示"数字滚动",不用管字母?
可以,这也是它的设计场景。纯数字 / 带小数点 / 带千分位逗号都正常(, 和 . 本来就在 numberItems 里)。
Q6:为什么布局 dump 里看不到那 12 个竖排字符?
TickItem 用 overflow: 'hidden' + translateY 只让目标字符露出来,被移出视口的节点 dump 不列。
实测每种字符只看到 1–2 个(12 + 6 = 18 个单字符 Text)✓
小结
react-native-ticker 是**"先量字、再滚动"的两段式数字翻牌组件**,
九项老写法全空、无 propTypes、无平台分支、无 measure()、零运行时依赖,
数字场景在鸿蒙上工作正常。
最值得记下来的是这几条:
-
★★★★★ 一个可用源码解释、并用对照实验证实了的缺陷:字符集一变就崩。
measureMap只累积不清理、measured一旦true就不复位 ⇒ 从123切到abc时
measureMap['a']是undefined⇒Cannot read property 'height' of undefined。
对照实验:改成只走纯数字(123→456→12.5)⇒ 三个阶段都没有 LogBox ✓✓
⇒ 规避:保持字符集稳定,或换key强制重挂载。 -
★★★★★ 桩统计可以做"行为计数"证据链,而且能和源码结构一一对上。
sharedValueCount = 12⇒ 6 个TickItem× 2 个useSharedValue;
每次改值withTiming+6 ⇒ 3 个字符位 × 2 个withTiming;
animatedStyleCount每次 +6 ⇒ 3 个字符位 × 1 个useAnimatedStyle✓✓
⇒ 给桩加"按 API 分类的计数",就能反推"渲染了多少实例、每实例调了几次 hook" ✓ -
★★★★ 抓到了状态机的时序证据。
@5s单字符 Text 还是 0 个、@13s变成 18 个 ⇒ 说明measured是"等所有onLayout回来"才置位 ✓
⇒ 也证明onLayout在 RNOH 上异步但可达 ✓ -
★★★★ 把"必填 prop 无默认值"作为预检的第一个检查项。
先确认propTypes完全没有 ⇒ 结构上就不可能踩那个陷阱 ⇒ 本轮零崩溃(唯一的崩是本文发现的那个真实缺陷)
⇒ 把一个高危检查项固定在预检最前面,收益立刻可见。 -
★★★ reanimated 的"用法面小"决定了打桩可行性。
只用 5 个 API ⇒ 一个 60 行的功能桩就能让结构跑起来(代价:动画过程看不见)。
这比硬接 HAR 省下大量时间,但一定要把"动画未验证"写清楚。 -
★★★ 又一次印证"视觉手段做的隐藏/位移,dump 看不见"。
TickItem全渲染 12 个 Text,但overflow:hidden+translateY⇒ dump 里只看得到 1–2 个 ⇒
别把"没看到"当成"没渲染" ✓ -
★ 零运行时依赖的库最省事:
dependencies为空 ⇒ 不用补装任何东西(省掉了file:接入不装依赖的老坑)✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-ticker(上游 6.0.1 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-ticker |
| 适配 TAG | 6.0.1-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | ⚠️ 其 peer react-native-reanimated 需 HAR(本轮打桩未接) |
| 库类型 | 数字滚动 / 翻牌(拆字 + translateY) |
| 运行时依赖 | ✅ dependencies 为空;⚠️ peer 是 react-native-reanimated |
prepublishOnly | ✅ npm install 不跑 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/browniefed/react-native-ticker(★456) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = TickerTestApp) |
# 装库(无 dependencies,不用补装)
npm install "git+https://atomgit.com/oh-react-native/react-native-ticker.git#6.0.1-ohos-1.0.0" --ignore-scripts
# peer 二选一:接 @react-native-ohos/react-native-reanimated@4.0.2(+babel plugin+HAR),或打功能桩
import Ticker, {Tick} from 'react-native-ticker';
// ① 数字滚动(推荐用法:字符集保持稳定)
<Ticker duration={250} textStyle={{fontSize: 40, height: 56}}>{'123'}</Ticker>
// ② 千分位 / 小数点也支持(',' 与 '.' 本来就在 numberItems 里)
<Ticker>{'1,234.5'}</Ticker>
// ③ ★ 需要切换字符集时,换 key 强制重挂载(否则会崩)
<Ticker key={String(value)} textStyle={{fontSize: 40}}>{value}</Ticker>
// ⚠️ 字符集变化(数字→字母)会导致 Cannot read property 'height' of undefined
// ⚠️ 首帧无内容:要先用隐藏 Text + onLayout 把每个字符量一遍
// ⚠️ 需要 react-native-reanimated(可打功能桩,但那样没有滚动过程)
// ⚠️ TickItem 全渲染 12 个字符,靠 overflow:hidden 只露一个 ⇒ dump 只看得到 1–2 个
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey TickerTestApp
判据的复现方式
# ① 契约 + reanimated 桩统计:逐条 PASS/FAIL 日志
hdc shell "hilog -x | grep rnt-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
# ⇒ Ticker/Tick 是函数、defaultProps.duration=250、(无 propTypes)、桩统计演进
# ② 文本级判据:数"内容是单个字符"的 Text
devecocli ui layout | Select-String 'Text#Paragraph'
# ⇒ 数字路径下 , . 0..9 各 1 个 + a b c 各 2 个 = 18 个
# ③ 崩溃复现:让 ticker 的值从数字切到字母 ⇒ LogBox 出现
# "Cannot read property 'height' of undefined"
# ④ 对照:只走纯数字路径 ⇒ 三个阶段都无 LogBox
验证环境
| 项 | 版本 |
|---|---|
| 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 |
| 宿主依赖 | ⚠️ react-native-reanimated 用功能桩隔离 |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 42 秒 ⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 契约 7 条全 PASS;桩统计行为计数与源码结构精确对应(12 / +6 / +6);单字符 Text 计数与 rotateItems 吻合(18 个);状态机时序证据(@5s 0 个 → @13s 18 个);缺陷复现 + 对照实验(纯数字路径不崩);4 张截图互异,分别对应 4 个相位 |
⚠️ 基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
本库无任何已移除写法、无平台分支 ⇒ 结论跨基线成立。
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)