Compose Compose动画之中止和入场效果
- 动画的中止
- 出入场动画效果实现:AnimatedVisibility
打断动画
stop()
主动打断/终止一个正在执行的动画,可以用stop
函数
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
val exponentDecay = exponentialDecay<Dp>()
LaunchedEffect(key1 = Unit) {
delay(1000)
try {
anim.animateDecay(1200.dp, exponentDecay)
} catch (e: CancellationException) {
Log.e(TAG, "anim is CancellationException")
e.printStackTrace()
}
}
LaunchedEffect(key1 = Unit) {
delay(1300)
// 中断动画
anim.stop()
}
Box(
Modifier
.padding(0.dp, top = anim.value, 0.dp, 0.dp)
.size(100.dp)
.background(Color.Red)
)
}
}
并且,我们在动画执行代码中去try-catch
捕获异常,在被中断时是可以catch
到异常的:
E/StopAnimateActivity: anim is CancellationException
W/System.err: kotlinx.coroutines.JobCancellationException: ScopeCoroutine was cancelled; job=ScopeCoroutine{Cancelled}@54d4673
updateBounds()
这是我们通过调用stop函数主动的停止。还有一种很常见的场景,边界中断。就和现实中一个物体滑动撞到墙一样,停了下来,Compose
动画也提供了这种场景的实现:updateBounds
函数。
fun updateBounds(lowerBound: T? = this.lowerBound, upperBound: T? = this.upperBound)
比如对上述示例代码设置边界:
// 更新边界,到达边界自动停止
anim.updateBounds(upperBound = 200.dp)
需要注意的是,这种方式的停止,和stop
是不一样,它并不会抛出取消异常,该种方式被视为一种正常的中止操作。
动画函数返回值
animateDecay、animateTo等函数之前只介绍了它们的参数,其实它们是有返回值的
suspend fun animateDecay(
initialVelocity: T,
animationSpec: DecayAnimationSpec<T>,
block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V>
class AnimationResult<T, V : AnimationVector>(
val endState: AnimationState<T, V>, // 结束时的状态
val endReason: AnimationEndReason // 结束原因
)
enum class AnimationEndReason {
BoundReached, // 到达边界
Finished // 完成
}
通过源码,看到动画结束时的原因有两种,一种正常完成Finished
,另一种到达边界BoundReached
如上述所演示的那样,当我们通过updateBounds
函数设置边界,提前中止了动画后,获取结果就会是BoundReached
Transition
Transition
可管理一个或多个动画作为其子项,并在多个状态之间同时运行这些动画。
这里的状态可以是任何数据类型。在大多数情况下,可以使用自定义 enum
类型
updateTransition()
updateTransition
可创建并记住 Transition
的实例,并更新其状态。
@Composable
fun <T> updateTransition(
targetState: T,
label: String? = null
): Transition<T> {
val transition = remember { Transition(targetState, label = label) }
transition.animateTo(targetState)
DisposableEffect(transition) {
onDispose {
// Clean up on the way out, to ensure the observers are not stuck in an in-between
// state.
transition.onTransitionEnd()
}
}
return transition
}
举个例子,实现一个方块可以变大小的同时变化透明度的动画过程:
先定义一个枚举表示状态:
enum class BoxState {
SMALL, BIG
}
使用updateTransition
,根据状态更改方块的大小和透明度:
@Composable
@Preview
fun TransitionExample() {
// 设定当前的状态
var curBoxState by remember { mutableStateOf(BoxState.SMALL) }
// 创建/更新Transition
val transition = updateTransition(targetState = curBoxState, label = "BoxState")
// 大小根据状态变化
val size by transition.animateDp(label = "size") {
if (it == BoxState.BIG) 96.dp else 48.dp
}
// 透明度根据状态变化
val alpha by transition.animateFloat(label = "alpha") {
if (it == BoxState.BIG) 0.1f else 0.9f
}
Box(
Modifier
.size(size)
.alpha(alpha)
.background(Color.Green)
.clickable {
curBoxState = if (curBoxState == BoxState.SMALL) BoxState.BIG
else BoxState.SMALL
})
}
运行看最终的效果:
上述案例中,多个状态的初始状态是由目标状态值targetState
,也就是这里的curBoxState
决定的。默认初始状态与第一个目标状态是一致的。
val transition = updateTransition(targetState = curBoxState, label = "BoxState")
那如果我们想对初始值做单独的设定,希望初始状态与第一个目标状态不同,就可以通过结合使用 updateTransition
和 MutableTransitionState
来实现这一点。
// 设定初始状态
val curBoxTransitionState = remember { MutableTransitionState(BoxState.SMALL) }
// 设定目标状态
curBoxTransitionState.targetState = BoxState.BIG
// 创建/更新Transition
val transition = updateTransition(targetState = curBoxTransitionState, label = "BoxState")
结合AnimationSpec
如果想对某个点状态的动画过程做细节的定制,也可以和之前之前学习的AnimationSpec
结合使用:
在animateDp
函数API中,为我们提供这样一个参数transitionSpec
,它是一个FiniteAnimationSpec
对象
inline fun <S> Transition<S>.animateDp(
noinline transitionSpec: @Composable Transition.Segment<S>.() -> FiniteAnimationSpec<Dp> = {
spring(visibilityThreshold = Dp.VisibilityThreshold)
},
label: String = "DpAnimation",
targetValueByState: @Composable (state: S) -> Dp
): State<Dp> =
animateValue(Dp.VectorConverter, transitionSpec, label, targetValueByState)
所以当我们想对上面的方块的大小状态进行弹性动画,就可以这样做:
val size by transition.animateDp(
{
if (curBoxTransitionState.currentState == BoxState.SMALL
&& curBoxTransitionState.targetState == BoxState.BIG
) {
tween(2000)
} else spring()
},
label = "size"
) {
if (it.targetState == BoxState.BIG) 200.dp else 48.dp
}
AnimatedVisibility
如果想对一个view
的显隐状态变化做动画,或者说是出入场动画,就可以用AnimatedVisibility
:
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
val transition = updateTransition(visible, label)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
给我们提供了很多扩展方法,便于使用,比如Column
内的view
:
@Composable
fun ColumnScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandVertically(), //
exit: ExitTransition = fadeOut() + shrinkVertically(), //
label: String = "AnimatedVisibility",
content: @Composable AnimatedVisibilityScope.() -> Unit
) {
val transition = updateTransition(visible, label)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
其中最重要的两个参数就是enter
和exit
,可以实现入场和出场动画效果的配置。下面详细介绍下入场enter
,出场exit
和它相反的。
比如,实现一个Box
的淡入效果,使用fadeIn
:
var visible by remember {
mutableStateOf(false)
}
// 淡入使用fadeIn,initialAlpha: 初始透明度
AnimatedVisibility(visible = visible, enter = fadeIn(initialAlpha = 0.2f)) {
BoxView()
}
比如,实现滑动入场效果,使用slideIn
:
// 滑动入场使用slideIn,initialOffset:设置出场位置
AnimatedVisibility(
visible = visible,
enter = slideIn(initialOffset = { IntOffset(-it.width, -it.height) })
) {
BoxView()
}
比如,展开,裁剪入场效果,使用expandIn
:
// 展开,裁剪效果入场,expandFrom: 展开的方向, initialSize: 展开的初始位置,clip: 是否裁剪,默认是true
AnimatedVisibility(
visible = visible,
enter = expandIn(
tween(5000),
expandFrom = Alignment.TopStart,
initialSize = { IntSize(it.width / 2, it.height / 2) },
clip = true
)
) {
BoxView()
}
再比如设置缩放入场效果,使用scaleIn
:
// initialScale:初次缩放值得,transformOrigin: 设置缩放中心,默认是中心
AnimatedVisibility(
visible = visible,
enter = scaleIn(
initialScale = 0f, transformOrigin = TransformOrigin(0f, 0f)
)
) {
BoxView()
}
如果想实现更复杂的效果,比如对上述的多个入场效果进行叠加,也非常简单,一个+
就可以解决:
// 叠加效果:淡入+裁剪展开
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(5000), initialAlpha = 0.1f) + expandIn(tween(3000), expandFrom = Alignment.TopStart)
) {
BoxView("多种效果叠加入场")
}
需要注意的是,如果+
前后使用了重复的效果,并不会叠加,而是优先使用前者,从源码实现可以看出:
operator fun plus(enter: EnterTransition): EnterTransition {
return EnterTransitionImpl(
TransitionData(
fade = data.fade ?: enter.data.fade,
slide = data.slide ?: enter.data.slide,
changeSize = data.changeSize ?: enter.data.changeSize,
scale = data.scale ?: enter.data.scale
)
)
}