androidkotlinandroid-jetpack-composejetpack-compose-animation

Is it possible to track the end of animation in AnimatedVisibility and, if it is completed, change the visibility of nested components?


I'm trying to make it so that when I click a button, an element disappears and then appears. I've looked for different ways to implement it, but none of them work.

I tried to implement such animation by tracking transition state, but without success. For some reason, the element appears on the same side to which it moved during the exit animation. Below is my code:

@Composable
fun Greeting() {

    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {

        val visibleState = remember { MutableTransitionState(initialState = true).apply { targetState = true } }

        AnimatedVisibility(
            visibleState = visibleState,
            enter = slideInHorizontally(
                animationSpec = tween(100, easing = LinearEasing),
                initialOffsetX = { it }
            ),
            exit = slideOutHorizontally (
                animationSpec = tween(100, easing = LinearEasing),
                targetOffsetX = { -it }
            )
        ) {
            RedBox()
        }

        Button {
            visibleState.targetState = false
        }

        DisposableEffect(visibleState.currentState) {
            onDispose {
                visibleState.targetState = true
            }
        }

    }

}

Solution

  • The problem with AnimatedVisibility is that its content disappears for a moment, which can cause adjacent elements to move. As an alternative you can animate IntOffset with a Transition:

    private enum class VisibleState { VISIBLE, EXIT, ENTER }
    
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        val boxSize = 50.dp
        val pxToMove = with(LocalDensity.current) {
            boxSize.toPx().roundToInt()
        }
        val visibleState = remember { MutableTransitionState(VisibleState.VISIBLE) }
        val transition = rememberTransition(visibleState)
        val offset by transition.animateIntOffset(
            transitionSpec = {
                if (VisibleState.EXIT isTransitioningTo VisibleState.ENTER) {
                    snap()  // move from left to right immediately
                } else {
                    tween(200, easing = LinearEasing)
                }
            },
            label = "box animation",
        ) { targetState ->
            when (targetState) {
                VisibleState.EXIT -> IntOffset(-pxToMove, 0)
                VisibleState.ENTER -> IntOffset(pxToMove, 0)
                VisibleState.VISIBLE -> IntOffset.Zero
            }
        }
        if (visibleState.isIdle) {
            // If isIdle == true, it means the transition has arrived at its target state
            if (visibleState.targetState == VisibleState.EXIT) {
                // If target was Exit, transition to Enter (instantly move from left to right)
                visibleState.targetState = VisibleState.ENTER
            } else if (visibleState.targetState == VisibleState.ENTER) {
                // If target was Enter, transition to Visible (animate from right to middle)
                visibleState.targetState = VisibleState.VISIBLE
            }
        }
    
        Box(modifier = Modifier
            .clipToBounds()
            .offset { offset }
            .size(boxSize)
            .background(Color.Red)
        )
        Button(onClick = { visibleState.targetState = VisibleState.EXIT }) {
            Text("Toggle")
        }
    }
    

    screen capture