Skip to content

配置启动页(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 中添加:

yaml
dev_dependencies:
  flutter_native_splash: ^2.4.3

然后执行:

bash
flutter pub get

2. 配置启动页

pubspec.yaml 中添加 flutter_native_splash 配置段:

yaml
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: true

TIP

  • color 为必填项,指定启动页背景色
  • image 建议使用 1024 × 1024 像素的 PNG 图片,实际显示会被缩放
  • Android 12+ 的启动页样式与低版本不同,建议通过 android_12 单独配置

3. 生成启动页

bash
dart run flutter_native_splash:create

该命令会自动修改各平台的原生配置文件,生成对应的启动页资源。

4. 恢复默认

如果需要移除自定义启动页,恢复为默认白屏:

bash
dart run flutter_native_splash:remove

方式二:手动配置 Android 启动页

如果不使用插件,也可以手动配置。

低版本 Android(< 12)

编辑 android/app/src/main/res/drawable/launch_background.xml

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
<?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
<?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

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 中配置启动页:

json
{
  "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 中定义:
json
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    }
  ]
}

Web

使用 flutter_native_splash 插件

插件会自动修改 web/index.html,在页面中插入启动页样式和图片。

手动配置 Web 启动页

编辑 web/index.html,在 <body> 标签内添加启动页元素:

html
<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 函数中修改窗口背景色:

cpp
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,设置窗口背景色:

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 设置窗口背景色:

cpp
// 在 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 启动页上等待初始化完成,然后跳转到主页:

dart
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 的出现更有动感:

dart
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_darkimage_dark 配置:

yaml
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

然后重新生成启动页资源:

bash
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

json
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    }
  ]
}

暗色 — dark/element/color.json

json
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#121212"
    }
  ]
}

3. 配置 module.json5 引用资源

module.json5 中的配置不需要改动,$color:start_window_background$media:splash_image 会根据系统主题自动从 base/dark/ 目录中加载对应的资源:

json
{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "startWindowIcon": "$media:splash_image",
        "startWindowBackground": "$color:start_window_background"
      }
    ]
  }
}

TIP

鸿蒙的资源限定符机制和 Android 类似——系统会根据当前主题自动匹配 dark/ 目录下的资源,无需在代码中手动判断。

Flutter 自定义启动页的暗色适配

Flutter 侧的启动页需要根据系统主题切换背景色和图片。最简单的方式是通过 Theme.of(context) 获取当前主题:

dart
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,
            ),
          ],
        ),
      ),
    );
  }
}

更好的做法

如果你已经在 MaterialAppthemedarkTheme 中配置了 scaffoldBackgroundColor,可以直接使用 Theme.of(context).scaffoldBackgroundColor 作为启动页背景色,无需手动判断。这样启动页和整个应用的背景色自动保持一致。

最佳实践

实践说明
原生启动页保持简洁只展示 Logo 和品牌色,避免复杂内容
两层启动页背景色一致原生启动页和 Flutter 启动页使用相同的背景色,避免闪烁
控制启动页时长建议不超过 3 秒,避免用户等待
适配暗色模式亮色和暗色都要配置,避免深色系统下白屏刺眼
避免广告式启动页iOS 审核可能拒绝包含广告的启动页
启动页与首页风格一致确保从启动页到首页的过渡自然流畅

下一步

基于 Flutter 官方文档整理