上一篇文章中,学习了AnimateAsState,每个动画各自使用各自的时钟,是独立的,那么当需要同时控制多个动画时,就需要用到Transition了。
概念
Transition 在状态级别管理所有子动画。可以使用 Transition.animateFloat、Transition.animateValue、animateColor 等以声明性方式创建子动画。内部有currentState和targetState两个状态,当 targetState和currentState不一样时就会触发动画。
构造Transition对象
updateTransition
定义:
updateTransition
函数需要传入一个目标状态targetState
。
kotlin
@Composable
public fun <T> updateTransition(targetState: T, label: String? = null): Transition<T>
示例代码:
- 代码中定义方块的状态枚举类型
BoxState
,以及承载枚举变化的状态toState
。 - 在
updateTransition
中传入目标状态,也就是toState
- 通过
updateTransition.animateOffset
和updateTransition.animateColor
分别定义不同目标状态时的数据。 - 在UI组件中分别应用
offset
和color
,点击更改toState
的值,也就更改updateTransition的目标状态,就会触发动画。 Transition
对象里可以获取currentState
,targetState
,isRunning
等属性,示例中通过isRunning
控制点击按钮的Icon变化。
kotlin
enum class BoxState {
Left, Right
}
@Preview
@Composable
fun AnimationExample() {
var iconIsPlay by remember { mutableStateOf(false) }
var toState by remember { mutableStateOf(BoxState.Left) }
val updateTransition = updateTransition(
targetState = toState,
label = "updateTransition"
)
LaunchedEffect(updateTransition.isRunning) {
iconIsPlay = updateTransition.isRunning
}
val offset by updateTransition.animateOffset(
targetValueByState = { boxState ->
when (boxState) {
BoxState.Left -> Offset(x = 0f, y = 0f)
BoxState.Right -> Offset(x = 350f, y = 0f)
}
},
transitionSpec = {
spring(stiffness = 50f)
}
)
val color by updateTransition.animateColor(
targetValueByState = { boxState ->
when (boxState) {
BoxState.Left -> Color(0xFF4CAF50)
BoxState.Right -> Color(0xFF2196F3)
}
},
transitionSpec = {
spring(stiffness = 50f)
}
)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "UpdateTransition")
Box(
modifier = Modifier
.width(400.dp)
.height(50.dp)
.background(Color(0x405E5D5D))
) {
Box(
modifier = Modifier
.size(50.dp)
.offset(offset.x.dp, offset.y.dp)
.background(color)
)
}
Spacer(modifier = Modifier.height(10.dp))
Image(
imageVector = if (iconIsPlay) {
Icons.Filled.PauseCircle
} else {
Icons.Filled.PlayCircle
},
contentDescription = null,
modifier = Modifier
.size(50.dp)
.clickable {
toState = if (toState == BoxState.Left) {
BoxState.Right
} else {
BoxState.Left
}
}
)
}
}
效果:

rememberTransition
定义:
rememberTransition
的参数换成了TransitionState,意思是需要我们构建一个TransitionState,在updateTransition
中,内部也是将我们传入的targetState封装成TransitionState,因此rememberTransition
更加底层。
kotlin
@Composable
public fun <T> rememberTransition(
transitionState: TransitionState<T>,
label: String? = null
): Transition<T>
使用:
替换掉updateTransition
中的toState
为自定义的transitionState
,点击时候修改transitionState.targetState
即可。 优势 :相比updateTransition
的优势是,组件第一次组合时时可以立刻触发动画,构建transitionState对象后加上 LaunchedEffect(Unit) { transitionState.targetState = BoxState.Right }
即可,适用于UI启动动画的场合。
kotlin
// var toState by remember { mutableStateOf(BoxState.Left) }
//
// val updateTransition = updateTransition(
// targetState = toState,
// label = "updateTransition"
// )
val transitionState = remember { MutableTransitionState(BoxState.Left) }
val rememberTransition = rememberTransition(
transitionState = transitionState
)
LaunchedEffect(rememberTransition.isRunning) {
iconIsPlay = rememberTransition.isRunning
}
效果:
加上自启动的逻辑的效果,可以看到启动后立刻执行了动画。
TransitionState的两种类型
MutableTransitionState
,默认的类型,通过更改它的targetState
来触发动画,同时提供属性isIdle
来获取动画是否完成。SeekableTransitionState
,更加精细的控制动画,触发动画不能修改targetState
了,而是使用下列三个函数:
- animateTo:将动画执行到传入的目标状态,注意需要在协程作用域内调用,如下代码中。
kotlin
.clickable {
// transitionState.targetState =
// if (transitionState.targetState == BoxState.Left) {
// BoxState.Right
// } else {
// BoxState.Left
// }
scope.launch {
if (transitionState.targetState == BoxState.Left) {
transitionState.animateTo(BoxState.Right)
} else {
transitionState.animateTo(BoxState.Left)
}
}
}
- seekTo:允许控制动画的进度,比如增加一个slider来实现拖动触发动画,拖动的比例就是动画的时间比例。
kotlin
Slider(
value = sliderPosition,
modifier = Modifier
.width(400.dp)
.wrapContentHeight(),
onValueChange = { value ->
sliderPosition = value
scope.launch {
val target = if (transitionState.currentState == BoxState.Left) {
BoxState.Right
} else {
BoxState.Left
}
transitionState.seekTo(fraction = value, targetState = target)
}
}
)

- snapTo:直接跳转到目标状态,跳过动画过程,如下代码还是使用点击触发动画,将animateTo改为snapTo,可以看到往右时有动画,往左没有。
kotlin
.clickable {
scope.launch {
if (transitionState.targetState == BoxState.Left) {
transitionState.animateTo(BoxState.Right)
} else {
transitionState.snapTo(BoxState.Left)
}
}
}

子Transition
使用createChildTransition
创建transition的子Transition,父子Transition使用同一个动画时钟,一般用于有依赖关系的动画场景。
定义:
kotlin
@ExperimentalTransitionApi
@Composable
public inline fun <S, T> Transition<S>.createChildTransition(
label: String = "ChildTransition",
transformToChildState: @Composable (parentState: S) -> T,
): Transition<T>
使用:
kotlin
val childTransition = rememberTransition.createChildTransition { parentState ->
when (parentState) {
BoxState.Left -> TODO()
BoxState.Right -> TODO()
}
}
构建动画
前面的示例代码中其实已经用到了animateOffset
和animateColor
,还有其他的如下图:
animateValue
其中animateValue
是通用的类型构建,和上一篇文章中的animateValueAsState
是一样的逻辑,使用到了TwoWayConverter
转换,具体不会使用可以阅读上一篇文章compose动画从底层基础到顶层高级应用(二)核心API之--AnimateAsState
kotlin
@Composable
public inline fun <S, T, V : AnimationVector> Transition<S>.animateValue(
typeConverter: TwoWayConverter<T, V>,
noinline transitionSpec: @Composable Transition.Segment<S>.() -> FiniteAnimationSpec<T> = {
spring()
},
label: String = "ValueAnimation",
targetValueByState: @Composable (state: S) -> T
): State<T>
总结
当多个动画需要使用同一个时钟时,就要使用到Transition。但是Transition有一个不太好的地方是,如果动画不是采用的springSpec,中断动画不会使用定义的动画规格,而是使用默认设置的spring(),而且刚度1500f,看起来会非常快,就像突然跳转。具体可以阅读我的另一篇文章:Compose Transition中断动画的特殊性。 如果喜欢我的文章,请点赞收藏 :)