【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 界面的背景颜色,不过这些都可以修改。

应用启动方式

应用有三种启动状态:冷启动、温启动和热启动。

冷启动

冷启动是指应用进程完全不存在,系统需要从零开始创建整个应用环境。

典型场景

  • 第一次打开应用
  • 系统杀掉了后台进程(比如内存不足)
  • 用户强制关闭了应用

冷启动应用将经历两个阶段:

第一阶段

  1. 加载并启动应用

    • 当用户点击应用图标(或某个 Intent 启动入口)时, 系统(ActivityManagerService)会开始加载应用。
    • 系统根据应用的 AndroidManifest.xml 查找入口 Activity。
    • 系统准备启动该应用对应的进程环境。
  2. 显示空白启动窗口

    • 在启动 Activity 之前,为了防止用户看到黑屏或空白延迟,系统会立即显示一个"starting window"(启动窗口)。

    • 这个窗口的外观取决于你的应用主题的 windowBackground 属性。

    • 如果没有定义,它通常是默认的纯白色背景(这就是经常看到"白屏"的原因)。

    • 从 Android 12 开始,这个阶段由系统 SplashScreen 机制接管:会显示应用图标和背景,而不是纯白。

  3. 创建应用进程

    • 从这一刻起,责任开始从"系统"转移到"应用自身"。

第二阶段

系统一旦创建了应用进程,应用进程就要负责做以下的任务

  1. 创建 Application 对象
  2. 启动主线程
  3. 创建主 Activity
  4. 填充视图
  5. 布局计算
  6. 执行初次绘制

温启动

应用进程存在,但 Activity 栈被完全清理,需要重新创建主 Activity。温启动会执行冷启动中的部分操作,但比冷启动轻一些,比热启动重一些。

热启动

应用进程已经在后台运行,用户重新打开应用。

启动画面的元素

图上四个区域分别是:

  1. 中央显示的图标:必须是矢量可绘制对象。可以指定一个静态图标或动画图标,动画持续时间理论上没有限制,但官方建议不要超过 1 秒,启动画面的目的只是"平滑过渡",而不是做长时间动画。如果没有显示指定图标,默认使用应用启动器图标。
  2. 图标背景:可选项,用于在标与启动页背景颜色之间增加视觉对比度。如果你的图标颜色太浅或太透明,比如白色图标配白底,就可以使用图标背景。如果图标本身是 Adaptive Icon(自适应图标),系统会自动判断是否显示它的背景层。
  3. 遮罩区域:启动画面中央的图标(前景层)会被按自适应图标规范进行裁剪(mask)。裁剪比例和自适应图标一样图标前景的边界约占总直径的 2/3(即留 1/3 的安全边距)。图标设计时不要画满整个圆,否则会被裁切。这样做的目的是保持视觉一致性,不同 App 的启动图标在 SplashScreen 中尺寸一致,不会显得不协调。
  4. 窗口背景 :启动画面的背景是一个纯色 。不支持渐变或图片填充。如果没有明确指定颜色,系统会使用主题中设置的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

下面详细看一下它的属性:

  1. android:windowSplashScreenBackground

    启动画面背景颜色(必须是单一不透明颜色 )。当系统决定使用 SplashScreen 时,会把该颜色填充为背景。如果未设置该属性,系统会回退去使用 android:windowBackground(但仅当它是纯色时才会被用到)。

  2. android:windowSplashScreenAnimatedIcon

    指定启动页中间显示的图标,可以是静态矢量或 AnimatedVectorDrawable(推荐使用矢量)。如果未指定,系统会尝试使用应用的 launcher icon(前提:图标为可识别的 adaptive/vector)。官方建议动画时长 不要超过 1000 ms

  3. android:windowSplashScreenAnimationDuration

    表示图标动画的时长(毫秒)。

    注意 :这个属性不控制 SplashScreen 在屏幕上保持的总时长 ,它只是便于你在自定义退出动画时读取该时长(通过 API 获取 SplashScreenView.getIconAnimationDuration()),以便做同步动画。它不能让 Splash 显示更久。想延长停留时间应使用 SplashScreen.setKeepOnScreenCondition(...)OnPreDrawListener

  4. android:windowSplashScreenIconBackgroundColor

    图标后面的圆背景色(增强图标与背景的对比)。当图标颜色与窗口背景对比不足时很有用。如果使用的是 adaptive icon(自适应图标),系统会根据对比度决定是否显示背景,也可以明确提供一个颜色。

  5. android:windowSplashScreenBrandingImage

    在启动页底部显示一个品牌图片(logo、slogan 等)。官方不推荐频繁使用,因会增加视觉噪音并可能导致布局兼容问题。如果使用,保持图片简单、轻量,避免在不同屏幕比例下被截断。

  6. android:windowSplashScreenBehavior(Android 13+)

    指定是否始终显示图标或遵循系统/Activity 提示的显示策略。

    默认行为:如果启动 Activity 指定了 SPLASH_SCREEN_STYLE_ICON 风格,显示图标;否则遵循系统默认行为(系统可能在某些场景下不显示空白图标)。

    icon_preferred优先显示图标,即便系统默认不会显示空白图标时也强制显示动画图标,避免出现空白启动页。

    如果不想出现空白(没有图标)的启动页,且总是希望看到 app 图标,可以把 windowSplashScreenBehavior 设置为 icon_preferred

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

    }
}
相关推荐
Java天梯之路2 小时前
09 Java 异常处理
java·后端
玖剹2 小时前
多线程编程:从日志到单例模式全解析
java·linux·c语言·c++·ubuntu·单例模式·策略模式
一 乐2 小时前
社区养老保障|智慧养老|基于springboot+小程序社区养老保障系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·小程序
全栈派森2 小时前
初见 Dart:这门新语言如何让你的 App「动」起来?
android·flutter·ios
q***98522 小时前
图文详述:MySQL的下载、安装、配置、使用
android·mysql·adb
随机昵称_1234562 小时前
Linux如何从docker hub下载arm镜像
java·linux·arm开发·docker
恋猫de小郭2 小时前
Dart 3.10 发布,快来看有什么更新吧
android·前端·flutter
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于JavaWeb医院住院信息管理系统的设计与实现为例,包含答辩的问题和答案
java·eclipse
q***47182 小时前
解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域
java·前端·spring