React Native鸿蒙开发实战:手写Shimmer闪光效果
2026/9/19 2:46:03 网站建设 项目流程

一年多前我把一个 React Native 项目往鸿蒙上迁移,绕了不少路;最近又给新版 App 加了 Shimmer 闪光效果,发现这个原本在 iOS/Android 上很成熟的动效,在鸿蒙跨平台体系里其实有不少细节值得单独聊聊。这篇内容会从环境准备、工程接入、组件实现到问题排查,完整过一遍用 React Native 开发鸿蒙页面并实现 Shimmer 闪光效果的基础流程。如果你团队正准备用 RN 做鸿蒙端、或者已经在做但想加一个好看又不坑的加载占位,这篇文章应该能帮你省下不少踩坑时间。

1. 项目背景与方案选型:为什么是 React Native + 自研 Shimmer

1.1 鸿蒙跨平台开发:React Native 的生态位置

鸿蒙应用开发现在有好几条路可以走:直接写 ArkUI,用 Flutter 跨平台,用 React Native 跨平台,甚至还有 Tauri 这类偏 Web 的方案。每条的取舍都不一样。直接写 ArkUI 性能最好、系统能力拿得最全,但业务代码基本只能留在鸿蒙生态里;Flutter 渲染引擎自成一派,在鸿蒙上也有适配,但如果你团队本来就以 React / 前端技术栈为主,那 React Native 的学习成本是最低的。

React Native 的鸿蒙适配思路,是把 RN 的组件树映射到鸿蒙的 ArkUI 组件上,JS 业务代码不变,底层渲染交给鸿蒙原生控件。这意味着你之前写的组件、状态管理、网络层、工具函数,大部分可以复用。对于已经有 RN 历史项目、又必须出一个鸿蒙版本的情况,这个方案非常值得优先评估。当然也要清醒认识到:鸿蒙的 RN 适配不是 React Native 官方主仓直接维护,版本更新会比社区慢一点,个别第三方原生模块可能没有对应实现。所以做技术选型时,优先挑那些纯 JS 实现的功能,Shimmer 闪光效果就是一个典型例子。

1.2 Shimmer 闪光效果不是花架子

Shimmer 闪光效果,又叫微光效果,最常出现在骨架屏加载占位里。页面内容还没准备好时,先画几个灰色圆角方块,一道高光从左到右扫过去,视觉上告诉用户“我正在加载,别急”。这个效果在 iOS 和 Android 上已经很常见,现在鸿蒙端也越来越需要。

除了好看,它更重要的价值是降低“等待焦虑”。尤其是 React Native 应用,从原生启动页进入 JS 页面时,经常有一段空白期,社区里管这个叫启动白屏。白屏一旦超过一两秒,用户很容易认为 App 卡死或没响应。如果用原生启动页先兜底,再让 RN 页面里挂一个 Shimmer 骨架屏,用户看到的就是“启动动画 -> 加载高光 -> 真实内容”的连续流程,白屏带来的流失风险会小很多。所以这个效果不是装饰,是实打实的体验优化。

1.3 社区库不是首选,自研反而更省心

我在最初调研时,第一个想法是找个现成库,比如react-native-shimmerreact-native-shimmer-placeholder。但在鸿蒙场景下,这些库大多依赖原生模块,而鸿蒙的 RN 适配层不一定完整支持。如果硬接,轻则要在鸿蒙工程里补原生代码,重则直接编译不过,维护成本很高。

后来我决定用 RN 自带的AnimatedAPI 自己实现一个。它的原理很简单,本质上就是把一个半透明高光层从容器一侧移动到另一侧,再用overflow: hidden裁剪边界。纯 JS 实现,不依赖任何第三方原生模块,同一套代码能在 iOS、Android、鸿蒙上跑,出问题也好排查。这篇博客后面给到的代码,就是我实际在项目里用过的简化版,基础入门完全够用。

2. 环境准备与工程接入:先把 RN 工程跑上鸿蒙

2.1 鸿蒙 RN 开发的工具清单

在开始写代码之前,先把环境理清楚。React Native 鸿蒙开发的基础工具和普通 RN 开发差不多,但多了鸿蒙侧的 SDK 编译环境。我建议按下面这套准备:

  • Node.js:建议 LTS 版本,我用的是 18.x,20.x 也可以;
  • JDK:React Native 编译 Android 需要 JDK 17,鸿蒙侧做 js bundle 和构建时也可能依赖 Java 环境;
  • DevEco Studio:鸿蒙应用开发的主 IDE,用来编译和签名鸿蒙壳工程,装最新稳定版就行;
  • OpenHarmony SDK / HarmonyOS SDK:DevEco Studio 会自动下载,但你要确认本机的 SDK 版本和 RN 适配层匹配;
  • hdc 命令行工具:DevEco Studio 自带,用来连真机、看日志;
  • react-native 命令行工具:建议通过 npm 全局安装或者直接用 npx,避免版本错乱。

这里面最容易踩坑的是版本匹配。RN 的版本、鸿蒙适配包的版本、DevEco Studio 的 SDK 版本,三者必须能对上。我的做法是先固定 RN 版本,再去查该版本对应的鸿蒙适配说明,不要一上来就追最新版。

2.2 初始化 RN 项目与鸿蒙壳工程

环境准备好之后,创建一个全新的 RN 工程。常规做法是执行:

npx @react-native-community/cli init RNHarmonyShimmer

这个命令会生成一个标准的 RN 项目,包含 iOS 和 Android 目录。接着需要把鸿蒙壳工程接入进来。目前 React Native 的鸿蒙适配社区提供了对应的模板和接入命令,初始化后项目里会多出一个harmony目录,这就是鸿蒙原生壳工程。

我当时走的是模板初始化路线,整体操作大致是这样:用社区推荐的 RN 鸿蒙模板创建项目,或者在一个已有 RN 工程里执行鸿蒙适配命令,让它自动生成harmony配置目录。具体命令以当时最新的适配仓库 README 为准,因为版本一升级,命令细节可能会变。

生成之后,用 DevEco Studio 打开项目里的harmony目录,SDK、签名、依赖会自动同步。这时候先把一个空的 RN 页面跑到鸿蒙真机上,再往下做功能,这是一个很重要的检查点。不要一上来就写 Shimmer,如果连基础工程都跑不通,后面所有工作都会被这里的问题堵住。

2.3 被问爆的启动白屏:先给原生启动页兜底

很多人在鸿蒙上跑起 RN 项目后,第一个遇到的问题就是启动白屏。这其实是 React Native 的运行机制造成的:RN 页面要等 JS bundle 加载、React 组件初始化完成之后才能渲染第一帧。在开发模式下,还要先连 Metro 服务,白屏时间会更明显。

要解决白屏,首先要分清阶段。RN 组件真正渲染之前,页面没有内容,这时候 Shimmer 救不了,需要原生侧兜底。通常的做法是设置原生启动页,让 App 启动后先显示品牌 Logo 或背景色,等 RN 页面可交互后再切过去。

等 RN 页面挂载完成、开始请求后端数据时,才是 Shimmer 的主场。页面先展示骨架屏,数据到了再替换成真实内容,用户看到的就不是白屏或者卡死的假象。所以 Shimmer 不是替代原生启动页,而是和它配合,把“启动白屏”和“加载空白”这两段最难熬的时间都填满。

3. 手写 Shimmer 组件:原理拆解与代码实现

3.1 一个闪光效果的“零件”有哪些

Shimmer 的本质是一次“扫光”动画。拆开来看,只需要四个零件:

  • 一个灰色的基础容器,代表占位区域的底色;
  • 一个半透明高光层,宽度比容器窄,高度比容器大,方便做旋转;
  • 一个位移动画,让高光从容器外左侧扫到容器外右侧;
  • 一个裁剪容器,把超出边界的高光部分隐藏掉,看起来光只出现在方框内部。

动画部分用 RN 的Animated.Value驱动。这个值从 0 变到 1,再映射成高光层的translateX。之所以不用setState去做逐帧位移,是因为setState会频繁触发 React 渲染更新,跟不上一秒钟几十帧的动画节奏,也容易掉帧。Animated会把每一帧的更新直接作用到原生视图上,效率高得多。

具体到参数计算,假设容器宽度为width,高光层宽度是容器宽度的一半。初始状态下高光左边界在-width位置,完全在容器左侧外面;终点状态让高光左边界到width * 2位置,因为容器右边界在width,高光宽度只有半个容器,所以这个距离足够让整根光条完全扫过容器右侧。这样动画的outputRange就设置成[-width, width * 2]

3.2 核心代码:最小可用的 Shimmer 组件

下面这段代码是精简后的核心组件,基于AnimatedAPI 实现,不依赖任何原生模块,直接复制到项目里就能用。

import React, { useEffect, useRef } from 'react'; import { Animated, Easing, StyleSheet, View, StyleProp, ViewStyle, } from 'react-native'; type ShimmerProps = { width: number; height: number; duration?: number; baseColor?: string; highlightColor?: string; borderRadius?: number; style?: StyleProp<ViewStyle>; children?: React.ReactNode; }; const Shimmer: React.FC<ShimmerProps> = ({ width, height, duration = 1500, baseColor = '#E8E8E8', highlightColor = '#F5F5F5', borderRadius = 6, style, children, }) => { // 动画进度:从 0 -> 1 const progress = useRef(new Animated.Value(0)).current; useEffect(() => { const animation = Animated.loop( Animated.timing(progress, { toValue: 1, duration, easing: Easing.inOut(Easing.ease), // 鸿蒙原生驱动支持的情况下,优先走原生驱动 useNativeDriver: true, }), ); animation.start(); return () => animation.stop(); }, [progress, duration]); // 把进度映射成高光层的水平位移 const translateX = progress.interpolate({ inputRange: [0, 1], outputRange: [-width, width * 2], }); return ( <View style={[ styles.container, { width, height, backgroundColor: baseColor, borderRadius }, style, ]} > {children} <Animated.View style={[ styles.highlight, { width: width * 0.5, height: height * 2, top: -height * 0.5, left: 0, backgroundColor: highlightColor, opacity: 0.5, transform: [ { translateX }, { rotate: '20deg' }, ], }, ]} /> </View> ); }; const styles = StyleSheet.create({ container: { overflow: 'hidden', position: 'relative', }, highlight: { position: 'absolute', }, }); export default React.memo(Shimmer);

在页面里的用法很简单,像一个普通 View 一样填宽高:

<Shimmer width={200} height={24} borderRadius={12} /> <Shimmer width={140} height={140} borderRadius={70} />

第一行可以当作一行文字骨架,第二行可以当作头像骨架。组件内部的高光层会按指定的时长反复扫描,直到组件卸载。

这里有个小细节要注意:children放在高光层前面,意味着闪光会覆盖在子元素上方。如果你需要的是一个纯灰色占位块,不要传 children;如果要在占位块上面再叠文案或图标,就要接受闪光会从它们表面扫过。这个特性在图片加载场景下很好用。

3.3 参数化与调优:闪光方向、速度、透明度

上面代码里的几个参数,durationbaseColorhighlightColorborderRadius都是高频配置。duration控制一次扫描的时长,我一般推荐 1200ms 到 1800ms。太短了会显得刺眼,太长了用户会觉着加载慢。骨架屏推荐 1500ms 左右,图片占位可以稍微快一点,1200ms 左右。

highlightColor的透明度也很关键。纯白色扫过去视觉上太硬,我习惯用接近背景色但更亮一点的颜色,比如背景是#E8E8E8,高光用#F5F5F5,再配合 0.4 到 0.6 的透明度,效果会比较柔和。如果你想让高光更明显,再把透明度调高。

闪光方向默认是水平从左到右。如果想做垂直方向,可以把高光改成横向条,把translateX换成translateYoutputRange换成[-height, height * 2]。这是最直接的方案。如果还想要对角效果,在 transform 里保持旋转角度,再调整高光初始位置就行。

还有一个进阶需求:页面里同时出现多个骨架块时,最好能让它们的闪光相位错开,否则一眼看过去像复制粘贴,很呆。可以在父组件里给不同 Shimmer 传不同的duration,或者在动画启动前给progress.setValue(-0.2)之类的初始值,实现简单的错峰。不过基础场景下,同时扫描的一致性反而更整齐,我一般只在卡片多的时候去优化这个细节。

3.4 骨架屏模板:列表加载、图片加载的通用写法

实际业务里不会只有一个孤零零的色块,更多是列表页、卡片页、详情页整屏占位。做一个列表骨架很简单,复用上面的Shimmer组件拼一下就行:

const ListItemSkeleton = () => ( <View style={{ flexDirection: 'row', padding: 16, alignItems: 'center' }}> <Shimmer width={48} height={48} borderRadius={24} /> <View style={{ flex: 1, marginLeft: 12 }}> <Shimmer width={180} height={16} borderRadius={8} /> <View style={{ height: 8 }} /> <Shimmer width={120} height={14} borderRadius={7} /> </View> </View> );

在 FlatList 的数据还没返回时,可以先渲染 4 到 6 个ListItemSkeleton,等数据回来再替换成真实的renderItem。这种做法的好处是骨架屏和真实列表样式接近,切换时不会让用户觉得“页面跳了”。

图片加载也可以这样套。包装一个组件,图片没加载完成前先显示 Shimmer,加载完成后再显示图片。因为 Shimmer 是绝对定位的高光层,天然适合覆盖在图片上方:

const ImageWithShimmer = ({ uri, width, height }) => { const [loaded, setLoaded] = React.useState(false); return ( <View style={{ width, height, overflow: 'hidden', backgroundColor: '#E8E8E8' }}> {!loaded && <Shimmer width={width} height={height} />} <Image source={{ uri }} style={{ width, height, opacity: loaded ? 1 : 0 }} onLoadEnd={() => setLoaded(true)} /> </View> ); };

这里的opacity切换会让图片有一个淡入效果,比生硬地把 Shimmer 撤掉要自然。

4. 鸿蒙端运行验证与性能优化

4.1 从 DevEco Studio 跑起来的正确姿势

工程接入之后,开发调试一般会同时开两个工具:一个是 Metro,负责提供 JS bundle;另一个是 DevEco Studio,负责编译鸿蒙壳工程、安装到真机。DevEco 里面打开harmony目录后,会自动识别鸿蒙模块。第一次编译会下载不少依赖,需要耐心等。

连真机调试时,比较推荐用 release 模式打包本地 bundle,这样可以脱离 Metro 单独运行。Release 模式下,RN 会把 JS bundle 打进 App 里,启动速度更快,也更容易复现用户实际遇到的白屏问题。开发模式虽然方便热更新,但因为要连 Metro,真机访问电脑端口这一层就多了一些网络环节,出错概率反而高。

如果你想在 DevEco 里直接看跑起来的页面,通过 hdc 命令连上设备,然后 Run 就可以了。控制台里的hdc日志和 Metro 日志要同时看,很多时候页面没出来,并不是 RN 代码问题,而是鸿蒙壳工程没把 App 装成功。

4.2 动画性能三板斧

Shimmer 看起来简单,但如果在列表里面一次放几十个未优化组件,鸿蒙端依然可能出现掉帧。我实践下来有三条原则最重要。

第一,只动transformopacity。RN 的useNativeDriver只能原生驱动这两个属性相关的动画。如果你图省事去改lefttop或者width,每帧都要触发布局计算,性能直线下降。在 Shimmer 组件里,我把所有位移都放在transformtranslateX上,就是为了遵守这个原则。

第二,动画配置里务必写useNativeDriver: true。如果鸿蒙适配层支持,动画会在原生侧执行,不经过 JS 线程,页面再复杂也不会被 JS 逻辑拖慢。如果在某些版本上遇到动画不生效,再把值改成false,牺牲一点性能换兼容性。

第三,用React.memo包裹组件,避免父组件任意一次状态更新都重启动画。Shimmer 组件内部只有progress这一个动画值,父组件刷新不应该影响它。memo 之后,只要widthheight这些 props 没变,组件就不会重复渲染,动画 loop 也能稳定运行。

4.3 让 Shimmer 从启动阶段无缝衔接到内容

光有 Shimmer 还不够,更自然的体验是从骨架屏切换到真实内容时不那么突兀。我一般会在页面容器外面包一层淡入淡出动画:数据加载中显示骨架屏,加载完成后把骨架屏隐藏,同时让内容区渐渐浮现。这样用户看到的是“闪光扫过 -> 内容淡入”,不会觉得画面被硬切。

具体到一个详情页,流程可以设计成:

  • 原生启动页显示 Logo;
  • RN 页面挂载后立即渲染 Shimmer 骨架屏;
  • 数据请求期间,Shimmer 持续扫描;
  • 数据返回后,触发内容区淡入动画,并卸载 Shimmer 组件。

这里面最需要留意的还是白屏问题。如果页面在 RN 渲染第一帧之前就有了,说明原生壳工程或 bundle 加载有问题,这时候 Shimmer 再好看也发挥不了作用。先把页面显示出来,再谈闪光效果,这个顺序不能反。

5. 常见问题与排查技巧实录

5.1 鸿蒙适配层版本和 RN 版本对不上

这是我在鸿蒙 RN 开发里遇到过最多的问题,没有之一。React Native 版本升级之后,部分原生接口会变化,如果鸿蒙适配包没有同步跟上,编译时就会出现各种奇奇怪怪的错误,最常见的是方法找不到、TurboModule注册失败、或者 ArkUI 组件映射缺失。

处理办法只有一个原则:锁版本。初始化项目时记录下 RN 的确切版本和鸿蒙适配包版本,放到package.json里固定住。后续升级不要跳版本,先看鸿蒙适配仓库的兼容矩阵,确认没问题再升。如果项目里已经有原生代码,升级 RN 往往还要同步修改鸿蒙壳工程里的调用代码,这个工作量也要提前评估。

5.2 Shimmer 动画在鸿蒙上“不动”或首帧跳变

如果动画完全不跑,先检查两件事:一是Animated.loop是否被stop了,二是是否受到系统“减少动态效果”设置影响。鸿蒙系统如果开启了减弱动画的辅助功能,RN 侧可能会关闭非必要动画,这是系统行为,不是代码 bug。

另一种更隐蔽的问题是首帧跳变。那是因为动画值默认从 0 开始,高光层一开始就在初始位置,如果初始位置能被用户看到,就会感觉闪了一下。解决办法是给动画加一个短暂的延迟,或者在组件挂载后先让高光层透明度为 0,等动画跑起来再恢复。我这个组件的初始位置在容器外部,所以正常情况下不会出现首帧闪现,但如果你改了初始位置,就要注意这个问题。

还有一点,如果useNativeDriver: true时动画不动,很可能是鸿蒙适配层对原生驱动的支持不完全。把useNativeDriver改成false再试,如果动画能跑,说明问题出在原生驱动支持上。这种场景下的性能影响通常可以接受,因为 Shimmer 动画本身的元素很少。

5.3 页面白屏与 Metro 连接失败

开发模式下,页面一直白屏,最常见的原因是 RN 页面找不着 Metro 服务。真机调试时,手机会按照打包时写入的地址去连接电脑的 Metro,如果手机和电脑不在同一个局域网,或者防火墙把端口拦了,就会一直卡在加载 bundle 的环节。

排查时可以先用浏览器访问一下 Metro 服务的地址,能出现 bundle 相关页面说明服务正常;再检查真机网络和打包配置。如果不想在网络上纠结太多,直接切 release 模式,把 bundle 打进包里,白屏问题会少一大半。

另一个细节是原生启动页。部分 RN 模板工程里没有配置鸿蒙原生启动页,App 启动后会有一段黑屏或者白屏,这其实是原生窗口的默认背景,不是 RN 渲染问题。在 DevEco 里给模块配置启动页背景色或启动图,就能把这个阶段填上。

5.4 问题速查表

现象可能原因解决方法
页面一直白屏,没有内容JS bundle 没加载,Metro 未启动或网络不通切 release 模式,或检查 Metro / 同一局域网
RN 页面出来了,但 Shimmer 不闪系统减动效设置开启,或 useNativeDriver 不支持检查辅助功能设置;改用 useNativeDriver: false
动画卡顿,掉帧明显高光层太多,或动了布局属性减少动画节点,只使用 transform / opacity
编译报错,找不到原生方法RN 版本和鸿蒙适配包版本不匹配固定版本,查适配矩阵,同步升级
Shimmer 闪了一下后消失动画值初始位置不对,或动画被父组件刷新重置设置合理的初始位置,用 React.memo 隔离
数据回来后骨架屏消失太生硬直接卸载组件,没有过渡给内容区加淡入动画,保持连贯

最后再说两句

如果你准备把 React Native 技术栈带到鸿蒙生态,我建议先跑通一个最小的 HelloWorld,再叠加这类纯 JS 动效。Shimmer 看着简单,但它把 Animated API、原生驱动、布局裁剪、性能优化这几个点都串起来了,做完之后你会对 RN 在鸿蒙上的运行机制有一个很直观的理解。我踩得最深的坑是版本匹配和动画降级,写在这里,希望你能绕过去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询