Expo expo-splash-screen 模块实战:JS API、iOS/Android 原生配置与深色模式适配全解
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
expo-splash-screen是 Expo 生态中负责「启动画面(splash screen / launch screen)」的官方模块,它覆盖从 JS 层的显隐控制(preventAutoHideAsync/hideAsync)到 iOS Storyboard、Android 资源文件的完整原生配置链路。本文以模块的 README 为主体内容,并结合仓库中 iOS 端实现、Android 端实现 与 JS 桥接层 的源码进行纵深解读。读完本文,你将能够:用 JS API 精确控制原生启动画面的自动隐藏时机、按模块规范手动配置双平台原生启动画面、并适配深色模式与 StatusBar 样式。
1. 模块定位与核心特性
启动画面是用户打开应用时看到的第一屏,出现在应用加载完成之前。Expo 官方文档中常称之为 launch screen,模块 README 的原文定义是:
expo-splash-screenallows you to customize your app's splash screen, which is the initial screen users see when the app is launched, before it has loaded.
模块提供的核心特性包括三类:内置的图片缩放模式(resize modes)、按外观区分的启动画面(per-appearance,即深色模式支持)、以及启动期间的 StatusBar 定制。
1.1 内置图片缩放模式
expo-splash-screen内置了启动画面图片的展示处理逻辑,其语义与 React Native<Image>组件的resizeMode风格保持一致,共三种模式:
CONTAIN:等比缩放图片(保持纵横比),使图片的宽和高都不超过设备屏幕对应维度,即「完整显示、可能留白」。这也是默认的 resizeMode。COVER:等比缩放图片(保持纵横比),使图片的宽和高都不小于设备屏幕对应维度,即「铺满屏幕、可能裁边」。NATIVE(仅 Android):直接利用 Android 在应用启动阶段展示静态位图的能力。由于 Android(与 iOS 不同)在启动阶段不支持对图片做拉伸处理,该模式下应用会以图片原始尺寸、居中方式展示。选择该模式需要额外完成原生配置,参见 NATIVE 模式调整 中res/drawable/splashscreen.xml与res/drawable/splashscreen_background.png两节。
1.2 按外观区分的启动画面(深色模式)
模块支持 per-appearance(亦称 dark-mode)启动画面:响应 iOS 13+ 的系统外观切换,以及 Android 10+ 的深色模式切换。实现方式在两大平台完全不同,详见后文的 iOS/Android 配置章节。
1.3 StatusBar 定制
模块允许在启动画面展示期间定制 StatusBar,其取值语义遵循 React Native StatusBar API 的定义(可查阅 React Native 官方文档StatusBar条目)。
2. JavaScript API
API 入口如下:
import * as SplashScreen from 'expo-splash-screen';2.1 自动隐藏机制:为什么需要preventAutoHideAsync
通过该模块控制的原生启动画面,会在 React Native 视图层级挂载后自动隐藏——也就是当你的应用首次render出视图组件时,原生启动画面随即隐藏。源码印证了「内容出现即隐藏」这一机制:
- Android 端:SplashScreenManager.kt 中注册了一个
ReactMarker监听器,当收到CONTENT_APPEARED标记且preventAutoHideCalled为false时调用hide(); - iOS 端:SplashScreenManager.swift 中监听
RCTContentDidAppearNotification通知,在onAppReady回调里执行同样判断。
因此,默认行为通常「够用」;只有当应用需要先准备/下载资源或完成 API 调用、再渲染真实视图时,才需要阻止自动隐藏。
2.2SplashScreen.preventAutoHideAsync()
使原生启动画面保持可见,直到调用SplashScreen.hideAsync()。约束条件是:必须在任何 React Native 视图层级渲染之前调用——既可以放在主组件的全局作用域,也可以在初始渲染null的组件中调用(见 第 3 节示例)。
返回值语义(以 README 契约为准):
Promiseresolve 为true:阻止自动隐藏成功;- resolve 为
false:原生启动画面此前已被阻止过自动隐藏(例如已调用过本方法); Promisereject:大概率意味着原生启动画面此时已无法被阻止自动隐藏(调用时它已经隐藏了)。
从当前源码结构看,双平台的preventAutoHideAsync实现都会将userControlledAutoHideEnabled置为true并直接返回true(见 Android 实现 与 iOS 实现);源码注释明确说明该标记是供expo-router等上层库判断「启动画面是否由用户接管」的协议信号——调用过preventAutoHideAsync后,internalMaybeHideAsync(内部自动隐藏入口)就不会再主动隐藏。
2.3SplashScreen.hideAsync()
隐藏原生启动画面,仅当此前调用过preventAutoHideAsync()时才真正起作用。Promise在启动画面隐藏后 resolve。
2.4SplashScreen.setOptions(options)
从 SplashScreen.types.ts 的类型定义看,可配置隐藏动画的默认行为:
export type SplashScreenOptions = { /** 淡出动画时长(毫秒)。@default 400 */ duration?: number; /** 是否以淡出动画方式隐藏启动画面。@platform ios @default false */ fade?: boolean; };两个参数的底层实现差异值得一读:
- iOS:SplashScreenManager.swift 中,
fade为true时走UIView.transition(..., options: .transitionCrossDissolve)交叉溶解过渡,随后移除 loadingView;否则直接isHidden = true并移除视图; - Android:SplashScreenManager.kt 中通过
setOnExitAnimationListener对SplashScreenView执行alpha(0f)淡出动画(使用AccelerateInterpolator),时长即duration,并对 API 31 以下的系统做了splashScreenViewProvider.remove()的分支处理。
需要注意平台边界:SplashScreen.native.ts 中,setOptions在 Expo Go 内会打印警告并直接返回——该能力需要在 development build 中使用。
2.5 平台实现分发
模块采用 Expo 模块体系的标准分发:JS 层入口 src/index.ts 同时导出 SplashScreen.ts(Web/无原生环境下的 no-op 占位实现,函数体为空)与 SplashScreen.native.ts(通过requireOptionalNativeModule('ExpoSplashScreen')获取原生模块桥接)。原生端模块名在双平台均注册为ExpoSplashScreen(见 SplashScreenModule.kt 与 SplashScreenModule.swift)。
3. 使用示例
3.1 在全局作用域调用preventAutoHideAsync
App.tsx:
import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; import * as SplashScreen from 'expo-splash-screen'; // Prevent native splash screen from autohiding before App component declaration SplashScreen.preventAutoHideAsync() .then((result) => console.log(`SplashScreen.preventAutoHideAsync() succeeded: ${result}`)) .catch(console.warn); // it's good to explicitly catch and inspect any error export default class App extends React.Component { componentDidMount() { // Hides native splash screen after 2s setTimeout(async () => { await SplashScreen.hideAsync(); }, 2000); } render() { return ( <View style={styles.container}> <Text style={styles.text}>SplashScreen Demo! 👋</Text> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#aabbcc', }, text: { color: 'white', fontWeight: 'bold', }, });要点:preventAutoHideAsync在模块导入后、组件声明前于全局作用域执行,并用.catch(console.warn)显式捕获错误——因为自动隐藏可能已发生,Promise 会被 reject。
3.2 在初始渲染null的组件中调用
App.tsx:
import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; import * as SplashScreen from 'expo-splash-screen'; export default class App extends React.Component { state = { appIsReady: false, }; async componentDidMount() { // Prevent native splash screen from autohiding try { await SplashScreen.preventAutoHideAsync(); } catch (e) { console.warn(e); } this.prepareResources(); } /** * Method that serves to load resources and make API calls */ prepareResources = async () => { await performAPICalls(...); await downloadAssets(...); this.setState({ appIsReady: true }, async () => { await SplashScreen.hideAsync(); }); } render() { if (!this.state.appIsReady) { return null; } return ( <View style={styles.container}> <Text style={styles.text}>SplashScreen Demo! 👋</Text> </View> ) } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#aabbcc', }, text: { color: 'white', fontWeight: 'bold', }, });这种模式的适用场景:应用需要在首屏渲染前完成资源加载与 API 调用。组件先渲染null占位,资源就绪后再切到真实视图并调用hideAsync()收尾。
4. 安装
- 托管(managed)Expo 项目:
npx expo install expo-splash-screen(README 建议同时参阅 Expo 官方文档的 SplashScreen 章节)。 - bare React Native 项目:需先确保已安装并配置
expo包(参考 Expo 官方「Installing Expo Modules」指南),再执行同样的npx expo install expo-splash-screen。 - iOS:安装后运行
npx pod-install。
5. iOS 原生配置(手动)
要获得原生启动画面(iOS 生态中称为LaunchScreen)行为,需要提供SplashScreen.storyboard或SplashScreen.xib文件,并配置 Xcode 工程。官方推荐流程为六步:
- 向
Images.xcassets添加图片; - 创建
SplashScreen.storyboard; - 为 Storyboard 中的
ImageView选择Content Mode; - 将
SplashScreen.storyboard标记为 LaunchScreen; - (可选)启用深色模式;
- (可选)定制 StatusBar。
5.1 向Images.xcassets添加图片
- 在 Xcode 工程打开
.xcassets(通常名为Images.xcassets或Assets.xcassets); - 在内容面板新建
New image set,命名为SplashScreen; - 提供准备好的启动画面图片(需要三个不同的 @1x/@2x/@3x 缩放版本)。
5.2 创建SplashScreen.storyboard
这是启动画面的实际定义文件,系统会用它来渲染启动画面。
- 创建
SplashScreen.storyboard文件; - 添加
View Controller:打开Library(右上角+按钮)→ 找到View Controller元素 → 拖入.storyboard; - 添加
Image View:先移除View Controller中其他View元素 → 从Library找到Image View→ 拖拽为View Controller的子级; - 设置
Storyboard ID为SplashScreenViewController:选中View Controller,在右侧Identity Inspector中修改; - 勾选
Is Initial View Controller:在Attributes Inspector的 View Controller 分区中勾选; - 配置
Image View图片源:在Attributes Inspector的Image参数中选择SplashScreen; - 配置
Image View的Background:需要#RRGGBB值时,选择Custom,在弹出的Colors Popup第二个标签页中从下拉框选择RGB Sliders。
5.3 为ImageView选择Content Mode
这一步决定图片如何展示在屏幕上:
- 打开
SplashScreen.storyboard,从View Controller中选中Image View; - 在右侧
Attributes Inspector找到Content Mode; - 选择其一:
Aspect Fit—— 对应CONTAIN缩放模式;Aspect Fill—— 对应COVER缩放模式;
- 也可以选择其他选项以实现不同的定位与缩放效果。
5.4 将SplashScreen.storyboard标记为 LaunchScreen
新创建的SplashScreen.storyboard必须在 Xcode 工程中标记为Launch Screen File,才能从应用启动的最初阶段就呈现:
- 在
Project Navigator中选中工程; - 在
TARGETS面板选中工程名,切换到General标签; - 找到
App Icons and Launch Images分区的Launch Screen File选项; - 选择或输入
SplashScreen作为该选项的值。
5.5 (可选)启用深色模式
iOS 端有两种互补做法:
做法 A:提供不同的背景色(named colors)
- 在
.xcassets中(可新建,也可复用已有如图片的 asset catalog)创建New Color Set,命名为SplashScreenBackground;将Attributes Inspector中的Appearance改为Any, Dark,分别为每种模式选择颜色; - 在
SplashScreen.storyboard中将其选为Image View的Background(Background参数选择你创建的SplashScreenBackgroundnamed color)。
若还要让背景色铺满全屏,需要把SplashScreen.storyboard改为「一个顶层View+ 两个Image View子视图」的结构(底层为纯色背景图,上层为真正的启动画面图):
- 第一个
Image View(背景色):Image设为SplashScreenBackground,Content Mode设为Scale To Fill;通过Add new constraints底部菜单,确保未勾选Constrain to margin,每个方向的下拉框选择父View、值设0,点击Add 4 Constraints使其撑满父视图; - 第二个
Image View(真正启动画面图):选择正确的Image与期望的Content Mode,同样以四边约束撑满父视图。
做法 B:提供不同的深色模式启动画面图片
- 打开
SplashScreen图片集(前面创建的 asset); - 在
Attributes Inspector的Appearances分区选择Any, Dark,为深色模式框中放入专门准备的深色图片。
系统切换到深色模式时即会改用这张图片。
5.6 (可选)定制 StatusBar
- StatusBar hiding:在
TARGETS面板选中工程名,切换到Info标签,添加或修改Status bar initially hidden属性; - StatusBar style:同样在
Info标签,添加或修改Status bar style属性。
5.7 iOS 端实现印证
从源码看,JS 隐藏调用只是「摘掉」系统之上叠加的启动视图。SplashScreenManager.swift 的showSplashScreen()会从Info.plist读取UILaunchStoryboardName(缺省为SplashScreen)来实例化 Storyboard 作为loadingView;若资源缺失则静默返回——注释说明这是为了在 brownfield(混合集成)应用中避免崩溃。hide()(L35-L58)在 App Extension 环境中直接跳过,主线程上执行淡出或直接移除视图,这解释了为何fade选项标注为@platform ios。
6. Android 原生配置(手动)
要获得全原生的启动画面行为,expo-splash-screen需要挂接到原生视图层级,并消费若干放在/android/app/src/res目录下的资源。官方手动配置流程为八步:
- 配置
res/drawable/splashscreen_image.png; - 配置
res/values/colors.xml; - 配置
res/drawable/splashscreen.xml; - 配置
res/values/styles.xml; - 配置
AndroidManifest.xml; - (可选)定制
resizeMode; - (可选)启用深色模式;
- (可选)定制 StatusBar。
6.1res/drawable/splashscreen_image.png
提供启动画面图片并放入res/drawable目录。该图片会在 Android 挂载应用原生视图层级时立刻加载。
NATIVE模式调整:若已在res/values/strings.xml中将<string name="expo_splash_screen_resize_mode">覆盖为native,则需为不同 DPI 设备准备多份资源。可在res目录下建立若干drawable-*子目录(X为不同 DPI 等级),系统按设备 DPI 选择对应版本:
res/drawable-mdpi— 1x — 中密度(~160dpi,基线密度);res/drawable-hdpi— 1.5x — 高密度(~240dpi);res/drawable-xhdpi— 2x — 超高密度(~320dpi);res/drawable-xxhdpi— 3x — 超高超高密度(~480dpi);res/drawable-xxxhdpi— 4x — 特超高密度(~640dpi)。
每个目录都应有同名的splashscreen_image.png,但分辨率按上述倍率缩放。
6.2res/values/colors.xml
该文件存放应用原生层复用的颜色。更新(或新建)以下内容:
<resources> + <color name="splashscreen_background">#AABBCC</color> <!-- #AARRGGBB or #RRGGBB format --> <!-- Other colors defined for your application --> </resources>6.3res/drawable/splashscreen.xml
该文件描述启动画面视图应如何被 Android 系统绘制。创建文件并写入:
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@color/splashscreen_background"/> + </layer-list>NATIVE模式调整:若已在strings.xml覆盖为native,则应追加一个居中位图项:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@color/splashscreen_background"/> + <item> + <bitmap android:gravity="center" android:src="@drawable/splashscreen_image"/> + </item> </layer-list>6.4res/values/styles.xml
定位主 Activity 的主题(位于/android/app/src/res/values/styles.xml,缺失则新建):
<!-- Main activity theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> + <item name="android:windowBackground">@drawable/splashscreen</item> <!-- 指示系统以 'splashscreen.xml' 作为整个应用的背景 --> <!-- Other style properties --> </style>6.5AndroidManifest.xml
让主AndroidManifest.xml中<activity>的android:theme指向包含启动画面配置的 style:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp"> ... <application ...> + <!-- 确保 'android:theme' 指向包含原生启动画面引用的 style(见 'styles.xml') --> <activity android:name=".MainActivity" + android:theme="@style/AppTheme" ... > ... </activity> </application> </manifest>6.6 (可选)定制resizeMode
默认 resizeMode 为CONTAIN。如需更改,在res/values/strings.xml中覆盖:
--- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <resources> <string name="app_name">sdk42</string> + <string name="expo_splash_screen_resize_mode">contain|cover|native</string> </resources>6.7 (可选)启用深色模式
不同的背景色—res/values-night/colors.xml:在res/values-night目录下创建与colors.xml同构的文件,系统在深色模式下会读取其中的值:
<resources> + <color name="splashscreen_background">#AABBCC</color> <!-- #AARRGGBB or #RRGGBB format --> </resources>不同的启动画面图片—res/drawable-night/splashscreen_image.png:在res/drawable-night目录下放置与浅色版同名的图片即可。此步骤可选——例如你只有一张浅色 logo,希望两种模式下仅背景色不同。
6.8 (可选)定制 StatusBar
- StatusBar hiding:更新
res/values/styles.xml,使状态栏完全隐藏(取消隐藏则删除该条目或改为false):
<!-- Main/SplashScreen activity theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowBackground">@drawable/splashscreen</item> + <item name="android:windowFullscreen">true</item> <!-- Other style properties --> </style>若存在多个目录下的styles.xml含有完全相同的style条目(例如res/values-night、res/values-night-v23),务必同步修改。android:windowFullscreen的语义可参阅 Android 官方R.attr文档。
- StatusBar style:仅对 Android 6.0+ 设备生效。要在指定系统颜色模式下强制
light/dark状态栏样式,需要准备或更新res/values-v23/styles.xml(该属性自 API 23 起支持,因此必须放在特定命名的目录中):
<!-- Main/SplashScreen activity theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowBackground">@drawable/splashscreen</item> + <item name="android:windowLightStatusBar">true|false</item> <!-- Other style properties --> </style>取值:true为深色图标,false为浅色图标。同样注意同步res/values-night-v23等目录下的同名style条目。多 API 级别资源覆盖的机制详见 Android 官方「providing resources」文档。
6.9 Android 端实现印证
从 SplashScreenManager.kt 看,模块基于 AndroidXinstallSplashScreen()API 工作:
keepSplashScreenOnScreen期间通过OnPreDrawListener持续返回false阻止内容绘制——源码注释解释了这么做的原因:setKeepOnScreenCondition()在 API 33 以下不可用,因此自行实现;hide()只是把开关置为false,真正的移除发生在系统 splash 退出动画回调里(setOnExitAnimationListener,见 L38-L53),并按Build.VERSION分支处理SplashScreenView.remove();- 针对 API 31–33 上 splash 退出监听器可能在 Activity 停止后触发导致的
SurfaceControl.checkNotReleased()崩溃(Google Issue Tracker 242118185),专门注册了ActivityLifecycleCallbacks,在onActivityStopped时clearOnExitAnimationListener()作为缓解措施。
这些细节说明:JS 层hide()的调用是「解除保持条件」,最终呈现仍由 Android 系统 splash 框架完成,这也决定了NATIVE模式必须走纯资源配置(strings.xml+ 多密度 drawable)路线。
7. 配置插件(Config Plugin)
除手动配置外,模块自带配置插件,可将上述资源文件自动化生成:插件入口为 withSplashScreen.ts,按平台拆分为withIosSplashScreen、withAndroidSplashScreen等插件(见 plugin/src 目录),分别处理 iOS 的 assets/Info.plist/Storyboard 与 Xcode 工程修改,以及 Android 的 drawable/strings/styles/MainActivity 注入。各插件均配有测试用例,如 withIosSplashScreen-test.ts、withAndroidSplashDrawables-test.ts 等,可用来核对插件产出的资源结构。使用方式是通过app.json的plugins字段声明expo-splash-screen并传入图片/背景色配置(模块根目录提供 app.plugin.js 作为插件入口)。
8. 已知问题
8.1 iOS 缓存
iOS 应用的启动画面有时会遇到缓存问题:新图片出现前,旧图片会闪现一下。官方建议:重启设备、卸载并重新安装应用;但缓存可能持续一两天,请对前述步骤保持耐心。
8.2NATIVE模式会将启动画面图片略微上推
即 NATIVE 模式预览 中可见的偏移现象。模块维护方已知晓该问题,截至 README 撰写时尚无解决方案。
9. 从旧版本迁移
9.1 从expo-splash-screen< 0.12.0 迁移
旧版代码保持向后兼容,仍可按原方式工作。若要迁移到新的模块 API,步骤如下:
- 将项目从
react-native-unimodules迁移到expo-modules-core; - 从
MainActivity中移除旧的expo-splash-screen代码:
--- a/android/app/src/main/java/com/helloworld/MainActivity.java +++ b/android/app/src/main/java/com/helloworld/MainActivity.java import com.facebook.react.ReactRootView; import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; -import host.exp.exponent.experience.splashscreen.legacy.singletons.SplashScreen; -import host.exp.exponent.experience.splashscreen.legacy.SplashScreenImageResizeMode; - public class MainActivity extends ReactActivity { @Override protected void onCreate(Bundle savedInstanceState) { // This is required for expo-splash-screen. setTheme(R.style.AppTheme); super.onCreate(null); - // SplashScreen.show(...) has to be called after super.onCreate(...) - SplashScreen.show(this, SplashScreenImageResizeMode.CONTAIN, ReactRootView.class, false); }- 在
strings.xml中覆盖默认resizeMode:
--- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <resources> <string name="app_name">sdk42</string> + <string name="expo_splash_screen_resize_mode">contain</string> </resources>10. 致谢(Hall of Fame)
该模块构建在以下开源项目(Expo 仓库内 README 的 Hall of Fame 章节)的坚实工作之上:
- react-native-splash-screen(crazycodeboy)
- react-native-bootsplash(zoontek)
- react-native-make(bamlab)
11. 延伸阅读
- 模块变更记录:CHANGELOG
- JS 桥接层与类型定义:SplashScreen.native.ts、SplashScreen.types.ts
- 原生实现:iOS SplashScreenManager、iOS 模块定义、Android SplashScreenManager、Android 模块定义
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考