配置启动页(Splash Screen)
启动页(Splash Screen)是应用启动时显示的过渡画面,用于提升用户体验——在 Flutter 引擎初始化期间展示品牌标识,避免出现白屏或黑屏。
Flutter 2.5 及以上版本引入了 Android 内置启动屏(Android Splash Screen API)支持,可以方便地在各平台配置原生启动页。本章将介绍如何在 Android、iOS、HarmonyOS、Web 以及桌面平台配置启动页,以及如何使用 Flutter 代码实现自定义启动动画。
启动页的工作原理
用户点击图标 → 系统显示原生启动页 → Flutter 引擎初始化 → 首帧渲染完成 → 启动页消失启动页的核心作用是填补 Flutter 引擎初始化到首帧渲染之间的空白时间。各平台的原生启动页由系统直接显示,无需等待 Flutter 引擎就绪,因此能实现即时响应。
Android
Android 12 及以上版本强制要求所有应用提供启动页,Flutter 从 2.5 版本开始适配了这一机制。
方式一:使用 flutter_native_splash 插件(推荐)
flutter_native_splash 是社区最常用的启动页生成工具,可以一键为 Android、iOS 和 Web 生成启动页资源。
WARNING
flutter_native_splash 不支持鸿蒙平台,鸿蒙启动页需手动配置(见下方 HarmonyOS 章节)。
1. 添加依赖
在 pubspec.yaml 中添加:
dev_dependencies:
flutter_native_splash: ^2.4.3然后执行:
flutter pub get2. 配置启动页
在 pubspec.yaml 中添加 flutter_native_splash 配置段:
flutter_native_splash:
color: "#FFFFFF" # 背景色(必填)
image: assets/images/splash.png # 居中显示的图片(可选)
branding: assets/images/brand.png # 底部品牌图片(可选)
android_12:
image: assets/images/splash_12.png # Android 12+ 专用图标
color: "#FFFFFF"
ios: true
android: true
web: trueTIP
color为必填项,指定启动页背景色image建议使用 1024 × 1024 像素的 PNG 图片,实际显示会被缩放- Android 12+ 的启动页样式与低版本不同,建议通过
android_12单独配置
3. 生成启动页
dart run flutter_native_splash:create该命令会自动修改各平台的原生配置文件,生成对应的启动页资源。
4. 恢复默认
如果需要移除自定义启动页,恢复为默认白屏:
dart run flutter_native_splash:remove方式二:手动配置 Android 启动页
如果不使用插件,也可以手动配置。
低版本 Android(< 12)
编辑 android/app/src/main/res/drawable/launch_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="#FFFFFF" />
</item>
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash_image" />
</item>
</layer-list>将 splash_image.png 放到 android/app/src/main/res/drawable/ 目录下。
Android 12+
Android 12+ 使用 styles.xml 中的 windowSplashScreen* 属性配置。由于这些属性仅在 API 31+ 可用,需要在 values-v31 目录下单独定义。
创建 android/app/src/main/res/values-v31/styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_image</item>
</style>
</resources>WARNING
Android 12+ 的 windowSplashScreenAnimatedIcon 尺寸限制为 直径 240dp 的圆内,超出部分会被裁切。
iOS
iOS 的启动页通过 Launch Screen Storyboard 实现。
使用 flutter_native_splash 插件
按照上面 Android 章节的步骤配置即可,插件会自动处理 iOS 的 LaunchScreen.storyboard 和相关图片资源。
手动配置 iOS 启动页
1. 修改 LaunchScreen.storyboard
编辑 ios/Runner/Base.lproj/LaunchScreen.storyboard:
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0">
<scenes>
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsToSubviews="YES"
userInteractionEnabled="NO"
contentMode="scaleAspectFit"
horizontalHuggingPriority="251"
verticalHuggingPriority="251"
image="SplashImage"
translatesAutoresizingMaskIntoConstraints="NO"
id="8JM-mR-2Jx">
<rect key="frame" x="200" y="200" width="200" height="200"/>
<constraints>
<constraint firstAttribute="width" constant="200" id="HcV-Li-k3f"/>
<constraint firstAttribute="height" constant="200" id="qIg-wL-KbF"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1"/>
<constraints>
<constraint firstItem="8JM-mR-2Jx" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="KSH-fg-DVj"/>
<constraint firstItem="8JM-mR-2Jx" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="m6z-ct-aQj"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ago" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="SplashImage" width="1024" height="1024"/>
</resources>
</document>2. 添加图片资源
将启动页图片 SplashImage.png 添加到 ios/Runner/Assets.xcassets/LaunchImage.imageset/ 目录,并编辑 Contents.json:
{
"images": [
{
"idiom": "universal",
"filename": "SplashImage.png",
"scale": "1x"
},
{
"idiom": "universal",
"filename": "SplashImage@2x.png",
"scale": "2x"
},
{
"idiom": "universal",
"filename": "SplashImage@3x.png",
"scale": "3x"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}TIP
iOS 对启动页的审核较严,建议避免使用广告性质的启动页,保持简洁的品牌展示即可。
HarmonyOS
鸿蒙平台目前没有专用的启动页插件,需要手动配置。
在 ohos/entry/src/main/module.json5 中配置启动页:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"startWindowIcon": "$media:splash_image",
"startWindowBackground": "$color:start_window_background"
}
]
}
}startWindowIcon:启动页图标,将splash_image.png放入ohos/entry/src/main/resources/base/media/目录startWindowBackground:启动页背景色,在ohos/entry/src/main/resources/base/element/color.json中定义:
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}Web
使用 flutter_native_splash 插件
插件会自动修改 web/index.html,在页面中插入启动页样式和图片。
手动配置 Web 启动页
编辑 web/index.html,在 <body> 标签内添加启动页元素:
<style>
.splash-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #FFFFFF;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.splash-screen img {
width: 200px;
height: 200px;
}
</style>
<div class="splash-screen">
<img src="assets/images/splash.png" alt="Loading..." />
</div>Flutter 引擎初始化完成后会自动移除该元素。
桌面平台(Windows / macOS / Linux)
Flutter 桌面端目前没有统一的原生启动页配置方式。如果需要避免白屏,可以在窗口创建时设置窗口背景色。
Windows
编辑 windows/runner/win32_window.cpp,在 CreateAndShow 函数中修改窗口背景色:
HWND window = CreateWindow(
window_class, title.c_str(),
WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX, // 禁用最大化
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
nullptr, nullptr, instance, this);
// 设置窗口背景色为白色
HBRUSH brush = CreateSolidBrush(RGB(255, 255, 255));
SetClassLongPtr(window, GCLP_HBRBACKGROUND, (LONG_PTR)brush);macOS
编辑 macos/Runner/MainFlutterWindow.swift,设置窗口背景色:
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController()
let windowFrame = self.frame
let contentViewController = flutterViewController
self.contentViewController = contentViewController
self.setFrame(windowFrame, display: true)
// 设置窗口背景色
self.backgroundColor = NSColor.white
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}Linux
编辑 linux/my_application.cc,在 my_application_activate 函数中通过 CSS 设置窗口背景色:
// 在 gtk_widget_show(window) 之前添加
GtkCssProvider* provider = gtk_css_provider_new();
gtk_css_provider_load_from_data(provider,
"window { background-color: #FFFFFF; }", -1, nullptr);
gtk_style_context_add_provider(
gtk_widget_get_style_context(GTK_WIDGET(window)),
GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g_object_unref(provider);自定义 Flutter 启动动画
原生启动页只能在 Flutter 引擎启动前展示一张静态图片和背景色。如果你的应用想要更丰富的效果——比如 Logo 渐入、缩放动画、加载进度条——就需要在 Flutter 侧再实现一个自定义启动页。
原生启动页和自定义启动页的关系
很多初学者会困惑:已经有了原生启动页,为什么还要写一个 Flutter 启动页?它们之间是什么关系?
应用启动时,用户看到的画面会经历三个阶段:
┌─────────────┐ ┌──────────────────┐ ┌──────────┐
│ 原生启动页 │ ──► │ Flutter 自定义启动页 │ ──► │ 应用主页 │
│ (静态图片) │ │ (动画 + 初始化) │ │ │
└─────────────┘ └──────────────────┘ └──────────┘
由系统直接显示 Flutter 渲染 正常页面
无需等待引擎 可做动画效果| 阶段 | 由谁负责 | 能做什么 | 用户感知 |
|---|---|---|---|
| 原生启动页 | 操作系统 | 显示静态图片 + 背景色 | 点击图标后立刻出现 |
| Flutter 启动页 | Flutter 代码 | 渐入、缩放、加载动画等 | 从原生启动页无缝过渡 |
| 应用主页 | Flutter 代码 | 正常的应用界面 | 初始化完成后进入 |
为什么要分两层?
原生启动页由操作系统在 Flutter 引擎启动之前就显示,所以能做到「点击图标立刻出现」。但它的能力有限,只能显示一张静态图片。等 Flutter 引擎就绪后,原生启动页会自动消失,这时就可以用 Flutter 代码做更丰富的动画效果了。
关键在于让两层启动页看起来无缝衔接:原生启动页的背景色和图片,要和 Flutter 启动页的初始状态保持一致,这样用户就不会感觉到「闪烁」或「跳变」。
最简单的自定义启动页
先看一个最简单的版本——不做动画,只在 Flutter 启动页上等待初始化完成,然后跳转到主页:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const SplashPage(), // 应用启动时先显示启动页
);
}
}
/// 启动页 —— 等待初始化完成后跳转到主页
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@override
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> {
@override
void initState() {
super.initState();
_initializeApp(); // 页面创建时开始初始化
}
Future<void> _initializeApp() async {
// 并行执行多个初始化任务
await Future.wait([
_loadLocalData(), // 读取本地存储
_fetchAppConfig(), // 请求远程配置
_checkLoginStatus(), // 检查登录状态
]);
// 初始化完成后,跳转到主页
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomePage()),
);
}
}
Future<void> _loadLocalData() async {
await Future.delayed(const Duration(seconds: 1));
}
Future<void> _fetchAppConfig() async {
await Future.delayed(const Duration(seconds: 1));
}
Future<void> _checkLoginStatus() async {
await Future.delayed(const Duration(milliseconds: 500));
}
@override
Widget build(BuildContext context) {
// 背景色要和原生启动页一致,避免闪烁
return const Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FlutterLogo(size: 120),
SizedBox(height: 48),
CircularProgressIndicator(),
],
),
),
);
}
}
/// 主页
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: const Center(child: Text('欢迎使用我的应用!')),
);
}
}关键点
Navigator.pushReplacement会替换当前页面,而不是压入新页面,用户无法按返回键回到启动页- 跳转前检查
mounted,防止页面已销毁后调用Navigator导致报错 backgroundColor: Colors.white要和原生启动页的背景色一致,这是实现「无缝过渡」的关键
添加启动动画
在简单版本的基础上,我们可以加上渐入和缩放动画,让 Logo 的出现更有动感:
class _SplashPageState extends State<SplashPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeIn;
late Animation<double> _scale;
@override
void initState() {
super.initState();
// 1. 创建动画控制器,总时长 2 秒
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
// 2. 定义渐入动画:透明度从 0 渐变到 1
_fadeIn = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeIn),
);
// 3. 定义缩放动画:从 0.5 倍放大到 1.0 倍
_scale = Tween<double>(begin: 0.5, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOutBack),
);
// 4. 启动动画
_controller.forward();
// 5. 动画结束后跳转到主页
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 等待 500ms 让用户看到完整动画,再跳转
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 500),
pageBuilder: (_, __, ___) => const HomePage(),
// 跳转时用渐隐过渡,比默认的滑动更自然
transitionsBuilder: (_, animation, __, child) {
return FadeTransition(opacity: animation, child: child);
},
),
);
}
});
}
});
}
@override
void dispose() {
_controller.dispose(); // 释放动画资源
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
// AnimatedBuilder:动画每帧都会重新调用 builder
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
// 同时应用渐入和缩放效果
return Opacity(
opacity: _fadeIn.value,
child: Transform.scale(
scale: _scale.value,
child: child,
),
);
},
// child 参数不会随动画重建,性能更好
child: const Column(
mainAxisSize: MainAxisSize.min,
children: [
FlutterLogo(size: 120),
SizedBox(height: 24),
Text('我的应用',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 48),
SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
),
),
);
}
}动画代码的执行流程如下:
initState()
├── 创建 AnimationController(2秒)
├── 定义 _fadeIn 动画(0.0 → 1.0)
├── 定义 _scale 动画(0.5 → 1.0)
├── _controller.forward() ← 启动动画
└── addStatusListener ← 监听动画结束
│
▼ 动画播放中(每帧调用 builder)
Opacity + Transform.scale 不断更新
│
▼ 动画完成(AnimationStatus.completed)
等待 500ms → Navigator.pushReplacement → 进入主页暗色模式适配
如果应用支持暗色模式,启动页也需要相应适配,否则在深色系统主题下会出现白色闪光,非常刺眼。
原生启动页的暗色适配
使用 flutter_native_splash 插件时,在 pubspec.yaml 中添加 color_dark 和 image_dark 配置:
flutter_native_splash:
color: "#FFFFFF" # 亮色背景
color_dark: "#121212" # 暗色背景
image: assets/images/splash_light.png # 亮色模式图片
image_dark: assets/images/splash_dark.png # 暗色模式图片
android_12:
image: assets/images/splash_12_light.png
color: "#FFFFFF"
image_dark: assets/images/splash_12_dark.png
color_dark: "#121212"
ios: true
android: true
web: true然后重新生成启动页资源:
dart run flutter_native_splash:create插件会自动生成亮色和暗色两套资源,系统会根据当前主题自动选择。
鸿蒙启动页的暗色适配
鸿蒙平台不支持 flutter_native_splash,需要手动为亮色和暗色分别配置资源。鸿蒙使用资源限定符目录来区分不同模式的资源:
1. 准备两套启动页图标
ohos/entry/src/main/resources/
├── base/ ← 默认(亮色)
│ ├── media/
│ │ └── splash_image.png ← 亮色启动页图标
│ └── element/
│ └── color.json ← 亮色颜色定义
└── dark/ ← 暗色模式
├── media/
│ └── splash_image.png ← 暗色启动页图标
└── element/
└── color.json ← 暗色颜色定义2. 定义亮色和暗色背景色
亮色 — base/element/color.json:
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}暗色 — dark/element/color.json:
{
"color": [
{
"name": "start_window_background",
"value": "#121212"
}
]
}3. 配置 module.json5 引用资源
module.json5 中的配置不需要改动,$color:start_window_background 和 $media:splash_image 会根据系统主题自动从 base/ 或 dark/ 目录中加载对应的资源:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"startWindowIcon": "$media:splash_image",
"startWindowBackground": "$color:start_window_background"
}
]
}
}TIP
鸿蒙的资源限定符机制和 Android 类似——系统会根据当前主题自动匹配 dark/ 目录下的资源,无需在代码中手动判断。
Flutter 自定义启动页的暗色适配
Flutter 侧的启动页需要根据系统主题切换背景色和图片。最简单的方式是通过 Theme.of(context) 获取当前主题:
class _SplashPageState extends State<SplashPage> {
@override
Widget build(BuildContext context) {
// 根据当前主题判断是否为暗色模式
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
// 背景色跟随主题
backgroundColor: isDark ? const Color(0xFF121212) : Colors.white,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 根据主题选择不同图片
Image.asset(
isDark ? 'assets/images/splash_dark.png'
: 'assets/images/splash_light.png',
width: 120,
),
const SizedBox(height: 48),
CircularProgressIndicator(
// 进度圈颜色也跟随主题
color: isDark ? Colors.white70 : Colors.black45,
),
],
),
),
);
}
}更好的做法
如果你已经在 MaterialApp 的 theme 和 darkTheme 中配置了 scaffoldBackgroundColor,可以直接使用 Theme.of(context).scaffoldBackgroundColor 作为启动页背景色,无需手动判断。这样启动页和整个应用的背景色自动保持一致。
最佳实践
| 实践 | 说明 |
|---|---|
| 原生启动页保持简洁 | 只展示 Logo 和品牌色,避免复杂内容 |
| 两层启动页背景色一致 | 原生启动页和 Flutter 启动页使用相同的背景色,避免闪烁 |
| 控制启动页时长 | 建议不超过 3 秒,避免用户等待 |
| 适配暗色模式 | 亮色和暗色都要配置,避免深色系统下白屏刺眼 |
| 避免广告式启动页 | iOS 审核可能拒绝包含广告的启动页 |
| 启动页与首页风格一致 | 确保从启动页到首页的过渡自然流畅 |
