6 min read, 1115 words, last updated: 2025/3/12
The Problem
Have you ever opened a mobile app only to see the launch screen flash by so quickly that it creates a jarring experience? This is a common issue that affects user perception of app quality. The launch screen appears for just a split second, creating an unpleasant flicker that makes the app feel unpolished.
The core issues are:
- Launch screens flash too quickly on fast devices
- Users experience visual discontinuity during app startup
- No apparent way to control the timing of system-managed launch screens
- Inconsistent behavior across different platforms and devices
Investigation
Let's examine what's happening during app launch and why this problem occurs.
iOS Launch Screen Behavior
On iOS, the Launch Screen is fundamentally controlled by the system:
- It's designed as a static image or Storyboard that displays while the app initializes
- Apple intentionally limits control over its display duration
- The system automatically determines how long to show it based on app loading time
- Duration depends on
AppDelegateandSceneDelegateinitialization time
Android Launch Screen Variations
Android has evolved its approach to launch screens:
- Android 12+: Official Splash Screen API with better control
- Earlier versions: Custom implementations using activities or themes
- System behavior: Similar to iOS, but with more flexibility for developers
Root Cause Analysis
The flash issue occurs when:
- App initialization is too fast: Modern devices load apps quickly, making launch screens appear for milliseconds
- Lack of minimum display time: No built-in mechanism to ensure adequate visibility
- Abrupt transitions: Sudden switches between launch screen and main interface
- Theme inconsistencies: Visual gaps between launch assets and app UI
Solution
Here are comprehensive solutions for both platforms:
iOS Implementation
Method 1: Custom Splash Screen with Transition Control
Instead of relying solely on the system Launch Screen, implement a controlled transition:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var splashViewController: UIViewController?
private let launchStartTime = Date()
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
// Load Launch Screen Storyboard
let storyboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
splashViewController = storyboard.instantiateInitialViewController()
// Set splash as root view controller
window?.rootViewController = splashViewController
window?.makeKeyAndVisible()
// Implement smart timing logic
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.evaluateAndTransition()
}
return true
}
private func evaluateAndTransition() {
let elapsedTime = Date().timeIntervalSince(launchStartTime)
if elapsedTime < 0.3 {
// Too fast - add minimum display time
let remainingTime = 0.5 - elapsedTime
DispatchQueue.main.asyncAfter(deadline: .now() + remainingTime) {
self.showMainScreen()
}
} else {
// Normal transition with animation
self.animateToMainScreen()
}
}
private func showMainScreen() {
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
let mainViewController = mainStoryboard.instantiateInitialViewController()
window?.rootViewController = mainViewController
}
private func animateToMainScreen() {
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
let mainViewController = mainStoryboard.instantiateInitialViewController()
UIView.transition(with: window!, duration: 0.3, options: .transitionCrossDissolve, animations: {
self.window?.rootViewController = mainViewController
}, completion: nil)
}
}Method 2: Scene-Based Implementation
For apps using SceneDelegate:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private let sceneStartTime = Date()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
// Show splash screen first
showSplashScreen()
// Evaluate transition timing
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.evaluateTransition()
}
}
private func showSplashScreen() {
let storyboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
let splashVC = storyboard.instantiateInitialViewController()
window?.rootViewController = splashVC
window?.makeKeyAndVisible()
}
private func evaluateTransition() {
let elapsedTime = Date().timeIntervalSince(sceneStartTime)
let minimumDisplayTime: TimeInterval = 0.5
if elapsedTime < minimumDisplayTime {
let delay = minimumDisplayTime - elapsedTime
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.transitionToMainScreen()
}
} else {
self.transitionToMainScreen()
}
}
private func transitionToMainScreen() {
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
let mainViewController = mainStoryboard.instantiateInitialViewController()
UIView.animate(withDuration: 0.3, animations: {
self.window?.rootViewController?.view.alpha = 0
}) { _ in
self.window?.rootViewController = mainViewController
UIView.animate(withDuration: 0.3) {
self.window?.rootViewController?.view.alpha = 1
}
}
}
}Android Implementation
Method 1: Official Splash Screen API (Android 12+)
<!-- res/values/themes.xml -->
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.MyApp.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/white</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash_icon</item>
<item name="windowSplashScreenAnimationDuration">500</item>
<item name="postSplashScreenTheme">@style/Theme.MyApp.Main</item>
</style>
<style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowBackground">@color/white</item>
</style>
</resources>import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
class MainActivity : ComponentActivity() {
private var isAppReady = false
private val startTime = System.currentTimeMillis()
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
// Control splash screen visibility
splashScreen.setKeepOnScreenCondition {
!isAppReady
}
super.onCreate(savedInstanceState)
// Initialize app with minimum display time
initializeApp()
setContentView(R.layout.activity_main)
}
private fun initializeApp() {
Thread {
val elapsedTime = System.currentTimeMillis() - startTime
val minimumDisplayTime = 500L
if (elapsedTime < minimumDisplayTime) {
// Ensure minimum display time
Thread.sleep(minimumDisplayTime - elapsedTime)
}
// Your app initialization logic here
runOnUiThread {
isAppReady = true
}
}.start()
}
}Method 2: Custom Splash Activity (Backward Compatibility)
class SplashActivity : AppCompatActivity() {
private val startTime = System.currentTimeMillis()
private val minimumDisplayTime = 500L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
evaluateTransition()
}
private fun evaluateTransition() {
val elapsedTime = System.currentTimeMillis() - startTime
if (elapsedTime < minimumDisplayTime) {
// Ensure minimum display time
val delay = minimumDisplayTime - elapsedTime
Handler(Looper.getMainLooper()).postDelayed({
navigateToMain()
}, delay)
} else {
// Navigate immediately if enough time has passed
navigateToMain()
}
}
private fun navigateToMain() {
startActivity(Intent(this, MainActivity::class.java))
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
finish()
}
}<!-- AndroidManifest.xml -->
<activity
android:name=".SplashActivity"
android:exported="true"
android:theme="@style/Theme.MyApp.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="false"
android:theme="@style/Theme.MyApp.Main" /><!-- res/values/themes.xml -->
<style name="Theme.MyApp.Splash" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowBackground">@drawable/splash_background</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>Lessons Learned
Prevention Tips
- Always implement minimum display times: Don't let splash screens flash by in under 300-500ms
- Use smooth transitions: Implement fade or cross-dissolve animations instead of abrupt switches
- Maintain visual consistency: Ensure splash screen assets match your app's initial UI state
- Test on various devices: Fast flagship devices and slower budget phones behave differently
Performance Considerations
- Don't artificially slow down your app: The goal is improving perceived performance, not actual performance
- Load critical resources during splash: Use the splash time productively for essential initialization
- Consider progressive loading: Show meaningful content as soon as possible rather than waiting for everything
Platform-Specific Best Practices
iOS:
- Leverage the system Launch Screen for initial display
- Add controlled transitions for better user experience
- Test across different iOS versions and devices
Android:
- Use the official Splash Screen API for modern Android versions
- Implement backward compatibility for older Android versions
- Consider using vector drawables for crisp splash screen graphics
User Experience Guidelines
- Keep it simple: Splash screens should be clean and focused
- Brand appropriately: Use splash screens to reinforce brand identity
- Provide feedback: Consider subtle
