In this post I will show you a sample of using transition animations in Android Compose to customize the UI of a BadgeBox.
Scenario
In my current project, I use a BadgeBox to inform the user there is an action for them to view. The default UI was plain and needed to be improved. The element needed an animation added so it could be more eye catching for the user when there is a new event.
I decided to use a transition animation so I could manipulate various aspects of the BadgeBox. I chose to animate the offset and the scale.
The sample is just using the onclick method in the BadgeBox to initiate the animation and update the count. This could be adjusted with a simple callback to fit into your project.
Code
CustomAnimBadgeBox
Below is the custom BadgeBox element.
@Composable
fun CustomAnimBadgeBox(badgeSize : Dp, iconSize : Dp, duration : Int, targetOffset: IntOffset, targetScale : Float){
var badgeBoxCount by remember { mutableStateOf(0) }
val animState = remember { MutableTransitionState(false) }
val transitionAnim = updateTransition(animState)
val scaleAnim by transitionAnim.animateFloat(
transitionSpec = {
tween(duration)
}
) { state ->
if(!state) 1.0f else targetScale
}
val offsetAnim by transitionAnim.animateIntOffset(
transitionSpec = {
tween(duration)
}
) { state ->
if(!state) IntOffset(x = 0, y = 0) else targetOffset
}
//this is used to determine if the animation is complete, if so then set state to false -> ready for next animation call
if(animState.currentState == animState.targetState){
animState.targetState = false
}
BadgedBox(badge = {
Badge(
modifier = Modifier
.size(badgeSize)
.offset { offsetAnim }
.graphicsLayer {
scaleX = scaleAnim
scaleY = scaleAnim
},
containerColor = MaterialTheme.colorScheme.primary
) {
Text(text = "$badgeBoxCount")
}
},
modifier = Modifier
.padding(end = 20.dp)
.clickable {
badgeBoxCount++
animState.targetState = true
}) {
Icon(
modifier = Modifier.size(iconSize)
.offset { offsetAnim }
.graphicsLayer {
scaleX = scaleAnim
scaleY = scaleAnim
},
imageVector = Icons.Filled.Favorite,
contentDescription = "Empty Desc",
tint = MaterialTheme.colorScheme.inversePrimary
)
}
}
I tried to add a few parameters so you may customize to your needs.
MainActivity
To use the custom BadgeBox with Android transition animations implemented, you can call it like the sample below.
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SampleBadgeBoxAnimationTheme {
Scaffold(modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = {
Text(
text = "Animated BadgeBox",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium
)
}, actions = {
CustomAnimBadgeBox(
badgeSize = 24.dp,
iconSize = 48.dp,
duration = 300,
targetOffset = IntOffset(x = -15, y = 10),
targetScale = 1.5f
)
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
)
}) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
Bonus
I first attempted to create the animation with the animateFloatasState() method. I will post this below but I had a few minor issues with it. If the state was updated too fast before the animations finished, it was locked in a state of true. This would create an unpleasant experience for the user. I am posting it anyway in case this sample could be useful for another situation for someone.
val scaleAnim = animateFloatAsState(
if(!animState) 1.0f else 1.5f,
animationSpec = repeatable(
iterations = 1,
animation = tween(300),
repeatMode = RepeatMode.Reverse
),
finishedListener = {
animState = false
})
I hope this was useful.

Update
I chose to post how I implemented this with a callback in my project. Also I also edited the animation slightly. I added a color animation to the transition animation.
@Composable
fun CustomAnimBadgeBox(
badgeBoxCount : Int,
badgeSize : Dp,
iconSize : Dp,
duration : Int,
targetOffset: IntOffset,
targetScale : Float,
targetColor : Color,
clickCallback : () -> Unit){
//var badgeBoxCount by remember { mutableStateOf(count) }
val animState = remember { MutableTransitionState(false) }
val transitionAnim = updateTransition(animState)
val scaleAnim by transitionAnim.animateFloat(
transitionSpec = {
tween(duration)
}
) { state ->
if(!state) 1.0f else targetScale
}
val offsetAnim by transitionAnim.animateIntOffset(
transitionSpec = {
tween(duration)
}
) { state ->
if(!state) IntOffset(x = 0, y = 0) else targetOffset
}
val colorAnim by transitionAnim.animateColor(
transitionSpec = {
tween(duration)
}
) { state ->
if(!state) MaterialTheme.colorScheme.inversePrimary else targetColor
}
//this is used to determine if the animation is complete, if so then set state to false -> ready for next animation call
if(animState.currentState == animState.targetState){
animState.targetState = false
}
LaunchedEffect(badgeBoxCount) {
animState.targetState = true
}
BadgedBox(badge = {
Badge(
modifier = Modifier
.size(badgeSize)
.offset { offsetAnim }
.graphicsLayer {
scaleX = scaleAnim
scaleY = scaleAnim
},
containerColor = MaterialTheme.colorScheme.primary
) {
Text(text = "$badgeBoxCount")
}
},
modifier = Modifier
.padding(end = 20.dp)
.clickable {
clickCallback()
}) {
Icon(
modifier = Modifier.size(iconSize)
.offset { offsetAnim }
.graphicsLayer {
scaleX = scaleAnim
scaleY = scaleAnim
},
imageVector = Icons.Filled.Favorite,
contentDescription = "Sync Requests Pending",
tint = colorAnim
)
}
}
CustomAnimBadgeBox
Then I called it from my main composable shown below.
CustomAnimBadgeBox(
badgeBoxCount = syncRequests.size,
badgeSize = 24.dp,
iconSize = 48.dp,
duration = 300,
targetOffset = IntOffset(x = -15, y = 10),
targetScale = 1.5f,
targetColor = Color.Green,
clickCallback = {
//your callback method here to process what you want from the BadgeBox clickable call
}
)