【Android】给App添加启动画面——SplashScreen

【Android】给App添加启动画面——SplashScreen

【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

复制代码

这里定义了一个主题,应继承自 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

复制代码

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

android:name=".MainActivity"

android:exported="true"

android:theme="@style/Theme.APP.starting">

第三步:在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, "");

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

复制代码

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();

});

}

}

🎨 相关创意作品

微信第三方登录与静默授权
完美体育365

微信第三方登录与静默授权

📅 07-01 👁️ 7429
win10搜狗输入法图标如何隐藏?隐藏win10搜狗输入法图标的方法
凭什么是库克?
bst365app

凭什么是库克?

📅 08-18 👁️ 4902