Let's cut to the chase: Android's default splash screen is boring. This library lets you create stunning animated splash screens using Compose without the headache. No more static drawables, no more janky transitions.
Look, we all know the pain points:
- Android's splash screen is just a static image
- Animations? Good luck with that
- Transitions that look like they're from 2010
- Zero Compose support out of the box
Here's what you get with androidx-splashscreen-compose:
- Drop-in Compose animations that actually look good
- Smooth transitions that don't make users cringe
- Complete control over timing and animations
- Works with AndroidX SplashScreen, not against it
- Add the dependency:
implementation 'net.kibotu:androidx-splashscreen-compose:{latest-version}'- Create your splash screen:
class MainActivity : ComponentActivity() {
private var splashScreen: SplashScreenDecorator? = null
override fun onCreate(savedInstanceState: Bundle?) {
// Initialize before super.onCreate()
splashScreen = splash {
content {
HeartBeatAnimation(
isVisible = isVisible.value,
exitAnimationDuration = exitAnimationDuration.milliseconds,
onStartExitAnimation = { startExitAnimation() }
)
}
}
// start your own splash screen animation
splashScreen?.shouldKeepOnScreen = false
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
MainScreen()
}
}
}
override fun onStart() {
super.onStart()
// trigger your custom splash animation
splashScreen?.dismiss()
}
}That's it. No, really.
Here's a real-world example of a heartbeat animation that actually ships in production apps:
fun HeartBeatAnimation(
modifier: Modifier = Modifier,
isVisible: Boolean = true,
exitAnimationDuration: Duration = Duration.ZERO,
onStartExitAnimation: () -> Unit = {}
) {
// Animation constants
val rippleCount = 4
val rippleDurationMs = 3313
val rippleDelayMs = rippleDurationMs / 8
val baseSize = 144.dp
val containerSize = 288.dp
// Track exit animation state
var isExitAnimationStarted by remember { mutableStateOf(false) }
// Trigger exit animation when visibility changes
LaunchedEffect(isVisible) {
if (!isVisible && !isExitAnimationStarted) {
isExitAnimationStarted = true
onStartExitAnimation()
}
}
// Calculate screen diagonal for exit animation scaling
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp
val screenHeight = configuration.screenHeightDp
val screenDiagonal = sqrt((screenWidth * screenWidth + screenHeight * screenHeight).toFloat())
// Exit animation scale with snappy easing
val snappyEasing = CubicBezierEasing(0.2f, 0.0f, 0.2f, 1.0f)
val exitAnimationScale by animateFloatAsState(
targetValue = if (isExitAnimationStarted) screenDiagonal / baseSize.value else 0f,
animationSpec = tween(
durationMillis = exitAnimationDuration.toInt(DurationUnit.MILLISECONDS),
easing = snappyEasing
),
label = "exitScale"
)
// Infinite ripple animation transition
val infiniteTransition = rememberInfiniteTransition(label = "heartbeatTransition")
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// Only show ripples when visible and not exiting
if (isVisible && !isExitAnimationStarted) {
Box(
modifier = Modifier.size(containerSize),
contentAlignment = Alignment.Center
) {
// Create ripple circles with staggered animations
repeat(rippleCount) { index ->
RippleCircle(
infiniteTransition = infiniteTransition,
index = index,
rippleDurationMs = rippleDurationMs,
rippleDelayMs = rippleDelayMs,
baseSize = baseSize
)
}
}
}
// Exit animation circle
if (isExitAnimationStarted) {
Box(
modifier = Modifier
.size(baseSize)
.graphicsLayer {
scaleX = exitAnimationScale
scaleY = exitAnimationScale
}
.background(
color = blueCatalina,
shape = CircleShape
)
)
}
}
}
@Composable
private fun RippleCircle(
infiniteTransition: InfiniteTransition,
index: Int,
rippleDurationMs: Int,
rippleDelayMs: Int,
baseSize: Dp
) {
val totalDuration = rippleDurationMs + (rippleDelayMs * index)
val easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
// Animate scale from 1f to 4f
val animatedScale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 4f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = totalDuration,
delayMillis = rippleDelayMs * index,
easing = easing
),
repeatMode = RepeatMode.Restart
),
label = "rippleScale$index"
)
// Animate alpha from 0.25f to 0f
val animatedAlpha by infiniteTransition.animateFloat(
initialValue = 0.25f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = totalDuration,
delayMillis = rippleDelayMs * index,
easing = easing
),
repeatMode = RepeatMode.Restart
),
label = "rippleAlpha$index"
)
Box(
modifier = Modifier
.size(baseSize)
.graphicsLayer {
scaleX = animatedScale
scaleY = animatedScale
alpha = animatedAlpha
}
.background(
color = blueCatalina,
shape = CircleShape
)
)
}
-
Timing is Everything
splashScreen = splash { content { exitAnimationDuration = 800L // Sweet spot for most animations composeViewFadeDurationOffset = 200L // Prevents jarring transitions } }
-
Memory Management
override fun onDestroy() { splashScreen = null // Don't leak memory super.onDestroy() }
-
Performance First
- Use
rememberInfiniteTransition()for repeating animations - Keep animations under 1 second (users hate waiting)
- Test on low-end devices
- Use
| Feature | androidx-splashscreen-compose | AndroidX SplashScreen |
|---|---|---|
| Animation Support | β Full Compose animations | β Static vector only |
| Custom Content | β Any Composable | β Icon + background only |
| Transition Control | β Precise timing control | β Limited control |
| Branding Flexibility | β Complete creative freedom | β Very constrained |
| Implementation Complexity | β Simple DSL setup | β Minimal setup |
| Performance | β Optimized Compose rendering | β Lightweight |
| Backward Compatibility | β Built on AndroidX | β Native support |
| Feature | androidx-splashscreen-compose | Custom Splash Activity |
|---|---|---|
| Android 12+ Compliance | β Fully compliant | β Requires extra work |
| App Launch Performance | β No additional activity | β Extra activity overhead |
| Transition Seamlessness | β Native system integration | β Potential flicker |
| Code Complexity | β Single file setup | β Multiple components |
| Maintenance | β Library handles updates | β Manual Android compliance |
Use androidx-splashscreen-compose when:
- You need animations that don't look like they're from a 2010 tutorial
- Your brand guidelines require more than a static logo
- You want Compose-based animations without the setup headache
- Android 12+ compliance with zero additional effort
- Seamless integration with existing AndroidX SplashScreen setup
Stick with AndroidX SplashScreen when:
- A static logo is all you need
- You're optimizing for the smallest possible APK size
- You don't need any custom animations
Choose Custom Splash Activity when:
- Pre-Android 12 apps with no compliance requirements
- Complex initialization flows requiring multiple screens
- Non-Compose apps with View-based animations
- Minimum Android SDK: 21
- Target Android SDK: 36
- Kotlin: 2.2.0
- Java: 17
- Gradle: 8.12.0
Got ideas? Found a bug? PRs are welcome:
- Fork it
- Create your feature branch (
git checkout -b feature/amazing) - Commit your changes (
git commit -m 'Add something amazing') - Push to the branch (
git push origin feature/amazing) - Open a Pull Request
Apache 2.0 - do what you want, just don't blame us if something goes wrong. See LICENSE for the boring details.
- Built on top of AndroidX SplashScreen
- Powered by Jetpack Compose
- Inspired by modern app branding expectations
- Made with β by kibotu
