Custom Splash Screen on Android – Why You’re Doing It Wrong

by | Oct 12, 2024

First, what even is a splash screen? It’s a screen that commonly displays some form of app branding, usually in the form of a logo, slogan, or the brand’s colors. The splash screen tends not to be very complicated because you want it to load fast. After all, the splash screen serves as the UI the user sees while the system loads up the app’s actual UI and any other resources your app requires.

I’ll show you the wrong approach and then walk you through implementing the official Android approach to handling a splash screen, which we’ve had access to for a few years. You’ll learn the downside to the custom solution, what you need to be aware of when choosing this approach related to the latest Android versions, and what you can achieve with the official Android splash screen API. I’ll also share some tips when your project requires a custom splash screen solution.

Take a look at the following splash screen animation that can be achieved using the official Android splash screen API:

GIF showcasing a splash screen with an animation

Wrong Approach – Custom Splash Screen Solution

If you ever had to implement a splash screen on Android, you might be familiar with creating a separate screen, like any other in your app. Whether this was a separate Activity, Fragment, or even Composable, it had a similar structure to this:

val navController = rememberNavController()

NavHost(
    navController = navController,
    startDestination = SplashRoute
) {
    composable<SplashRoute> {
        
        LaunchedEffect(Unit) {
            // Simulate some preparation needed before showing UI
            delay(3000)
            navController.navigate(HomeRoute)
        }
    
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(Color.Black),
            contentAlignment = Alignment.Center
        ) {
            Image(
                painter = painterResource(id = R.drawable.logo),
                contentDescription = "App Logo",
                modifier = Modifier.size(100.dp)
            )
        }

    }
    
    composable<HomeRoute> {
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = Color.White
        ) {
            Text(text = “Home")}  
        }
    }
}

You can read more in-depth about Jetpack Compose Type-Safe Navigation in one of our articles: How to Implement Official Type-Safe Navigation With Jetpack Compose & Custom Types

This approach is wrong because you could show your user two splash screens.

GIF showcasing a double splash screen

For apps that run on Android 12 or later, the system will, by default, show its own splash screen (which defaults to your app’s logo), which results in a double splash screen—not a great experience for your users.

The Android splash screen on Android 12 and above can’t be disabled.

Using the Official Android Splash Screen Solution

Now, let´s see how we can address this double splash screen issue by working with the Android system to customize the default splash screen it shows. Below, you can find the dependency that we need to include in our project:

[versions]
splashscreen = "1.0.1"

[libraries]
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref="splashscreen"}

After performing a Gradle sync, we can add the logo we want to display. To add one to your project, right-click your drawable folder and select New -> Vector Asset. Name it “logo,” once it’s been imported, you can find the new logo.xml file in your res/drawable folder.

Android has some sizing requirements for the logo to fit in the space allocated in the splash screen. If it is larger, it will be cropped, so take care to ensure your logo is not too large.

The logo.xml file consists of a collection of paths that form a vector. Each path consists of multiple points connected by a solid line and optionally filled with a color. After the import, you might not have the group tag in your file. Add this tag to group all the paths that make up your logo. Now, we can use the group tag to define the center point of our drawable. This is important because it will be where any transformations, like scaling and rotation, to the logo will be performed. By default, this point is in the far top left corner of the drawable, which is not usually what you’d want. To specify this point on our logo, we can use the pivotX and pivotY properties on our group tag that allow you to specify the X and Y coordinates of the center point of the logo.

The center point of the drawable is half of the viewPortHeight and viewPortWidth properties defined in the parent vector tag in the logo.xml file.

This information will be relevant when we want to animate this logo.

<vector 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="50dp"
    android:width="50dp" 
    android:viewportHeight="3500"
    android:viewportWidth="3500">
    <group
        android:name="animationGroup"
        android:pivotX="1750"
        android:pivotY="1750">
        <path android:fillColor="#00f15e"android:pathData="..." />
        <path android:fillColor="#00f15e"android:pathData="..." />
        <path android:fillColor="#00f15e"android:pathData="..." />
    </group>
</vector>

To achieve the logo animation that I’ve shown you at the beginning, we’ll need to create two new files:

  1. logo_animator.xml file – describes how we want to animate our logo
  2. animated_logo.xml – joins our logo and animator together

First, the animation file should be placed in the res/animator folder (you may need to create that folder first).

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:interpolator="@anim/overshoot_interpolator">

    <propertyValuesHolder
        android:propertyName="rotation"
        android:valueType="floatType"
        android:valueFrom="0.0"
        android:valueTo="360.0" />

    <propertyValuesHolder
        android:propertyName="scaleX"
        android:valueType="floatType"
        android:valueFrom="0.0"
        android:valueTo="0.4" />

    <propertyValuesHolder
        android:propertyName="scaleY"
        android:valueType="floatType"
        android:valueFrom="0.0"
        android:valueTo="0.4" />

</objectAnimator>

This file’s important parts are that it will perform a rotation (from 0 to 360 degrees) and change the X and Y coordinates to achieve the growth and shrink behavior we want. Each is defined using the propertyValuesHolder tag. The animation will complete within one second, as defined using duration=“1000”. We also specify an interpolator, which will make our animation appear a bit less rigid. Create and add the XML file named overshoot_interpolator.xml inside your res/anim package.

<overshootInterpolator xmlns:android="http://schemas.android.com/apk/res/android">

</overshootInterpolator>

Next, to associate the logo with this animator, we create another XML file, animated_logo.xml, inside res/drawable. In it, we will combine the animator with the logo so that we can use this file in our splash screen, and the animator will apply the animation we defined to our logo. The name attribute of the target tag points to the name property of the group tag in your logo.xml file, so make sure it matches.

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/logo">

    <target
        android:animation="@animator/logo_animator"
        android:name="animationGroup" />

</animated-vector>

The Android splash screen API does not require creating a separate Activity or Composable screen.

Next, let’s replace Android’s default screen. We can do this by defining our own theme inside the themes.xml file.

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="Theme.MyApp" parent="android:Theme.Material.Light.NoActionBar" />

    <style name="Theme.App.Starting" parent="Theme.SplashScreen">
        <item name="windowSplashScreenBackground">@color/black</item>
        <item name="postSplashScreenTheme">@style/Theme.MyApp</item>
        <item name="android:windowSplashScreenAnimatedIcon">@drawable/animated_logo</item>       
    </style>
</resources>

Your IDE may give you a warning if your minimum SDK is less than API 31. That is because the android:windowSplashScreenAnimatedIcon property is only supported from API 31 and above. You can use a resource qualifier to only apply this property for versions >= 31.

After defining our theme, we need to go to our Manifest file and set it to our application and main activity.

// Some attributes have been omitted
<application
    android:theme="@tyle/Theme.App.Starting">
    <activity
        android:name=".MainActivity"
        android:theme="@tyle/Theme.App.Starting">
        <intent-filter> 
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-flter>
    </activity>
</application>

Moving on from all this XML, let’s call installSplashScreen() in our MainActivity. This function comes from the splash screen dependency we added earlier on. Make sure to call it before setContent { }. If you had to run your app now, you’d see that the splash screen is missing an animation when the splash screen is removed. With Android’s splash screen API, we can create such an exit animation:

installSplashScreen().apply {
    setOnExitAnimationListener { screen ->
        val zoomX = ObjectAnimator.ofFloat(
            screen.iconView,
            View.SCALE_X,
            0.4f,
            0.0f
        )
        zoomX.interpolator = OvershootInterpolator()
        zoomX.duration = 500L
        zoomX.doOnEnd { screen.remove() }

        val zoomY = ObjectAnimator.ofFloat(
            screen.iconView,
            View.SCALE_Y,
            0.4f,
            0.0f
        )
        zoomY.interpolator = OvershootInterpolator()
        zoomY.duration = 500L
        zoomY.doOnEnd { screen.remove() }

        zoomX.start()
        zoomY.start()
    }
}

In the above snippet, we create an ObjectAnimator that will apply a scale animation to our logo to make it appear to shrink in size. We can access our logo by using the SplashScreenViewProvider that we get from the installSplashScreen function and pass it to our ObjectAnimator.

I go into more detail on the logo animation in his video: How to Build an Animated Splash Screen on Android – The Full Guide

Control For How Long The Splash Screen Is Visible For

Our code so far gives us a beautiful custom splash screen shown to the user while the Android system prepares to launch the app. But what if we need to perform some setup or configuration before showing our UI? In this case, the Android splash screen API has a function setKeepOnScreenCondition(), which takes a lambda that will get checked on every frame drawn. When the lambda returns true, the splash screen will be removed, and its exit animation will be executed. A great way to handle this state is by using a ViewModel.

class MainViewModel: ViewModel() {

    /**
     * Using a StateFlow here is also a good option
     * 
     *     private val _isReady = MutableStateFlow(false)
     *     val isReady = _isReady.asStateFlow()
     */
    var isReady by mutableStateOf(false)
        private set

    init {
        viewModelScope.launch {
            delay(3000L)
            isReady = true
        }
    }
}

Now we need to create an instance of our MainViewModel and pass our isReady boolean to the setKeepOnScreenCondition lambda as seen below:

//  Some code has been omitted to highlight the important pieces
class MainActivity : ComponentActivity() {

    private val viewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        installSplashScreen().apply {
            setKeepOnScreenCondition {
                !viewModel.isReady
            }
         }

         setContent { /* ... */ }
    }
}

Splash screens should never be longer than necessary. A user did not open your app to look at your company’s logo, so don’t delay dismissing your splash screen artificially.

Limitations You Should Know About

The official Android splash screen API comes with these limitations:

  1. It doesn’t allow you to draw outside of the cropped logo circle.
  2. It doesn’t allow complex animations that go beyond animating alpha & transform.

Think twice about whether a custom solution is worth the downsides of dealing with the system splash screen.

When a custom splash screen is necessary for your project, you’ll need to work alongside Android’s default splash screen on devices running Android 12 and later.

Your option is incorporating the system splash screen with your custom Composable splash screen. You’ll need to ensure the system splash screen smoothly transitions into your custom one without the user noticing. You could for example show the system splash screen with no logo and then start your more complex logo animation when the Composable splash screen is shown. This way, there will be a short time without seeing a logo, but then a more complex logo animation could start.

Conclusion

  1. A splash screen is a great way to load initial resources, like an auth token, to determine the initial screen of your app.
  2. Don’t artificially show a splash screen for longer than necessary.
  3. The Android splash screen API allows you to show a splash screen without defining a separate Activity, Fragment or Composable screen.
  4. The official Android splash screen API has limitations regarding the complexity of the animation and showing more besides a logo. In my opinion, no app needs more than that, but if you do, make sure your custom Composable splash screen integrates seamlessly with Google’s splash screen API.

The Essentials of Industry-Level Android App Development

Master Building Large-Scale Native Android Apps

Take this 23h online course to learn everything it takes to become an industry-ready native Android developer. Learn about multi-module architecture, Gradle, Jetpack Compose, authentication, offline-first development and more.

Read More