Explore Compose Transition Class
Lets explore the compose Transition, MutableTransitionState and SeekableTransitionState classes from the androidx.compose.animation package
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.