一、项目介绍
1. 闪屏页的价值与意义
在移动应用中,闪屏页(Splash Screen)通常出现在 App 启动的最前端,用来完成以下几个核心作用:
-
品牌展示:利用短暂时间展示应用 Logo、品牌口号、主色调,提升用户对品牌的第一印象
-
预热加载:在闪屏阶段提前进行必要的初始化操作(如网络请求、数据库初始化、热更新检查等),使后续界面更流畅
-
过渡体验:避免冷启动时出现一闪而过的白屏或卡顿,优化用户感知启动速度
-
定制化动画:通过动效设计,让启动过程更具趣味性和专业感
从 Android 12 (API 31)开始,系统对闪屏做了统一的样式规范(包括居中图标、动态主题色背景、入场出场过渡等);但在兼容旧版本、满足定制化需求时,我们仍需手动实现更灵活的闪屏逻辑。
2. 本文目标
本文旨在从零到一,构建一个兼容 Android 5.0(API 21)~最新版本的全功能闪屏页,包含:
-
静态启动画面:通过
windowBackground
预绘制纯色、图片或渐变背景 -
自定义动画:在 Java/Kotlin 代码中加载 Lottie 动画或属性动画
-
延时与异步初始化:在闪屏期间执行网络/数据库初始化,并根据完成情况决定跳转时机
-
沉浸式全屏:隐藏状态栏、导航栏,支持深色/浅色模式
-
系统级 API 兼容:在 Android 12+ 使用系统 SplashScreen API,旧版本降级到自定义实现
-
可配置化接口:通过主题属性和常量控制闪屏时长、动画类型、资源路径
-
一键多渠道脚本:集成打包脚本,可生成带渠道信息的闪屏包
文章最后还将给出CI/CD 集成、常见问答(FAQ)及拓展思路,助你快速掌握并应用到生产环境。
二、相关知识
在动手编码前,需要掌握或了解以下核心概念与技术点:
-
Android 启动流程
-
Cold Start 冷启动 vs Warm Start 热启动
-
Application
→Activity#onCreate
→Window
→setContentView
→View
绘制顺序
-
-
启动主题(windowBackground)
-
在主题中配置
android:windowBackground
,让系统在布局加载前即绘制静态背景 -
避免应用首屏白屏或延迟加载
-
-
系统级 SplashScreen API(Android 12+)
-
SplashScreen.installSplashScreen()
:系统在启动时自动显示预设元素 -
setKeepOnScreenCondition { }
:根据初始化完成情况决定何时隐藏
-
-
ViewBinding 和 DataBinding
-
简化 UI 代码绑定
-
DataBinding 可用于布局中直接绑定动画时长、图片资源等
-
-
动画方案
-
属性动画(
ObjectAnimator
、AnimatorSet
) -
Lottie(JSON 动画)
-
MotionLayout(基于 ConstraintSet 的进场、出场动画)
-
-
沉浸式全屏
-
旧版:
systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN…
-
新版(Android R+):
WindowCompat.setDecorFitsSystemWindows
+WindowInsetsControllerCompat
-
-
异步初始化与线程管理
-
Kotlin Coroutine / RxJava
-
在闪屏期间启动网络请求、数据库预热,并绑定生命周期
-
-
版本兼容与主题切换
-
DayNight 主题支持深色/浅色
-
根据系统版本动态选择实现分支
-
三、实现思路
-
启动静态背景
-
在
styles.xml
中为闪屏 Activity 定义专属主题,设置windowBackground
为渐变或静态图片
-
-
Activity 加载
-
在
SplashActivity#onCreate
中:-
调用系统 API(Android 12+)或自定义全屏逻辑
-
绑定布局(Lottie / ImageView / TextView)
-
启动动画
-
启动初始化任务,并根据完成条件决定何时过渡到主界面
-
-
-
过渡与跳转
-
监听动画结束或初始化完成后:
-
调用
startActivity(MainActivity)
-
使用
overridePendingTransition
或 MotionLayout 实现淡入淡出 -
调用
finish()
释放资源
-
-
-
可配置化接口
-
通过主题属性(
<item name="splashDuration">3000</item>
)或常量控制时长、是否启用动画
-
-
多渠道打包与自动生成
-
可选:在打包脚本中注入渠道参数,用于区分在闪屏时根据渠道展示不同 Logo 或文案
-
-
CI/CD 集成
-
Jenkins / GitLab CI 调用
./gradlew assemble…
并采集闪屏包产物
-
四、环境与依赖
// 文件: build.gradle (项目根目录)
buildscript {
ext.kotlin_version = "1.7.20"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.2.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
// 文件: app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.splashdemo"
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0"
// 默认闪屏时长(毫秒)
resValue "integer", "splash_duration", "3000"
}
signingConfigs {
release {
storeFile file("../keystore/myapp.jks")
storePassword "your_keystore_password"
keyAlias "your_alias"
keyPassword "your_key_password"
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG"
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
buildFeatures {
viewBinding true
}
// 兼容 Java 1.8 特性
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
// 自动重命名产物,区分版本
applicationVariants.all { variant ->
if (variant.buildType.name == "release") {
variant.outputs.each { output ->
def time = new Date().format("yyyyMMdd_HHmm")
def name = "splashdemo-${variant.versionName}-${time}.apk"
outputFileName = name
}
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// Lottie 动画
implementation 'com.airbnb.android:lottie:6.1.0'
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.core:core-splashscreen:1.0.0' // AndroidX SplashScreen 支持库
}
五、整合代码
// =======================================================
// 文件: app/src/main/AndroidManifest.xml
// =======================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.splashdemo">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.SplashDemo">
<!-- Splash Activity -->
<activity
android:name=".SplashActivity"
android:exported="true"
android:theme="@style/Theme.SplashDemo.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Main Activity -->
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.SplashDemo.Main"/>
</application>
</manifest>
// =======================================================
// 文件: app/src/main/res/values/styles.xml
// =======================================================
<resources>
<!-- 应用主主题 -->
<style name="Theme.SplashDemo" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/black</item>
<item name="android:windowLightStatusBar">false</item>
</style>
<!-- Splash 专属主题 -->
<style name="Theme.SplashDemo.Splash" parent="Theme.SplashDemo">
<!-- 全屏沉浸 -->
<item name="android:windowFullscreen">true</item>
<!-- 禁用 ActionBar -->
<item name="android:windowActionBar">false</item>
<!-- 启动背景:渐变或静态图片 -->
<item name="android:windowBackground">@drawable/splash_window_bg</item>
</style>
<!-- Main 界面主题 -->
<style name="Theme.SplashDemo.Main" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- 保持沉浸式,根据需求可调整 -->
<item name="android:windowFullscreen">false</item>
</style>
</resources>
// =======================================================
// 文件: app/src/main/res/drawable/splash_window_bg.xml
// =======================================================
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 渐变色背景 -->
<item>
<shape android:shape="rectangle">
<gradient
android:startColor="@color/purple_500"
android:endColor="@color/purple_700"
android:angle="90"/>
</shape>
</item>
<!-- 居中 Logo -->
<item>
<bitmap
android:gravity="center"
android:src="@drawable/ic_splash_logo"/>
</item>
</layer-list>
// =======================================================
// 文件: app/src/main/res/layout/activity_splash.xml
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/splashContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
<!-- Lottie 动画容器 -->
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieView"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_autoPlay="false"
app:lottie_loop="false"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:lottie_rawRes="@raw/splash_animation"/>
</androidx.constraintlayout.widget.ConstraintLayout>
// =======================================================
// 文件: app/src/main/res/layout/activity_main.xml
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mainContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<TextView
android:id="@+id/tvWelcome"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="欢迎进入主界面"
android:textSize="24sp"
android:textColor="@color/black"
android:layout_gravity="center"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
// =======================================================
// 文件: app/src/main/java/com/example/splashdemo/MyApplication.kt
// =======================================================
package com.example.splashdemo
import android.app.Application
import android.util.Log
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Log.i("MyApplication", "应用启动,初始化全局资源")
// 可在此初始化第三方 SDK(Analytics、Crash、Push 等)
}
}
// =======================================================
// 文件: app/src/main/java/com/example/splashdemo/SplashActivity.kt
// =======================================================
package com.example.splashdemo
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen
import androidx.core.splashscreen.SplashScreen.KeepOnScreenCondition
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.airbnb.lottie.LottieAnimationView
import kotlinx.coroutines.*
class SplashActivity : AppCompatActivity() {
// 配置:闪屏最大时长(毫秒),从 resources 获取
private val splashDuration: Long
get() = resources.getInteger(R.integer.splash_duration).toLong()
private var splashJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
// ===== Android 12+ 系统级 SplashScreen API =====
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installSplashScreen().apply {
// 在条件满足前一直保持闪屏
setKeepOnScreenCondition(KeepOnScreenCondition { !initFinished })
}
}
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
// ===== 关闭 ActionBar,沉浸式全屏 =====
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
// ===== 播放 Lottie 动画 =====
val animationView = findViewById<LottieAnimationView>(R.id.lottieView)
animationView.setAnimation(R.raw.splash_animation)
animationView.playAnimation()
// ===== 启动异步初始化与定时跳转 =====
splashJob = CoroutineScope(Dispatchers.Main).launch {
// 并行:应用初始化 + 最小显示时长
val initDeferred = async { performInitialization() }
delay(splashDuration)
initDeferred.await()
// 跳转主界面
goToMain()
}
}
// 标志初始化是否完成(系统 API 用)
private var initFinished = false
/** 执行应用启动时的初始化操作 */
private suspend fun performInitialization() = withContext(Dispatchers.IO) {
// 模拟耗时任务:网络请求、数据库启动、SDK 初始化等
delay(1000)
// TODO: 在这里初始化你的 SDK,如 Analytics.init(), Push.init() 等
initFinished = true
}
/** 跳转主界面并结束当前 Activity */
private fun goToMain() {
startActivity(Intent(this, MainActivity::class.java))
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
finish()
}
override fun onDestroy() {
super.onDestroy()
splashJob?.cancel()
}
}
// =======================================================
// 文件: app/src/main/java/com/example/splashdemo/MainActivity.kt
// =======================================================
package com.example.splashdemo
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.splashdemo.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvWelcome.text = "Hello, 欢迎使用应用!"
}
}
// =======================================================
// 文件: app/src/main/res/values/integers.xml
// =======================================================
<resources>
<!-- 控制闪屏最小显示时长 -->
<integer name="splash_duration">3000</integer>
</resources>
// =======================================================
// 文件: app/src/main/res/raw/splash_animation.json
// =======================================================
/* Lottie JSON 内容,此处省略,请放入你自己的动画文件 */
// =======================================================
// 文件: build_all_flavors.sh (可选一键打包脚本)
// =======================================================
#!/bin/bash
# 一键清理并打包 Debug/Release 各种 Variant,并移动到 output/ 目录
OUTPUT_DIR="./output_apks"
rm -rf $OUTPUT_DIR && mkdir -p $OUTPUT_DIR
# 枚举所有 assemble 任务
TASKS=(
"assembleDebug"
"assembleRelease"
)
for task in "${TASKS[@]}"; do
echo ">>> Running Gradle Task: $task"
./gradlew clean $task
# 收集生成的 APK
find ./app/build/outputs/apk -type f -name "*.apk" | while read apk; do
cp "$apk" "$OUTPUT_DIR/"
echo " → Copied: $apk"
done
done
echo "All APKs are in $OUTPUT_DIR"
六、代码解读
-
AndroidManifest.xml
-
将
SplashActivity
标记为LAUNCHER
,并指定专用主题Theme.SplashDemo.Splash
; -
MainActivity
使用普通主题。
-
-
styles.xml
-
Theme.SplashDemo.Splash
继承自无 ActionBar 的 DayNight 主题,并设置windowBackground
为splash_window_bg
,实现静态启动背景; -
windowFullscreen
+windowActionBar=false
实现沉浸式全屏;
-
-
splash_window_bg.xml
-
使用
layer-list
叠加一个渐变背景与一个居中的 Logo 位图;
-
-
activity_splash.xml
-
仅包含一个
LottieAnimationView
,用于播放动态动画;
-
-
SplashActivity
-
系统 API:在 Android 12+ 调用
installSplashScreen()
并通过setKeepOnScreenCondition
保持闪屏直到初始化完成; -
沉浸式:通过
systemUiVisibility
Flags 隐藏状态栏与导航栏; -
异步初始化:使用 Kotlin Coroutine 同时执行耗时初始化与固定延时,二者完成后跳转;
-
Lottie 动画:加载本地 JSON 动画文件,并开始播放;
-
过渡动画:使用
overridePendingTransition
创建淡入淡出效果;
-
-
MainActivity
-
使用 ViewBinding 显示主界面欢迎文字,验证闪屏跳转是否成功;
-
-
一键打包脚本
-
可根据项目需要扩展为多渠道、多 ABI、CI/CD 集成;
-
七、项目总结与拓展
1. 总结
-
本文实现了一个兼容 Android 5.0+ 的闪屏页,集成了:
-
系统级 SplashScreen API(Android 12+)
-
自定义渐变背景 + Lottie 动画
-
Kotlin Coroutine 驱动的异步初始化 + 定时逻辑
-
沉浸式全屏适配
-
可配置化时长与资源
-
-
完整的工程结构与打包脚本,便于在本地或 CI/CD 上一键执行并收集 APK。
2. 拓展方向
-
MotionLayout 实现进阶动画
-
使用
MotionScene
和ConstraintSet
定义更复杂的进场、出场动效;
-
-
多渠道闪屏
-
在打包脚本中注入渠道参数,根据渠道展示不同的 Logo 文案或背景;
-
-
动态数据加载
-
在闪屏阶段动态获取服务器下发的启动图或广告,并在本地缓存后展示;
-
-
深色模式下切换
-
根据系统深浅色模式切换闪屏背景和动画配色;
-
-
热更检查
-
在
performInitialization()
中加入热更新框架检查,并在必要时提示用户更新;
-
八、FAQ
Q1:为何要在主题中设置 windowBackground
?
A1:系统在启动 Activity 之前,会先绘制主题里的 windowBackground
,这样能避免布局加载前的一闪而白,提高首屏体验。
Q2:如何兼容 Android 12 以下的系统级 Splash?
A2:通过引入 androidx.core:core-splashscreen
支持库,installSplashScreen()
在低版本自动降级为无侵入的实现。
Q3:闪屏时长如何灵活控制?
A3:建议使用资源文件(integers.xml
)或主题属性来定义时长,方便发布时调整;并通过 Coroutine/Handler 动态取消。
Q4:Lottie 动画播放失败怎么办?
A4:检查 JSON 文件是否完整、放置在 res/raw
;也可通过 LottieListener
监听加载错误并退化为静态图。
Q5:闪屏期间如何处理用户按返回键?
A5:默认 SplashActivity
是应用入口,一般不处理返回键;若需禁止用户退出,可在 onBackPressed()
中留空或拦截。