sunakulto
sunakulto

Reputation: 71

Can't achieve smooth animation in container when using AnimatedVisibility

I need some advice on my code written in Jetpack Compose.

I am trying to implement a simple composable with a simple animation. Let's say I have a container with two horizontally arranged buttons inside it. When certain conditions are met (e.g., the user clicks a button three times), one of the buttons must disappear with an animation: the button smoothly shrinks until it is gone, and the other button smoothly expands until it occupies all the available space. At the end of this post, I will attach a GIF with the desired animation.

So, the most obvious solution I came up with is to use the following composable structure:

Row {
    AnimatedVisibility {
        Button { ... }
    }
    Button { ... }
}
@Composable
    fun Content() {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Bottom
        ) {
            val clicks = remember { mutableIntStateOf(0) }
            val isSkipVisible by remember { derivedStateOf { clicks.intValue != MaxClicks } }
            Text(text = "Clicks: ${clicks.intValue}")
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 24.dp, vertical = 16.dp),
                horizontalArrangement = Arrangement.spacedBy(32.dp)
            ) {
                AnimatedVisibility(
                    modifier = Modifier.weight(1f),
                    visible = isSkipVisible
                ) {
                    Button(
                        colors = ButtonDefaults.buttonColors(
                            containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
                        ),
                        onClick = { if (clicks.intValue > 0) clicks.intValue -= 1 }
                    ) { Text(text = "Decrement") }
                }
                Button(
                    modifier = Modifier.weight(1f),
                    onClick = {
                        when (clicks.intValue == MaxClicks) {
                            true -> clicks.intValue -= 1
                            false -> clicks.intValue += 1
                        }
                    }
                ) { Text(text = if (clicks.intValue == MaxClicks) "Decrement" else "Increment") }
            }
            Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
        }
    }

But that didn't work.

enter image description here

The animation is janky.

While the first button disappears smoothly, the second button jumps to its position instead of growing smoothly. Trust me, I tried a lot of combinations of enter/exit parameters, including shrink/expand, but I couldn't achieve the desired result even after hours of trying.

Next, I tried a "hacky" solution where instead of removing the button from the composition, I just smoothly changed its width to zero while increasing the other button's width to the maximum.

@Composable
fun Content() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Bottom
    ) {
        val clicks = remember { mutableIntStateOf(0) }
        val isSkipVisible by remember { derivedStateOf { clicks.intValue != MaxClicks } }
        var width by remember { mutableIntStateOf(0) }
        val space = 64.dp / 2f
        val skipButtonWidth by animateIntAsState(if (isSkipVisible) (width / 2 - space.value).roundToInt() else 0)
        val continueButtonWidth by animateIntAsState(if (isSkipVisible) (width / 2 - space.value).roundToInt() else width)
        Text(text = "Clicks: ${clicks.intValue}")
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 24.dp, vertical = 16.dp)
                .onGloballyPositioned { width = it.size.width }
        ) {
            Button(
                colors = ButtonDefaults.buttonColors(
                    containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
                ),
                modifier = Modifier
                    .align(Alignment.CenterStart)
                    .width(with(LocalDensity.current) { skipButtonWidth.toDp() }),
                onClick = {
                    if (clicks.intValue > 0) clicks.intValue -= 1
                }
            ) {
                Text(
                    text = "Decrement",
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis
                )
            }
            Button(
                modifier = Modifier
                    .width(with(LocalDensity.current) { continueButtonWidth.toDp() })
                    .align(Alignment.CenterEnd),
                onClick = {
                    when (clicks.intValue == MaxClicks) {
                        true -> clicks.intValue -= 1
                        false -> clicks.intValue += 1
                    }
                }
            ) { Text(text = if (clicks.intValue == MaxClicks) "Decrement" else "Increment") }
        }
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
    }
}

And here is the desired animation:

enter image description here

So, my question is this: In my opinion, animating the button width to simulate the button disappearing is like using a sledgehammer to crack a nut. I believe that a solution with AnimatedVisibility is much cleaner and more understandable to achieve my goal, but it is still unclear how to do it. Any opinions? Can I use my second solution, or can you suggest a more "professional" way to do it?

Upvotes: 2

Views: 211

Answers (1)

BenjyTec
BenjyTec

Reputation: 10887

The problem is that you are applying the weight Modifier onto the AnimatedVisibility. So it can't actually smoothly collapse because this Modifier forces it to stay at the given width. If you remove the Modifier, you will see that the animation starts working.

It seems like there is no quick solution to it, only workarounds as the one you found or as suggested in this stackoverflow answer.

Also note that setting horizontalArrangement = Arrangement.spacedBy(32.dp) will cause a stuttering when hiding the button. Instead, apply the padding directly on each of the two Buttons.

Upvotes: 0

Related Questions