本文记录把 react-native-shimmer-placeholder(骨架屏 / 闪光占位符)适配到 HarmonyOS 的完整过程。

这个库有一个特别容易静默踩坑的设计:它的 peerDependencies 里写着 react-native-linear-gradient(一个原生库),但代码里根本没有 import 它 —— 它用的是依赖注入 + View 兜底:

export const createShimmerPlaceholder = (LinearGradient = View) => …   // 不传就降级成 View

看起来"不装也能跑"(确实不报错、也确实在渲染)。但跑起来屏幕上是完全静止的 —— 因为那条负责"扫过"的闪光带子是:

<LinearGradient colors={shimmerColors} style={{flex: 1, width: width * shimmerWidthPercent}} … />

降级成 View 之后它没有任何背景色,也就是透明的;外层容器只有一块静态灰(shimmerColors[0] = #ebebeb)。⇒ 动画在跑(translateX 一直在变),但屏幕上什么都不会动。

这一条我用三点像素对照做成了硬证据:

实例配置两帧差异像素最大色差
P1默认(View 兜底)0 / 22000(0.00%)0
P2★ 换成有颜色的渐变替身8857 / 22000(40.26%)490
P4有颜色的替身 + stopAutoRun0 / 22000(0.00%)0

⇒ P1 完全静止、P2 明显在动、P4 关掉动画又回到静止 ⇒ 既证明了"默认看不到动",也证明了"动画本身一直在跑,只是带子透明"。

设备侧断言 21 / 21 全部通过。

在这里插入图片描述


一、先说结论

项结果
上游最新版2.0.9;license: MIT;上游 star 1088
库类型纯 JS 单组件库(骨架屏):ShimmerPlaceholder(class)+ createShimmerPlaceholder(HOC)
是否需要原生适配本库自己不需要(无 harmony/、无原生模块引用)
peerDependencies⚠️ react-native-linear-gradient >= 2.4.0(原生库)+ prop-types
它真的会 import 那个原生库吗❌ 不会 —— 靠 createShimmerPlaceholder(LinearGradient) 注入,不传就用 View 兜底
★ 不传的后果⚠️ 不报错、能渲染,但屏幕上看不到任何动效(闪光带子透明)见下文实测
运行时依赖零(dependencies 未声明)
prepare / prepublish都没有(scripts 只有 test)
main./lib/ShimmerPlaceholder.js(源码)
需要 HAR / 权限 / ohpm本库不需要;但想要真实渐变效果就得另接一个原生渐变库
设备侧断言✅ 21 / 21 全部通过
★ 像素判据✅ 三点对照:默认 0%、有颜色替身 40.26%、stopAutoRun 0%
★ 另一个坑width/height 的默认值不在 defaultProps 里,而在函数的解构默认里(实测不传尺寸 = 200x15 ✓)

二、判定与接入

步做法结果
①package.json 里有没有 harmony.autolinking没有
②仓库里有没有 harmony/没有
③代码里有没有 NativeModules / requireNativeComponent全无
④有没有 import react-native-linear-gradient没有(只在 peerDependencies 与文档里出现)
npm install "git+https://atomgit.com/oh-react-native/react-native-shimmer-placeholder.git#2.0.9-ohos-1.0.0" --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-shimmer-placeholder') ],
import ShimmerTestApp from './ShimmerTestApp';
AppRegistry.registerComponent('ShimmerTestApp', () => ShimmerTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey ShimmerTestApp

本库不需要 HAR / 权限 / ohpm / link-harmony,也不需要补装任何 dependencies。


三、读源码取到的真值(lib/ShimmerPlaceholder.js,共 162 行)

3.1 三个导出实体

class ShimmerPlaceholder extends PureComponent { … }          // 持有 Animated.Value,管 start/stop
const BasedShimmerPlaceholder = (props) => { … }              // 真正渲染(未导出)
export const createShimmerPlaceholder = (LinearGradient = View) =>
  React.forwardRef((props, ref) => (
    <ShimmerPlaceholder LinearGradient={LinearGradient} ref={ref} {...props} />
  ));
export default ShimmerPlaceholder;
  • ShimmerPlaceholder 是 class(PureComponent);
  • createShimmerPlaceholder() 返回的是 HOC 对象(forwardRef 的产物 ⇒ typeof 'object'、没有可用的 prototype);
  • BasedShimmerPlaceholder 是函数组件,但没导出。

3.2 defaultProps 只有 3 个键

ShimmerPlaceholder.defaultProps = {
  delay: 0,
  duration: 1000,
  isInteraction: true,
};

★ 注意:width / height / shimmerColors 这些常用默认值【不在】defaultProps 里 —— 它们在 BasedShimmerPlaceholder 的参数解构默认值里:

const {
  width = 200,
  height = 15,
  shimmerColors = ["#ebebeb", "#c5c5c5", "#ebebeb"],
  location = [0.3, 0.5, 0.7],
  isReversed = false,
  stopAutoRun = false,
  LinearGradient = View,          // ← 兜底就是这里
  shimmerWidthPercent = 1,
  …
} = props;

⇒ 所以"<ShimmerPlaceholder /> 什么都不传"渲染出来是 200 × 15(实测 200x15 ✓),但它不是 defaultProps 提供的。

3.3 动画与渲染结构

// 动画:无限循环 + 原生驱动(非 web)
Animated.loop(Animated.timing(beginShimmerPosition, {
  toValue: 1, delay, duration,
  useNativeDriver: Platform.OS !== "web",   // ⇒ 鸿蒙上走原生驱动,JS 侧读不到值
  isInteraction,
}))

// 位移:从 -width 扫到 +width(isReversed 则反向)
const linearTranslate = beginShimmerPosition.interpolate({
  inputRange: [-1, 1],
  outputRange: getOutputRange(width, isReversed),
});

// 结构(visible 为假时):
<View style={{flex:1, backgroundColor: shimmerColors[0]}}>        // 静态底色
  <Animated.View style={{flex:1, transform:[{translateX: linearTranslate}]}}>
    <LinearGradient colors={shimmerColors} style={{flex:1, width: width*shimmerWidthPercent}} … />
    {/* ↑ 降级成 View 时【没有 backgroundColor】⇒ 透明 ⇒ 什么都看不到 */}
  </Animated.View>
</View>

visible 的语义:为真时不渲染闪光带子、children 用 contentStyle 正常显示;为假时 children 被压成 0×0 + opacity 0。


四、验证设计:三点像素对照

因为 useNativeDriver 在鸿蒙上为真(JS 侧读不到动画值),只能用像素证。设计三个实例:

实例配置预期
P1<ShimmerPlaceholder width={300} height={40} />(默认 View 兜底)看不到动
P2★ 正对照:自造一个有颜色的"假渐变组件"(红色 View),用 createShimmerPlaceholder(FakeRedGradient) 注入看得到动
P4★ 反向对照:同用红色假渐变,但加 stopAutoRun看不到动

P2 的设计用意:不需要任何原生库,只用一个纯 JS 的红色 View 冒充渐变,就能把"那条带子"变得可见 ⇒ 从而证明 P1 看不到动,不是因为动画没跑,而是因为带子是透明的。

协议:对每个区域连拍两帧(间隔约 900ms),逐像素比较(判据:三通道差的绝对值之和 > 24 即记为"这个像素变了")。


五、实测结果

5.1 断言 21 / 21

组内容结果
A 契约导出/实体形态(class vs HOC 对象)、defaultProps 3 键、宿主里没有 react-native-linear-gradient11/11
B 渲染与几何渲染未抛错、标题不与状态栏重合、5 个实例的实测尺寸(含不传尺寸 = 200x15)9/9
C 行为与像素结论三个对照实例就位、原生驱动说明1/1
合计21/21

5.2 ★★ 三点像素对照(核心证据)

区域配置采样点差异像素占比最大色差
P1默认(View 兜底)2200000.00%0
P2红色假渐变(动画开)22000885740.26%490
P4红色假渐变 + stopAutoRun2200000.00%0

颜色分布也印证了结论:

P1 区(默认)最常见颜色:235,235,235 ×111   ← 就是 shimmerColors[0] 的静态灰,且两帧完全相同
P2 区(对照)最常见颜色:254,0,0   ×67      ← 红色带子确实存在

在这里插入图片描述

两帧拼图(同一区域各取两帧):

frame1frame2
P1 默认全灰全灰(与 frame1 完全相同)
P2 正对照红带在右侧红带在左侧 ← 明显移动了

在这里插入图片描述

5.3 ★ 另一个坑:width/height 默认值不在 defaultProps 里

用一个什么都不传的实例(P6)实测:200x15 ✓ —— 与源码里 BasedShimmerPlaceholder 的解构默认 width = 200, height = 15 一致。
而 ShimmerPlaceholder.defaultProps 里只有 delay / duration / isInteraction 三个键(实测通过)。

⇒ 想知道"不传会怎样",必须同时看 defaultProps 和函数参数的解构默认 —— 这个库两者都有,且分工不同。

5.4 UI:标题不与状态栏重合

TITLE measureInWindow y=420   (状态栏高 117)   ⇒ 420 > 117,不重合 ✓

在这里插入图片描述


六、我自己的三个失误(都值得记)

#失误后果纠正
1把 onLayout 挂在外层 stage 上,想量 shimmer 的尺寸量到的是 stage(全宽)⇒ "P1 尺寸 200x15"假失败用它自己的 containerProps={{onLayout}} 传进去(源码把 containerProps 展开到最外层 View 上)⇒ 实测 300x40 ✓
2断言与自己传的 props 矛盾:我给 P1 显式传了 width={300} height={40},却断言默认值 200x15又一条假失败拆成两个:P1 断言 300x40(我传的);另加一个不传尺寸的 P6 断言 200x15(真默认)
3用"按行替换"改代码,删掉了一个 }文件语法坏掉 ⇒ npm run dev 失败(exit 1)用行号重写修回;并且发现了下面这个更严重的问题 ⬇️

★★ 失误 3 牵出的真问题:打包失败时,HAP 会静默装成旧 bundle

那一次的现象是:

npm run dev   →  bundle exit=1        (Babel 解析报错,bundle 没生成)
hvigorw … assembleHap → BUILD SUCCESSFUL in 3 min 50 s     ← 仍然"成功"!
hdc install   →  装上的是【上一次】的 bundle

⇒ 于是页面上跑的还是旧断言、结果看起来"没变化",我一度以为修复没生效。
这是个很危险的现象:assembleHap 成功 ≠ 你刚写的代码进了包。

判据:把"打包"和"构建"的退出码分开看。只要 npm run dev 的 exit code != 0,后面 assembleHap 报成功也毫无意义 —— 它只是把旧的 bundle 又打了一遍。


七、已知限制与本次验证边界

7.1 用这个库要知道的

事项说明
不传渐变组件 ⇒ 看不到动效闪光带子降级成透明 View,只剩静态底色;不报错、不警告
想要真实渐变要自己接接 react-native-linear-gradient(原生库)并用 createShimmerPlaceholder(LinearGradient) 注入
width/height 默认值不在 defaultProps在函数参数的解构默认里(200 / 15)
createShimmerPlaceholder() 返回对象forwardRef 产物 ⇒ typeof 'object'、没有可用 prototype
useNativeDriver 在鸿蒙为真JS 侧读不到动画值 ⇒ 验证只能靠像素
visible 切换会重建渲染结构为真时不渲染闪光带子、children 正常显示

7.2 本次验证的边界(按用户选择:不接原生渐变库)

  1. 没有接 react-native-linear-gradient(本次范围外)⇒ 真实渐变效果未验证;本轮只证明了"换成有颜色的渐变替身就会动"(用纯 JS 红色 View 冒充)。
    鸿蒙侧确实有适配版可用:@react-native-ohos/react-native-linear-gradient@3.2.0(带 harmony 字段)—— 但接它要另接一个原生 HAR,本轮未做。
  2. 只在一台模拟器上验证(Pura X View,density 3,1320×2232),没有真机。
  3. delay / duration / isInteraction 未做行为验证(只用断言核对了 defaultProps 的取值)。
  4. shimmerColors / location / shimmerWidthPercent / shimmerStyle / contentStyle / containerProps(除 onLayout 外)未测。
  5. visible 的像素行为未做两帧验证(只确认了 children 会显示、带子不渲染)。
  6. isReversed 只渲染了实例,未做帧差对照(预期扫动方向相反)。
  7. children 在 visible=false 时被压成 0×0 的行为未做像素核对。
  8. 没有跨平台对照(未在 iOS/Android 上跑同一套用例)。

未改动库代码。


八、常见问题

Q1:peerDependencies 里有 react-native-linear-gradient,我不装会怎样?

不会报错,能跑,但屏幕上看不到任何动效 —— 因为移动的那条带子降级成了 View(没有背景色 ⇒ 透明),只剩一块静态灰。

实测:默认实例两帧像素差 0.00%;把它换成"有颜色的渐变替身"后两帧差 40.26%;再加 stopAutoRun 又回到 0.00%。

解法(二选一):

  • 接一个渐变库并注入:const S = createShimmerPlaceholder(LinearGradient);鸿蒙侧可用 @react-native-ohos/react-native-linear-gradient;
  • 或者自己写一个纯 JS 的"假渐变"(有背景色的组件即可,比如 <View style={{flex:1, backgroundColor:'#c5c5c5'}} />)—— 不算真渐变,但至少能看到扫动。

Q2:<ShimmerPlaceholder /> 什么都不传,尺寸是多少?

200 × 15(实测)。但注意它不是 defaultProps 提供的 —— 它在 BasedShimmerPlaceholder 的参数解构默认值里:

const { width = 200, height = 15, … } = props;

而 ShimmerPlaceholder.defaultProps 里只有 delay: 0 / duration: 1000 / isInteraction: true。

Q3:createShimmerPlaceholder() 返回的是什么?为什么我拿不到 prototype?

它返回的是 React.forwardRef(...) 的产物(一个对象,typeof 'object'),不是 class ⇒ 没有可用的 prototype。要拿实例引用用 ref(它把 ref 透传给了内部的 ShimmerPlaceholder)。

Q4:为什么我在 JS 里读不到动画进度?

源码里 useNativeDriver: Platform.OS !== "web" ⇒ 鸿蒙上走原生驱动,动画值不在 JS 侧更新。⇒ 验证动效只能靠像素(连拍两帧比差),或者只做结构/属性层面的断言。

Q5:stopAutoRun 是干什么的?

为 true 时不自动启动循环动画(useEffect 里 if (!stopAutoRun) animatedValue.start())。本轮用它做了反向对照:同样有颜色的带子,加上它之后两帧差回到 0.00% ⇒ 证明帧差确实由动画驱动。

Q6:visible 传 true 是什么效果?

不渲染闪光带子,children 正常显示(contentStyle);传 false 时 children 被压成 0×0 + opacity: 0,只显示骨架。典型用法是"数据没回来时 visible={false},回来了 visible={true}"。

Q7:这个库需要鸿蒙侧做什么特殊处理吗?

本库不需要(纯 JS、无原生模块、无 HAR/权限/ohpm)。唯一要注意的是:它不会自动帮你接渐变库 —— 不接就只能看到静止的灰块(而且不报错)。

Q8:为什么我改了代码、重新构建,页面上还是旧行为?

⚠️ 检查打包步骤的退出码。npm run dev 失败(比如语法错)时 hvigorw assembleHap 仍可能报 BUILD SUCCESSFUL —— 因为它只是把已存在的(旧的) bundle 打进包里。只有 npm run dev 的 exit code 为 0,构建结果才有意义。


小结

react-native-shimmer-placeholder 是纯 JS 单组件库(class + 一个 forwardRef HOC),本库零依赖、不需要任何原生接线。设备侧 21 / 21 断言通过。

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

  1. ★★ peerDependencies 里有原生库,但代码不 import 它 —— 这是"静默失效"的典型温床。
    它靠 createShimmerPlaceholder(LinearGradient = View) 依赖注入,不传就降级成透明的 View ⇒ 不报错、能渲染、但屏幕完全静止。
    实测三点对照:默认 0.00% 帧差、换成有颜色的替身 40.26%、加 stopAutoRun 又回到 0.00%。
    ⇒ 遇到"可选原生依赖",一定要在没有它的状态下实测效果,并把"没有它会发生什么"写成结论。 而且要自己做正/反两个对照,否则"没动"既可能是"没接依赖",也可能是"动画本来就没跑"。

  2. 默认值可能在两个地方,缺一不可查。 这个库的 defaultProps 只有 3 个键,而 width/height/shimmerColors/... 全在函数参数的解构默认值里。⇒ 只读 defaultProps 会漏掉一大半默认行为(实测"什么都不传" = 200x15)。

  3. ★★ assembleHap 成功 ≠ 你刚改的代码进了包。 我有一次 npm run dev 因语法错失败(exit 1),而 hvigorw 依然报 BUILD SUCCESSFUL、照样装上 —— 装进去的是上一次的 bundle,差点据此得出"修复没生效"的错误结论。
    ⇒ 打包步骤的退出码必须单独看;只要 npm run dev 非 0,后面的构建成功没有任何意义。

(另外两条我自己的低级错误也一并记着:onLayout 要挂在组件自己的 containerProps 上而不是外层包装;断言不能和自己传的 props 矛盾。)


本篇用到的库

项内容
三方库react-native-shimmer-placeholder(上游 2.0.9 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-shimmer-placeholder
适配 TAG2.0.9-ohos-1.0.0
需要 HAR / 权限 / ohpm本库不需要
库类型纯 JS 单组件库:骨架屏 / 闪光占位符
运行时依赖零;但 peerDependencies 里有 react-native-linear-gradient(原生库)+ prop-types,且代码不 import 前者
prepare / prepublish都没有
许可证MIT
上游仓库https://github.com/tomzaku/react-native-shimmer-placeholder(★1088)
宿主工程RNOH084Demo(测试页 rnAppKey = ShimmerTestApp)
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-shimmer-placeholder.git#2.0.9-ohos-1.0.0" --ignore-scripts

# 本地目录接入(开发常用)
npm install ../react-native-shimmer-placeholder --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-shimmer-placeholder') ],
import ShimmerPlaceholder, {createShimmerPlaceholder} from 'react-native-shimmer-placeholder';

// ① 直接用默认导出 —— ⚠️ 不传渐变组件 ⇒ 只有静态灰块,看不到扫动
<ShimmerPlaceholder width={300} height={40} />

// ② 想要真实渐变:接一个渐变库并注入
import LinearGradient from 'react-native-linear-gradient';   // 鸿蒙侧可用 @react-native-ohos/react-native-linear-gradient
const Shimmer = createShimmerPlaceholder(LinearGradient);
<Shimmer width={300} height={40} />

// ③ 或者用纯 JS 的"假渐变"(有背景色即可)—— 不算真渐变,但至少能看到扫动
const FakeGradient = props => <View {...props} style={[props.style, {backgroundColor: '#c5c5c5'}]} />;
const Shimmer2 = createShimmerPlaceholder(FakeGradient);
<Shimmer2 width={300} height={40} />

// ④ 数据回来后显示 children
<ShimmerPlaceholder width={300} height={40} visible={isFetched}>
  <Text>真实内容</Text>
</ShimmerPlaceholder>

// ⑤ 想量它的尺寸:用 containerProps 传 onLayout(不要挂在外层包装上)
<ShimmerPlaceholder
  width={300} height={40}
  containerProps={{onLayout: e => console.log(e.nativeEvent.layout)}}
/>
# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey ShimmerTestApp

像素协议复现(本节判据的复现方式)

# 对同一区域连拍两帧(间隔约 900ms),逐像素比较(三通道差之和 > 24 记为"变了")
hdc shell "snapshot_display -f /data/local/tmp/f1.jpeg"; hdc file recv /data/local/tmp/f1.jpeg .\f1.jpeg
Start-Sleep -Milliseconds 900
hdc shell "snapshot_display -f /data/local/tmp/f2.jpeg"; hdc file recv /data/local/tmp/f2.jpeg .\f2.jpeg

Add-Type -AssemblyName System.Drawing
$a = New-Object System.Drawing.Bitmap('f1.jpeg'); $b = New-Object System.Drawing.Bitmap('f2.jpeg')
$diff = 0; $total = 0
for ($y = $y0; $y -lt $y1; $y += 3) { for ($x = 0; $x -lt $a.Width; $x += 3) {
  $p1 = $a.GetPixel($x,$y); $p2 = $b.GetPixel($x,$y)
  if (([Math]::Abs($p1.R-$p2.R) + [Math]::Abs($p1.G-$p2.G) + [Math]::Abs($p1.B-$p2.B)) -gt 24) { $diff++ }
  $total++ } }
"差异像素 = $diff / $total"
# 默认(View 兜底)⇒ 0;有颜色的渐变替身 ⇒ 明显 > 0;再加 stopAutoRun ⇒ 回到 0

验证环境

项版本
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(页面留白取 48vp)
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap 3 分 40 秒 / 3 分 50 秒 / 3 分 52 秒(多轮)
验证规模设备侧 21 / 21 断言;三点像素对照(默认 0% / 有颜色替身 40.26% / stopAutoRun 0%);实例尺寸 6 个(含不传尺寸 200x15);UI 安全区 1 项(标题窗口 y=420 > 117)

相关社区与项目地址

项目地址
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

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐