Explore Compose Transition Class

Explore Compose Transition Class

Lets explore the compose Transition, MutableTransitionState and SeekableTransitionState classes from the androidx.compose.animation package

ยท

5 min read

What is Transition and TransitionState?

The Transition class serves as a versatile controller, facilitating the creation and management of numerous child animations through a single object. This capability is particularly beneficial when needing precise control over multiple animations simultaneously, such as managing various AnimatedVisibility components collectively.

It relies exclusively on TransitionState to operate, enabling comprehensive animation control. By leveraging TransitionState, we gain the ability to effectively manage various animations within our application.

We have two ways to instantiate a Transition object. Explicitly by creating a TransitionState or implicitly through the updateTransition function, which takes a targetState: T as input. Whenever there's a change in the passed state value, the transition's targetState also changes during recomposition. Internally, it utilizes a MutableTransitionState to handle state changes.

Explicitly

val mutableTransitionState = remember { MutableTransitionState(initialState = false) }
val transition = rememberTransition(transitionState = mutableTransitionState)

Implicitly

val visible = remember { mutableStateOf(false) }
val transition = updateTransition(targetState = visible.value, label = "label")

How to use Transition?

To utilize Transition, we leverage the extension functions provided within the Transition class. These functions enable the creation of child animations effortlessly. Specifically, we can employ Transition's extension functions such as Transition::AnimatedVisibility or Transition::AnimatedContent.

val visible = remember { MutableTransitionState(initialState = false) }
val transition = rememberTransition(transitionState = visible)

val floatState = transition.animateFloat(label = "") {
        if (it) 1f else 0.5f
}

Button(onClick = { visible.targetState = !visible.targetState }) {
    Text(text = "Invert")
}

transition.AnimatedContent { targetValue ->
    if (targetValue) {
        Item(Color.Green)
    } else {
        Item(Color.Magenta)
    }
}

Box(
    Modifier.graphicsLayer {
        this.alpha = floatState.value
    }
) {
    Item(Color.Blue)
}

transition.AnimatedVisibility(visible = { it }) {
    Item(Color.Red)
}

@Composable
private fun Item(color: Color) {
    Box(Modifier.fillMaxWidth().height(100.dp).background(color))
}

With Transition, we have the capability to create numerous child transition animations effortlessly. Alongside this, Transition offers a range of animate extension functions, including animateDp, animateOffset, animateIntOffset, and others.

The transition extension of AnimatedVisibility have a lambda parameter visible instead of a boolean value. This feature allows for flexible control over visibility. For instance, if there are two instances of AnimatedVisibility, we can invert the visibility of one by adjusting the incoming lambda parameter.

transition.AnimatedVisibility(
    visible = { incomingValue ->
        !incomingValue
        // This will be visible what the state is false and
        // invisible when true
    }
) {
    Item(Color.Red)
}

Problem with MutableTransitionState !

If we just want to change the state and expects the animation to happen and reach the target ui state, then no problem...!๐Ÿ™„.

However, if we require the ability to listen for the animation's completion, additional code is necessary to monitor these changes. MutableTransitionState provides access to currentState, targetState, and isIdle flags, requiring us to handle the tracking ourselves.

LaunchedEffect(visible.currentState) {
    if (visible.currentState && visible.isIdle) {
        // Animation finished for visible state on true
    }
}

In this context, MutableTransitionState::currentState serves as a composable state, which we incorporate as a key within LaunchedEffect. Consequently, when the currentState undergoes a change, the effect lambda is triggered. We utilize this mechanism to ensure that currentState is true and the animation is idle.

As we delve into more sequential and complex animations, the complexity escalates further. The need to code multiple LaunchedEffects increases significantly. However, there's a new contender on the scene: SeekableTransitionState. This alternative offers a more streamlined API compared to MutableTransitionState, simplifying the process of managing intricate animations.

SeekableTransitionState to the rescue ๐ŸŽŠ๐ŸŽ‰๐Ÿฅณ

Introducing SeekableTransitionState, a new addition to Jetpack Compose 1.6 as an experimental feature. Unlike traditional state changes, SeekableTransitionState allows us to trigger animations directly using the animate(targetState) suspend function. This offers a more flexible approach to managing animations within Compose.

While SeekableTransitionState was introduced in Jetpack Compose 1.6 as an experimental feature, its API was not fully developed at that time. However, with the release of Compose 1.7 alpha05 version, the API has been finalized and marked as stable. For this example, we'll be utilizing version 1.7 alpha05 to leverage the full capabilities of SeekableTransitionState.

val transitionState = remember { SeekableTransitionState(initialState = false) }
val transition = rememberTransition(transitionState = transitionState)

LaunchedEffect(Unit) {
    delay(2000)
    transitionState.animateTo(true)
    // Animation done
}

transition.AnimatedVisibility(visible = { it }) {
    Item(Color.Red)
}

In this example, we're implementing a sequential animation scenario. After a 2-second delay using the delay function, we trigger the animation using TransitionState::animateTo, a suspend function that halts execution until the animation completes. We have three TransitionState instances, each running sequentially.

val transitionState1 = remember { SeekableTransitionState(false) }
val transition1 = rememberTransition(transitionState = transitionState1)

val transitionState2 = remember { SeekableTransitionState(false) }
val transition2 = rememberTransition(transitionState = transitionState2)

val transitionState3 = remember { SeekableTransitionState(false) }
val transition3 = rememberTransition(transitionState = transitionState3)

LaunchedEffect(Unit) {
    delay(2000)
    transitionState1.animateTo(true)
    delay(500)
    transitionState2.animateTo(true)
    delay(200)
    transitionState3.animateTo(true)
}

Column {
    transition1.AnimatedVisibility(visible = { it }) {
        Item(Color.Red)
    }
    transition2.AnimatedVisibility(visible = { it }) {
        Item(Color.Blue)
    }
    transition3.AnimatedVisibility(visible = { it }) {
        Item(Color.Green)
    }
}

animateTo suspend function accepts FiniteAnimationSpec as second param also. Below example have the animation spec of tween with duration of 2 seconds

LaunchedEffect(Unit) {
    transitionState.animateTo(true, tween(durationMillis = 2000))
}

We can also a custom type as the initial value and can pass different values other than just boolean.

enum class StateType {
    First, Second, Third
}

val transitionState = remember { SeekableTransitionState(initialValue = StateType.First) }
val transition = rememberTransition(transitionState = transitionState)

LaunchedEffect(Unit) {
    delay(2000)
    transitionState.animateTo(StateType.Second)
    delay(500)
    transitionState.animateTo(StateType.Third)
    delay(200)
    transitionState.animateTo(StateType.First)
}

Column {
    transition.AnimatedVisibility(visible = { it == StateType.First }) {
        Item(Color.Red)
    }
    transition.AnimatedVisibility(visible = { it == StateType.Second }) {
        Item(Color.Blue)
    }
    transition.AnimatedVisibility(visible = { it == StateType.Third }) {
        Item(Color.Green)
    }
}

We can also seek animations using fraction values with SeekableTransitionState. In the example below, clicking a button initiates a coroutine scope. After a 2-second delay, we seek the animation to the true state, specifically to 30% of its duration. As a result, the AnimatedVisibility component concludes the transition at the 30% mark, leaving the box partially visible.

val coroutineScope = rememberCoroutineScope()

Button(
    onClick = {
        coroutineScope.launch {
            delay(2000)
            transitionState.seekTo(0.3f, true)
        }
    }
) {
    Text(text = "Invert")
}

transition.AnimatedVisibility(visible = { it }) {
    Item(Color.Red)
}

I'm eagerly awaiting the stable release of Jetpack Compose 1.7 before integrating it into production code. SeekableTransitionState seems particularly promising with its impressive API, especially in how it aligns with other animation APIs in Compose by providing suspend functions. This consistency across animation functionalities makes it a compelling choice for future development.

ย