Meet the Jetpack Splashscreen API: a definitive guide for splash screens in Android

Splash screens, also called launch screens, are the screens shown by the app as it loads, during the app's startup. They exist to provide a momentary brand exposure or improve the user experience by showing a preview of the app as it starts, which can reduce the user's perceived load time.

On many projects I've worked on, in addition to showing the splash screen, we had to animate the transition between splash and first app screen. Depending on the animation type and how we're showing the first app views, we can have jank or a perceivable delay between the animations. So, we have to choose the right approach accordingly to the use case.

In this article you'll see how animated splash screens are created in Android and how this should be done now that we have Jetpack's Core SplashScreen, which backports Android 12 SplashScreen API back to Android Marshmallow (API 23).

Suppose we have to implement the following branded splash screen for an Android app:

Note that we start with a brand image, fade it out and then begin a slide down transition in the start screen. This use case will let us exercise multiple techniques in splash screen display, despite having simple transitions in the design. The animations must run sequentially and a long delay or jank between them is not acceptable.

Simple things first

Let's start simple: showing what happens in Android when you don't set up a splash screen. After creating an 'Empty Activity Project' in Android Studio, the main activity layout was adjusted to have the same design as the start screen reference. Nothing was made regarding the splash screen.
This is how it's displayed up to API level 30:

As we didn't add nothing to set up a splash screen yet, a black screen is shown while the main activity is loading.

Important: On an Android 12, a static branded splash screen was automatically displayed, using a centered app icon without any effort at all. Nice improvement!

To repeat this step, clone the following branch in Github's repo: github.com/tgloureiro/animated_splashscreen_android/tree/step1_no_splash

Showing a splash screen (Old way)

The traditional way of defining a splash screen in Android, is setting a window background to the the activity being launched. As the content is not attached to it yet, the window background is fully visible while the activity is loading. After it loads, content view is set inside the onCreate() method and it's views starts being rendered.

Therefore, to show the splash screen we have to set up the window background in the theme being used by the activity:

/res/values/themes.xml
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.AnimatedSplashScreen" 
        parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <!-- Drawable to be rendered in window's background 
             while the activity is being loaded -->
        <item name="android:windowBackground">
            @drawable/your_splashscreen_drawable
        </item>
    </style>
</resources>

If we need the splash screen to have a logo centered over some background drawable, we can use a LayerDrawable, that allows us to specify a list of drawables that is drawn in the order they're declared. In this example, we have a flat colored background with the logo centered in the screen. The use of android:gravity informs the platform to center the second drawable in the middle of the screen.

/res/drawable/splash_screen.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/splash_background"/>
    <item android:gravity="center" 
        android:drawable="@drawable/splash_logo" />
</layer-list>

For the splash background, we can use any type of drawable. For this example, we'll use a solid color. The color can be specified directly in the layer list, instead of pointing to another Drawable as we did here

/res/drawable/splash_screen.xml
<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@color/blue" />
</shape>

This is the old way of doing a splash screen and it's recommended up to API Level 22, the ones not covered by the new Core Splash Screen API.

Fading out the logo

There's an easy way of implementing the splash screen's fade out after activity loading: we can have the illusion of a logo fade out making a cross-fade between the splash screen and the screen background without the logo. We can do it using a TransitionDrawable. A transition drawable allows us to define two drawable layers and easily cross-fade between them using the startTransition(int) method.

/res/drawable/splash_transition.xml
<?xml version="1.0" encoding="utf-8"?>
<transition 
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/splash_screen" />
    <item android:drawable="@drawable/splash_background" />
</transition>

We have to perform the following tasks:

  1. Check if the splash screen was not displayed, otherwise we just set content view
  2. Get a reference to the window background and start the crossfade transition
  3. Launch a coroutine and after waiting for the animation duration, set the content view
MainActivity.kt
class MainActivity : AppCompatActivity() {
  ...
  override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      // Checks if the splash screen was displayed before
      // ->SavedInstanceState is not null after recreation!
      val splashWasDisplayed = savedInstanceState != null
      if(!splashWasDisplayed){
          // 1 - Start fading out the logo
          (window.decorView.background 
              as TransitionDrawable).startTransition(
                  logoCrossFadeDurationMillis
              )
          // 2 = As we can't register a listener to be 
          // notified when the transition drawable finishes,
          // launches a coroutine that blocks while animation
          // is being performed and sets the content view
          lifecycleScope.launch {
              // Time between the animations
              delay(logoCrossFadeDurationMillis.toLong()
                + spacingAfterFadeDurationMillis)
              window.decorView.background = 
                AppCompatResources.getDrawable(
                    this@MainActivity,
                    R.drawable.splash_background
                )
              setContentView(R.layout.activity_main)
          }
      }else{
          // Splash was shown before, no need to animate it.
          // 1 - Updates the window background (if needed)
          window.decorView.background = 
            AppCompatResources.getDrawable(
              this,R.drawable.splash_background
            )
          // 2 - Sets the content view instantly
          setContentView(R.layout.activity_main)
      }
  }
  ...

Check how it is rendered in a phone:

To repeat this step, clone the following branch in Github's repo: github.com/tgloureiro/animated_splashscreen_android/tree/step2_fade_out_splash

Showing the splash screen using Core SplashScreen API

The Core Splash Screen API provides a way of defining the elements we have in the Splash Screen. To setup a splash screen, you have to do the following steps:

1- Setup a style with Theme.SplashScreen as a parent, in themes.xml

themes.xml
    ...
    <style name="Theme.AppSplash" parent="Theme.SplashScreen">

    </style>

2- Setup windowSplashScreenBackground, windowSplashScreenAnimatedIcon as the background and the centered logo in the screen

themes.xml
    ...
    <style name="Theme.AppSplash" parent="Theme.SplashScreen">
        <item name="windowSplashScreenBackground">@drawable/your_background_drawable</item>
        <item name="windowSplashScreenAnimatedIcon">@drawable/your_icon_drawable</item>
    </style>

3- Setup the theme to be displayed in activity after the splash screen

themes.xml
    ...
    <style name="Theme.AppSplash" parent="Theme.SplashScreen">
        <item name="windowSplashScreenBackground">@drawable/your_background_drawable</item>
        <item name="windowSplashScreenAnimatedIcon">@drawable/your_icon_drawable</item>
        <item name="postSplashScreenTheme">@style/your_activity_theme</item>
    </style>

4- In build.gradle, change your compileSdkVersion to "android-S" (to be changed to apiLevel 31 with the release of Android 12) and include the library in dependencies.

build.gradle

...
android {
    compileSdkVersion "android-S"
    ...
}
...
dependencies {
    ...
    implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'
}

5- Finally, in the MainActivity (or the entry point for your app) you just have to call installSplashScreen() before setContentView()

MainActivity.kt
...
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        installSplashScreen()
        setContentView(R.layout.activity_main)
    }
}

Very straightforward, huh? With these simple steps you'll be showing your splash screen from API23 to Android12.

Core SplashScreen API + Animations

In Android 12, the logo itself can be animated using AnimationDrawable or AnimatedVector. The AnimationDrawable works similar to a classical animation which you have to represent
frame by frame and duration for each one. AnimatedVectorDrawable let's you describe your animation that will be interpolated by the framework.

The backported version of the Core Splash Screen API doesn't support animations on the logo yet. But we can implement a fadeout animation after splash's exhibition and I'll show how to implement it using this API.

We have to perform the following tasks:

  1. Check if the splash screen was not displayed, otherwise we manually set the theme and the content view
  2. Install the splash screen and listen for the ExitAnimation event
  3. Get a reference to the the logo, fade it out, remove the splash and set content view after it finishes
MainActivity.kt
class MainActivity : AppCompatActivity() {
    companion object{
        const val splashFadeDurationMillis = 300
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val splashWasDisplayed = savedInstanceState != null
        if(!splashWasDisplayed){
            val splashScreen = installSplashScreen()
            splashScreen.setOnExitAnimationListener { 
                splashScreenViewProvider ->
                // Get logo and start a fade out animation
                splashScreenViewProvider.iconView
                    .animate()
                    .setDuration(
                        splashFadeDurationMillis.toLong()
                    )
                    .alpha(0f)
                    .withEndAction {
                        // After the fade out, remove the 
                        // splash and set content view
                        splashScreenViewProvider.remove()
                        setContentView(R.layout.activity_main)
                    }.start()
            }
        }else{
            setTheme(R.style.Theme_AnimatedSplashScreen)
            setContentView(R.layout.activity_main)
        }
    }
}

Check how it is rendered in a phone:

To repeat this step, clone the following branch in Github's repo: github.com/tgloureiro/animated_splashscreen_android/tree/step3_core_splash_api_fade_out

Showing the splash screen using Core SplashScreen API with Jetpack Compose

The splash steps are pretty much the same, except we use Compose's setContent{} instead of setContentView(int). That's it.

The challenge here is to replace the start screen animation, originally made using MotionLayout, with one made using Compose. We can use an initially invisible AnimatedVisibility Composable and make it visible in the first pass using a LaunchedEffect. Making it visible will start the animation as described in the widget.

MainActivity.kt
fun StartScreen() {
    var visible by remember { mutableStateOf(false) }

    Scaffold(
        content = {
            Box(
                modifier = Modifier
                    .background(
                        colorResource(id = R.color.blue)
                    )
                    .fillMaxSize(),
            ) {
                AnimatedVisibility(
                    visible = visible,
                    enter = slideInVertically(
                        initialOffsetY = {
                            // Slide in from top
                            -it
                        },
                        animationSpec = tween(
                          durationMillis = 
                          MainActivity.splashFadeDurationMillis,
                        easing = LinearEasing
                    )
                    ),
                ) {
                    Column(
                        verticalArrangement = 
                            Arrangement.Center,
                        horizontalAlignment = 
                            Alignment.CenterHorizontally,
                        modifier = Modifier
                            .padding(
                                0.dp, 
                                0.dp, 
                                0.dp,
                                0.dp
                            )
                            .background(
                                colorResource(
                                    id = R.color.blue
                                )
                            )
                            .fillMaxSize()
                    ) {
                        Text(
                            stringResource(
                                id = R.string.start_screen_title
                            ),
                            fontSize = 36.sp,
                            modifier = Modifier.padding(
                                bottom = 
                                dimensionResource(
                                    R.dimen.start_content_title_margin_bottom
                                    )
                                ),
                            color = Color.White,
                            fontWeight = FontWeight.Bold
                        )
                        Box(
                            modifier = Modifier
                                .height(
                                    dimensionResource(
                                        R.dimen.start_content_size
                                        )
                                    )
                                .width(dimensionResource(
                                    R.dimen.start_content_size
                                    )
                                )
                                .clip(
                                    RoundedCornerShape(
                                        8.dp
                                    )
                                )
                                .background(color = Color.White)
                        )
                    }
                }
            }
            LaunchedEffect(true) {
                visible = true
            }
        }
    )
}

Check how it is rendered in a phone:

To repeat this step, clone the following branch in Github's repo: github.com/tgloureiro/animated_splashscreen_android/tree/step4_core_splash_jetpack

Final thoughts

The new Jetpack Core Splashscreen API provides a default way to create splash screens back to API23, with useful mechanisms we can use to know whether the loading ended and a way to continue showing the splash if we want to do additional work after starting the activity. The results are similar to what we had before, with the benefit of reduced boilerplate and compatibility with new Android versions.

21