Makuhari Development Corporation
6 min read, 1115 words, last updated: 2025/3/12
TwitterLinkedInFacebookEmail

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 AppDelegate and SceneDelegate initialization 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:

  1. App initialization is too fast: Modern devices load apps quickly, making launch screens appear for milliseconds
  2. Lack of minimum display time: No built-in mechanism to ensure adequate visibility
  3. Abrupt transitions: Sudden switches between launch screen and main interface
  4. 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

  1. Always implement minimum display times: Don't let splash screens flash by in under 300-500ms
  2. Use smooth transitions: Implement fade or cross-dissolve animations instead of abrupt switches
  3. Maintain visual consistency: Ensure splash screen assets match your app's initial UI state
  4. 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

  1. Keep it simple: Splash screens should be clean and focused
  2. Brand appropriately: Use splash screens to reinforce brand identity
  3. Provide feedback: Consider subtle
Makuhari Development Corporation
法人番号: 6040001134259
ご利用にあたって
個人情報保護方針
個人情報取扱に関する同意事項
お問い合わせ
Copyright© Makuhari Development Corporation. All Rights Reserved.