【Android】给App添加启动画面------SplashScreen
引言
当我们点击应用图标时,经常会看到一瞬间的白屏或黑屏,然后才进入应用界面。这种现象并不是 bug,在 Android 12 之前,这是系统在创建进程到 Activity 完成第一帧绘制之间临时绘制的一个默认窗口,这个窗口的背景就是我们在主题中设置的windowBackground,默认是白色或黑色。
从 Android 12开始,在所有应用的冷启动 和温启动 期间,系统会应用 Android 系统的默认启动画面。SplashScreen 在 Android 12 上是强制的,即使什么都不做,应用在 Android 12 上也会自动拥有 SplashScreen 界面。默认情况下,App 的 Launcher 图标会作为 SplashScreen 界面的中央图标,windowBackground属性指定的颜色会作为 SplashScreen 界面的背景颜色,不过这些都可以修改。
应用启动方式
应用有三种启动状态:冷启动、温启动和热启动。
冷启动
冷启动是指应用进程完全不存在,系统需要从零开始创建整个应用环境。
典型场景:
- 第一次打开应用
- 系统杀掉了后台进程(比如内存不足)
- 用户强制关闭了应用
冷启动应用将经历两个阶段:
第一阶段
加载并启动应用
- 当用户点击应用图标(或某个 Intent 启动入口)时, 系统(ActivityManagerService)会开始加载应用。
- 系统根据应用的
AndroidManifest.xml查找入口 Activity。- 系统准备启动该应用对应的进程环境。
显示空白启动窗口
在启动 Activity 之前,为了防止用户看到黑屏或空白延迟,系统会立即显示一个"starting window"(启动窗口)。
这个窗口的外观取决于你的应用主题的
windowBackground属性。如果没有定义,它通常是默认的纯白色背景(这就是经常看到"白屏"的原因)。
从 Android 12 开始,这个阶段由系统 SplashScreen 机制接管:会显示应用图标和背景,而不是纯白。
创建应用进程
- 从这一刻起,责任开始从"系统"转移到"应用自身"。
第二阶段
系统一旦创建了应用进程,应用进程就要负责做以下的任务
- 创建 Application 对象
- 启动主线程
- 创建主 Activity
- 填充视图
- 布局计算
- 执行初次绘制
温启动
应用进程存在,但 Activity 栈被完全清理,需要重新创建主 Activity。温启动会执行冷启动中的部分操作,但比冷启动轻一些,比热启动重一些。
热启动
应用进程已经在后台运行,用户重新打开应用。
启动画面的元素

图上四个区域分别是:
- 中央显示的图标:必须是矢量可绘制对象。可以指定一个静态图标或动画图标,动画持续时间理论上没有限制,但官方建议不要超过 1 秒,启动画面的目的只是"平滑过渡",而不是做长时间动画。如果没有显示指定图标,默认使用应用启动器图标。
- 图标背景:可选项,用于在标与启动页背景颜色之间增加视觉对比度。如果你的图标颜色太浅或太透明,比如白色图标配白底,就可以使用图标背景。如果图标本身是 Adaptive Icon(自适应图标),系统会自动判断是否显示它的背景层。
- 遮罩区域:启动画面中央的图标(前景层)会被按自适应图标规范进行裁剪(mask)。裁剪比例和自适应图标一样图标前景的边界约占总直径的 2/3(即留 1/3 的安全边距)。图标设计时不要画满整个圆,否则会被裁切。这样做的目的是保持视觉一致性,不同 App 的启动图标在 SplashScreen 中尺寸一致,不会显得不协调。
- 窗口背景 :启动画面的背景是一个纯色 。不支持渐变或图片填充。如果没有明确指定颜色,系统会使用主题中设置的
windowBackground作为默认背景颜色。
SplashScreen API 基本使用
环境配置
首先添加依赖:
java
dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1'
}
第一步:创建 SplashScreen 主题
xml
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<!-- 设置背景颜色 -->
<item name="windowSplashScreenBackground">@color/splash_background</item>
<!-- 设置中心图标 -->
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<!-- 设置图标动画时长(毫秒) -->
<item name="windowSplashScreenAnimationDuration">1000</item>
<!-- 设置图标背景(可选) -->
<item name="windowSplashScreenIconBackground">@color/splash_icon_background</item>
<!-- 设置底部品牌图(不推荐常用) -->
<item name="android:windowSplashScreenBrandingImage">@drawable/...</item>
<!-- 设置图标显示策略(Android 13+ 可控制是否强制显示图标) -->
<item name="android:windowSplashScreenBehavior">icon_preferred</item>
<!-- 设置SplashScreen结束后使用的主题 -->
<item name="postSplashScreenTheme">@style/Theme.App</item>
</style>
这里定义了一个主题,应继承自 Theme.SplashScreen。
下面详细看一下它的属性:
-
android:windowSplashScreenBackground启动画面背景颜色(必须是单一不透明颜色 )。当系统决定使用 SplashScreen 时,会把该颜色填充为背景。如果未设置该属性,系统会回退去使用
android:windowBackground(但仅当它是纯色时才会被用到)。 -
android:windowSplashScreenAnimatedIcon指定启动页中间显示的图标,可以是静态矢量或
AnimatedVectorDrawable(推荐使用矢量)。如果未指定,系统会尝试使用应用的 launcher icon(前提:图标为可识别的 adaptive/vector)。官方建议动画时长 不要超过 1000 ms。 -
android:windowSplashScreenAnimationDuration表示图标动画的时长(毫秒)。
注意 :这个属性不控制 SplashScreen 在屏幕上保持的总时长 ,它只是便于你在自定义退出动画时读取该时长(通过 API 获取
SplashScreenView.getIconAnimationDuration()),以便做同步动画。它不能让 Splash 显示更久。想延长停留时间应使用SplashScreen.setKeepOnScreenCondition(...)或OnPreDrawListener。 -
android:windowSplashScreenIconBackgroundColor图标后面的圆背景色(增强图标与背景的对比)。当图标颜色与窗口背景对比不足时很有用。如果使用的是 adaptive icon(自适应图标),系统会根据对比度决定是否显示背景,也可以明确提供一个颜色。
-
android:windowSplashScreenBrandingImage在启动页底部显示一个品牌图片(logo、slogan 等)。官方不推荐频繁使用,因会增加视觉噪音并可能导致布局兼容问题。如果使用,保持图片简单、轻量,避免在不同屏幕比例下被截断。
-
android:windowSplashScreenBehavior(Android 13+)指定是否始终显示图标或遵循系统/Activity 提示的显示策略。
默认行为:如果启动 Activity 指定了
SPLASH_SCREEN_STYLE_ICON风格,显示图标;否则遵循系统默认行为(系统可能在某些场景下不显示空白图标)。icon_preferred:优先显示图标,即便系统默认不会显示空白图标时也强制显示动画图标,避免出现空白启动页。如果不想出现空白(没有图标)的启动页,且总是希望看到 app 图标,可以把
windowSplashScreenBehavior设置为icon_preferred。 -
postSplashScreenTheme系统启动完 SplashScreen 后自动切换到的正式主题。
第二步:在 Manifest 中应用主题
xml
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.APP">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.APP.starting">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
第三步:在MainActivity中安装SplashScreen
java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}
}
installSplashScreen 源码
静态入口方法
java
@JvmStatic
@NotNull
public static final SplashScreen installSplashScreen(@NotNull Activity $this$installSplashScreen) {
return Companion.installSplashScreen($this$installSplashScreen);
}
这里是一个简单的委托,实际工作交给Companion对象。
Companion 对象实现
java
@JvmStatic
@NotNull
public final SplashScreen installSplashScreen(@NotNull Activity $this$installSplashScreen) {
Intrinsics.checkNotNullParameter($this$installSplashScreen, "<this>");
SplashScreen splashScreen = new SplashScreen($this$installSplashScreen, (DefaultConstructorMarker)null);
splashScreen.install();
return splashScreen;
}
这里创建了实际的实现对象。
install() 方法核心逻辑
java
public void install() {
TypedValue typedValue = new TypedValue();
Resources.Theme currentTheme = this.activity.getTheme();
// 解析背景属性
if (currentTheme.resolveAttribute(attr.windowSplashScreenBackground, typedValue, true)) {
this.backgroundResId = typedValue.resourceId;
this.backgroundColor = typedValue.data;
}
// 解析动画图标属性
if (currentTheme.resolveAttribute(attr.windowSplashScreenAnimatedIcon, typedValue, true)) {
this.icon = currentTheme.getDrawable(typedValue.resourceId);
}
// 解析图标尺寸属性
if (currentTheme.resolveAttribute(attr.splashScreenIconSize, typedValue, true)) {
this.hasBackground = typedValue.resourceId == dimen.splashscreen_icon_size_with_background;
}
Intrinsics.checkNotNullExpressionValue(currentTheme, "currentTheme");
// 设置SplashScreen后的主题
this.setPostSplashScreenTheme(currentTheme, typedValue);
}
设置后置主题(关键)
java
protected final void setPostSplashScreenTheme(@NotNull Resources.Theme currentTheme, @NotNull TypedValue typedValue) {
Intrinsics.checkNotNullParameter(currentTheme, "currentTheme");
Intrinsics.checkNotNullParameter(typedValue, "typedValue");
if (currentTheme.resolveAttribute(attr.postSplashScreenTheme, typedValue, true)) {
this.finalThemeId = typedValue.resourceId;
if (this.finalThemeId != 0) {
this.activity.setTheme(this.finalThemeId);
}
}
}
- 查找
postSplashScreenTheme属性- 如果找到且不为0,调用
activity.setTheme(this.finalThemeId)- 这是 SplashScreen 消失后应用使用的主题
注意 :installSplashScreen()方法最好在super.onCreate()之前调用,必须保证在setContentView()之前调用。
让启动画面在屏幕中显示更长时间
1. 使用 setKeepOnScreenCondition
java
splashScreen.setKeepOnScreenCondition(new BooleanSupplier() {
@Override
public boolean getAsBoolean() {
return keepSplashOnScreen;
}
});
- 当传入的条件返回
true时,启动画面会保持显示- 当条件返回
false时,启动画面会自动消失
2. 使用 ViewTreeObserver.OnPreDrawListener
java
final View content = findViewById(android.R.id.content);
content.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (mViewModel.isReady()) {
// 数据就绪,允许绘制并移除监听器
content.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
} else {
// 数据未就绪,取消本次绘制
return false;
}
}
});
- OnPreDrawListener 在View即将绘制前被调用
- 返回
false会取消当前绘制周期- 返回
true允许绘制正常进行- 系统会在下一个绘制周期再次尝试,直到返回
true
自定义用于关闭启画面的动画
java
// 获取当前SplashScreen并设置退出动画监听器
getSplashScreen().setOnExitAnimationListener(splashScreenView -> {
// 自定义动画
// ...
// 添加动画监听器,处理动画结束后的逻辑
slideUp.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 动画完成后必须移除SplashScreen视图,释放资源
splashScreenView.remove();
}
});
// 启动动画
slideUp.start();
});
示例
效果如下:

代码如下:
xml
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<!-- 设置背景颜色 -->
<item name="windowSplashScreenBackground">#2A94C7</item>
<!-- 设置中心图标 -->
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<!-- 设置图标背景 -->
<item name="android:windowSplashScreenIconBackgroundColor">#1DC3B3</item>
<!-- 设置SplashScreen结束后使用的主题 -->
<item name="postSplashScreenTheme">@style/Theme.SplashScreenTest</item>
</style>
java
public class MainActivity extends AppCompatActivity {
private boolean keepSplashOnScreen = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EdgeToEdge.enable(this);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
final View content = findViewById(android.R.id.content);
splashScreen.setKeepOnScreenCondition(() -> keepSplashOnScreen);
new Handler().postDelayed(() -> {
keepSplashOnScreen = false;
}, 1000);
splashScreen.setOnExitAnimationListener(splashScreenView -> {
View iconView = splashScreenView.getIconView();
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int translationDistance = (int) (-iconView.getRootView().getHeight() * 0.3f);
ObjectAnimator floatUp = ObjectAnimator.ofFloat(
iconView,
View.TRANSLATION_Y,
0f,
translationDistance
);
floatUp.setDuration(600);
floatUp.setInterpolator(new DecelerateInterpolator());
ObjectAnimator scaleDown = ObjectAnimator.ofFloat(iconView, View.SCALE_X, 1f, 0.5f);
ObjectAnimator scaleUp = ObjectAnimator.ofFloat(iconView, View.SCALE_Y, 1f, 0.5f);
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(iconView, View.ALPHA, 1f, 0f);
AnimatorSet phase2 = new AnimatorSet();
phase2.playTogether(scaleDown, scaleUp, fadeOut);
phase2.setDuration(400);
AnimatorSet fullAnimation = new AnimatorSet();
fullAnimation.playSequentially(floatUp, phase2);
fullAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
splashScreenView.remove();
}
});
fullAnimation.start();
});
}
}