Android 单 Activity 架构下的 Splash Screen 与主题规范指南
在传统的 Android 多 Activity 架构中,开发者通常会创建一个独立的SplashActivity来展示品牌 Logo 或启动动画,然后再跳转到MainActivity并将自身finish()掉。
然而,在单 Activity 架构(尤其是结合 Jetpack Compose Navigation 时)中,这种做法不再适用。如果新增一个SplashActivity,就打破了“单 Activity”的初衷;如果直接把 Splash 逻辑塞进MainActivity的布局中,又无法解决应用冷启动时系统渲染第一帧之前的白屏/黑屏问题。
为了解决这一问题,Android 12 引入了官方的Splash Screen API,并通过androidx.core:core-splashscreen库向后兼容。在单 Activity 架构中,它的核心思想是:“利用系统级的主题切换机制,让同一个 Activity 在冷启动瞬间使用 Splash 主题,在绘制第一帧内容前自动切换回普通主题。”
下面将详细讲解这一机制的标准做法。
1. 核心原理:postSplashScreenTheme机制
在引入了core-splashscreen库后,我们可以定义一个专门继承自Theme.SplashScreen的主题。
这个主题不仅可以定义启动图标、背景色,最关键的是它必须包含一个名为postSplashScreenTheme的属性。
它的工作流如下:
- 进程启动前:系统读取
AndroidManifest.xml中 Activity 配置的SplashTheme。 - 冷启动瞬间:系统利用该主题的属性(如
windowSplashScreenBackground)快速绘制一个启动画面,避免白屏。 - Activity 创建:在
Activity.onCreate()中,我们在super.onCreate()之前调用installSplashScreen()。 - 主题动态切换:
installSplashScreen()会读取postSplashScreenTheme属性中指定的真实业务主题,并将当前 Activity 的 Context Theme动态替换为该业务主题。 - UI 渲染:接下来执行
setContent { ... }或setContentView(),此时底层的原生 Context 已经恢复为正常的业务主题,Compose 可以正常读取所需的各种 Window 属性(如无标题栏、状态栏透明等)。
2. 标准落地步骤与代码示例
第一步:引入依赖
确保在build.gradle中引入了官方的兼容库:
implementation("androidx.core:core-splashscreen:1.0.1") // 或最新版本第二步:定义主题配置 (themes.xml/styles.xml)
在你的资源文件中,你需要同时定义业务主题和启动主题。
1. 正常的业务主题(单 Activity 推荐 NoActionBar):
<!-- 这是你原本正常使用的业务主题,负责沉浸式、颜色等 --><stylename="AppTheme"parent="Theme.Material3.DayNight.NoActionBar"><!-- 这里可以配置状态栏透明,以便 Compose 处理 Edge-To-Edge --> <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item> <!-- 其他业务相关的属性配置 --></style>2. 专用的 Splash 主题:
<!-- 必须继承自 Theme.SplashScreen --><stylename="AppSplashTheme"parent="Theme.SplashScreen"><!-- 启动页的背景颜色 --> <item name="windowSplashScreenBackground">@color/white</item> <!-- 启动页中心展示的 Icon,可以放应用的 Logo --> <item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_round</item> <!-- 【核心属性】指定启动结束后,Activity 要恢复成哪个真实主题 --> <item name="postSplashScreenTheme">@style/AppTheme</item> <!-- 可选:如果 Logo 有动画,可以设置动画时长上限 --> <item name="windowSplashScreenAnimationDuration">1000</item></style>第三步:在 Manifest 中应用 Splash 主题
在AndroidManifest.xml中,将这个单 Activity 的主题指向刚刚定义的AppSplashTheme,而不是AppTheme。
<activityandroid:name=".MainActivity"android:theme="@style/AppSplashTheme"android:exported="true"><intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/></intent-filter></activity>第四步:在 Activity 中调用installSplashScreen()
这是最关键的一步,必须在super.onCreate()之前调用。
importandroid.os.Bundleimportandroidx.activity.ComponentActivityimportandroidx.activity.compose.setContentimportandroidx.core.splashscreen.SplashScreen.Companion.installSplashScreenclassMainActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){// 1. 必须在 super.onCreate() 之前调用// 它会负责执行主题的切换:将 AppSplashTheme 替换为 postSplashScreenTheme 指定的 AppThemeinstallSplashScreen()super.onCreate(savedInstanceState)// 2. 这里已经处于 AppTheme 环境下了setContent{YourComposeAppTheme{// 你的单 Activity Compose 导航容器AppNavHost()}}}}3. 常见误区与避坑指南
直接给 Activity 永久套用
Theme.SplashScreen
如果只在 Manifest 中写了AppSplashTheme,却忘了在代码里调用installSplashScreen(),或者没有配置postSplashScreenTheme,那么 Activity 就会一直运行在 Splash 环境下。这会导致很多意想不到的 UI 属性丢失,因为 Splash 主题并没有提供常规业务所需的那些底层配置。试图用 Compose 解决冷启动白屏
有很多开发者尝试在 Compose 中画一个满屏的 Logo 来做 Splash。但请注意,从用户点击桌面图标,到 Android 进程启动,再到 Compose 引擎初始化并渲染出第一帧,中间是有一段明显的时间差的。如果不用原生SplashTheme的windowSplashScreenBackground兜底,这段时间必定会闪白屏或黑屏。单 Activity 中的“业务等待页”与原生 Splash 的结合
如果你的 App 启动时需要请求网络、校验 Token、初始化重型 SDK 等,原生 Splash API 的停留时间是不够的(它默认只停留在绘制出第一帧前)。标准解法是无缝衔接:
- 阶段 A(进程初始化):系统展示原生
AppSplashTheme(Logo + 纯色背景),避免白屏。 - 阶段 B(业务等待,即 Compose 的 SplashScreen):
- Compose 渲染出第一帧,此时进入你的 Compose
SplashScreen路由节点。 - UI 设计(需要你自己写):这个 Compose 页面并不是系统自动生成的,你需要自己用 Compose 把它写得和原生 Splash 一模一样(一样的背景色、一样的 Logo 居中大小)。这样一来,在原生 Splash 消失、Compose 页面展现的瞬间,用户在视觉上感觉不到画面发生了切换,认为是同一个 Splash 页面在继续展示。
- 如何保证一模一样:
- 背景色与暗黑模式兼容:在 Compose 页面顶层的
Box或Surface中,绝对不能硬编码使用Color.White等固定色值,否则在暗黑模式下会导致严重的闪屏。你需要确保背景色与原生themes.xml中windowSplashScreenBackground保持动态一致。- 方案 A(推荐,使用 Compose 主题系统):确保你的 Compose Theme 能够跟随系统深色模式切换,并使用
MaterialTheme.colorScheme.background作为背景色。 - 方案 B(直接引用原生颜色):使用
colorResource(id = R.color.splash_bg),前提是你在res/values/colors.xml和res/values-night/colors.xml中分别定义了白天和黑夜的splash_bg,并将其同时用于themes.xml的windowSplashScreenBackground配置中。
- 方案 A(推荐,使用 Compose 主题系统):确保你的 Compose Theme 能够跟随系统深色模式切换,并使用
- Logo 资源:使用相同的图标资源。
- Logo 大小:官方的 SplashScreen API 对中间 Icon 的限制是
288dp,并且如果在有圆形的遮罩内通常被裁切到192dp或者更小。在 Compose 的Image修饰符中,给定具体的宽度和高度(通常在96dp到192dp之间,需要基于你的原始矢量图或切图具体调参),将其完全放置在屏幕正中央contentAlignment = Alignment.Center。 - 状态栏适配:因为是全屏展示,Compose
SplashScreen需要调用Modifier.fillMaxSize(),如果原本的 Activity 做到了 Edge-To-Edge 沉浸式,那 Logo 的居中自然就能对齐。如果仍有微小偏差,需要你真机对比微调(通常在 1~2 个像素以内用户是无感知的)。
- 背景色与暗黑模式兼容:在 Compose 页面顶层的
- 如何保证一模一样:
- 业务职责:在这个 Compose 页面里,你可以使用 ViewModel 结合
LaunchedEffect来做网络请求、读取本地 Token、拉取配置等耗时操作。这意味着可以适当进行较长时间的等待。 - 关于 Application 初始化:强依赖组件(如 Crash 收集、日志、DI 框架如 Koin)仍必须留在
Application.onCreate()。但弱依赖组件、隐私合规授权后的 SDK 初始化、首页需要的预加载数据,非常推荐搬到这个 Compose SplashScreen 阶段来做,以此缩短 Application 的冷启动耗时。
- Compose 渲染出第一帧,此时进入你的 Compose
- 阶段 C(动态路由跳转):
- 当上述业务就绪后,根据结果决定跳去哪里。
- 如果有网且已登录,
navController.navigate("home")。 - 如果没网或 Token 失效,
navController.navigate("login")或错误引导页。 - 关键动作:跳转时必须把 Splash 路由移出返回栈(
popUpTo("splash") { inclusive = true }),防止用户按返回键退回启动页。
- 阶段 A(进程初始化):系统展示原生
