Android实现闪屏页(附带源码)

一、项目介绍

1. 闪屏页的价值与意义

在移动应用中,闪屏页(Splash Screen)通常出现在 App 启动的最前端,用来完成以下几个核心作用:

  • 品牌展示:利用短暂时间展示应用 Logo、品牌口号、主色调,提升用户对品牌的第一印象

  • 预热加载:在闪屏阶段提前进行必要的初始化操作(如网络请求、数据库初始化、热更新检查等),使后续界面更流畅

  • 过渡体验:避免冷启动时出现一闪而过的白屏或卡顿,优化用户感知启动速度

  • 定制化动画:通过动效设计,让启动过程更具趣味性和专业感

从 Android 12 (API 31)开始,系统对闪屏做了统一的样式规范(包括居中图标、动态主题色背景、入场出场过渡等);但在兼容旧版本、满足定制化需求时,我们仍需手动实现更灵活的闪屏逻辑。

2. 本文目标

本文旨在从零到一,构建一个兼容 Android 5.0(API 21)~最新版本的全功能闪屏页,包含:

  1. 静态启动画面:通过 windowBackground 预绘制纯色、图片或渐变背景

  2. 自定义动画:在 Java/Kotlin 代码中加载 Lottie 动画或属性动画

  3. 延时与异步初始化:在闪屏期间执行网络/数据库初始化,并根据完成情况决定跳转时机

  4. 沉浸式全屏:隐藏状态栏、导航栏,支持深色/浅色模式

  5. 系统级 API 兼容:在 Android 12+ 使用系统 SplashScreen API,旧版本降级到自定义实现

  6. 可配置化接口:通过主题属性和常量控制闪屏时长、动画类型、资源路径

  7. 一键多渠道脚本:集成打包脚本,可生成带渠道信息的闪屏包

文章最后还将给出CI/CD 集成常见问答(FAQ)拓展思路,助你快速掌握并应用到生产环境。


二、相关知识

在动手编码前,需要掌握或了解以下核心概念与技术点:

  1. Android 启动流程

    • Cold Start 冷启动 vs Warm Start 热启动

    • ApplicationActivity#onCreateWindowsetContentViewView 绘制顺序

  2. 启动主题(windowBackground)

    • 在主题中配置 android:windowBackground,让系统在布局加载前即绘制静态背景

    • 避免应用首屏白屏或延迟加载

  3. 系统级 SplashScreen API(Android 12+)

    • SplashScreen.installSplashScreen():系统在启动时自动显示预设元素

    • setKeepOnScreenCondition { }:根据初始化完成情况决定何时隐藏

  4. ViewBinding 和 DataBinding

    • 简化 UI 代码绑定

    • DataBinding 可用于布局中直接绑定动画时长、图片资源等

  5. 动画方案

    • 属性动画(ObjectAnimatorAnimatorSet

    • Lottie(JSON 动画)

    • MotionLayout(基于 ConstraintSet 的进场、出场动画)

  6. 沉浸式全屏

    • 旧版:systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN…

    • 新版(Android R+):WindowCompat.setDecorFitsSystemWindows + WindowInsetsControllerCompat

  7. 异步初始化与线程管理

    • Kotlin Coroutine / RxJava

    • 在闪屏期间启动网络请求、数据库预热,并绑定生命周期

  8. 版本兼容与主题切换

    • DayNight 主题支持深色/浅色

    • 根据系统版本动态选择实现分支


三、实现思路

  1. 启动静态背景

    • styles.xml 中为闪屏 Activity 定义专属主题,设置 windowBackground 为渐变或静态图片

  2. Activity 加载

    • SplashActivity#onCreate 中:

      • 调用系统 API(Android 12+)或自定义全屏逻辑

      • 绑定布局(Lottie / ImageView / TextView)

      • 启动动画

      • 启动初始化任务,并根据完成条件决定何时过渡到主界面

  3. 过渡与跳转

    • 监听动画结束或初始化完成后:

      • 调用 startActivity(MainActivity)

      • 使用 overridePendingTransition 或 MotionLayout 实现淡入淡出

      • 调用 finish() 释放资源

  4. 可配置化接口

    • 通过主题属性(<item name="splashDuration">3000</item>)或常量控制时长、是否启用动画

  5. 多渠道打包与自动生成

    • 可选:在打包脚本中注入渠道参数,用于区分在闪屏时根据渠道展示不同 Logo 或文案

  6. 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"

六、代码解读

  1. AndroidManifest.xml

    • SplashActivity 标记为 LAUNCHER,并指定专用主题 Theme.SplashDemo.Splash

    • MainActivity 使用普通主题。

  2. styles.xml

    • Theme.SplashDemo.Splash 继承自无 ActionBar 的 DayNight 主题,并设置 windowBackgroundsplash_window_bg,实现静态启动背景;

    • windowFullscreen + windowActionBar=false 实现沉浸式全屏;

  3. splash_window_bg.xml

    • 使用 layer-list 叠加一个渐变背景与一个居中的 Logo 位图;

  4. activity_splash.xml

    • 仅包含一个 LottieAnimationView,用于播放动态动画;

  5. SplashActivity

    • 系统 API:在 Android 12+ 调用 installSplashScreen() 并通过 setKeepOnScreenCondition 保持闪屏直到初始化完成;

    • 沉浸式:通过 systemUiVisibility Flags 隐藏状态栏与导航栏;

    • 异步初始化:使用 Kotlin Coroutine 同时执行耗时初始化与固定延时,二者完成后跳转;

    • Lottie 动画:加载本地 JSON 动画文件,并开始播放;

    • 过渡动画:使用 overridePendingTransition 创建淡入淡出效果;

  6. MainActivity

    • 使用 ViewBinding 显示主界面欢迎文字,验证闪屏跳转是否成功;

  7. 一键打包脚本

    • 可根据项目需要扩展为多渠道、多 ABI、CI/CD 集成;


七、项目总结与拓展

1. 总结

  • 本文实现了一个兼容 Android 5.0+ 的闪屏页,集成了:

    • 系统级 SplashScreen API(Android 12+)

    • 自定义渐变背景 + Lottie 动画

    • Kotlin Coroutine 驱动的异步初始化 + 定时逻辑

    • 沉浸式全屏适配

    • 可配置化时长与资源

  • 完整的工程结构与打包脚本,便于在本地或 CI/CD 上一键执行并收集 APK。

2. 拓展方向

  1. MotionLayout 实现进阶动画

    • 使用 MotionSceneConstraintSet 定义更复杂的进场、出场动效;

  2. 多渠道闪屏

    • 在打包脚本中注入渠道参数,根据渠道展示不同的 Logo 文案或背景;

  3. 动态数据加载

    • 在闪屏阶段动态获取服务器下发的启动图或广告,并在本地缓存后展示;

  4. 深色模式下切换

    • 根据系统深浅色模式切换闪屏背景和动画配色;

  5. 热更检查

    • 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() 中留空或拦截。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值