动画修饰符和可组合项

Compose 提供了内置的可组合项和修饰符,用于处理常见的动画用例。

内置动画可组合项

使用 AnimatedVisibility 实现出现和消失动画

Green composable showing and hiding itself
图 1. 在列中为项目的出现和消失添加动画效果

AnimatedVisibility 可组合项可为其内容添加出现和消失动画。

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

默认情况下,内容通过淡入和展开来出现,通过淡出和收缩来消失。可以通过指定 EnterTransitionExitTransition 来自定义过渡效果。

var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
    visible = visible,
    enter = slideInVertically {
        // Slide in from 40 dp from the top.
        with(density) { -40.dp.roundToPx() }
    } + expandVertically(
        // Expand from the top.
        expandFrom = Alignment.Top
    ) + fadeIn(
        // Fade in with the initial alpha of 0.3f.
        initialAlpha = 0.3f
    ),
    exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
    Text(
        "Hello",
        Modifier
            .fillMaxWidth()
            .height(200.dp)
    )
}

如上例所示,您可以使用 + 运算符组合多个 EnterTransitionExitTransition 对象,每个对象都接受可选参数以自定义其行为。如需了解详情,请参阅参考资料。

EnterTransitionExitTransition 示例

EnterTransition ExitTransition
fadeIn
fade in animation
fadeOut
fade out animation
slideIn
slide in animation
slideOut
slide out animation
slideInHorizontally
slide in horizontally animation
slideOutHorizontally
slide out horizontally animation
slideInVertically
slide in vertically animation
slideOutVertically
slide out vertically animation
scaleIn
scale in animation
scaleOut
scale out animation
expandIn
expand in animation
shrinkOut
shrink out animation
expandHorizontally
expand horizontally animation
shrinkHorizontally
shrink horizontally animation
expandVertically
expand vertically animation
shrinkVertically
shrink vertically animation

AnimatedVisibility 还提供了一个采用 MutableTransitionState 的变体。这使您可以在 AnimatedVisibility 添加到组合树时立即触发动画。它对于观察动画状态也很有用。

// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
    MutableTransitionState(false).apply {
        // Start the animation immediately.
        targetState = true
    }
}
Column {
    AnimatedVisibility(visibleState = state) {
        Text(text = "Hello, world!")
    }

    // Use the MutableTransitionState to know the current animation state
    // of the AnimatedVisibility.
    Text(
        text = when {
            state.isIdle && state.currentState -> "Visible"
            !state.isIdle && state.currentState -> "Disappearing"
            state.isIdle && !state.currentState -> "Invisible"
            else -> "Appearing"
        }
    )
}

为子级添加进入和退出动画

AnimatedVisibility 中的内容(直接或间接子级)可以使用 animateEnterExit 修饰符,为每个子级指定不同的动画行为。每个子级的视觉效果是 AnimatedVisibility 可组合项中指定的动画与子级自己的进入和退出动画的组合。

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) {
    // Fade in/out the background and the foreground.
    Box(
        Modifier
            .fillMaxSize()
            .background(Color.DarkGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .animateEnterExit(
                    // Slide in/out the inner box.
                    enter = slideInVertically(),
                    exit = slideOutVertically()
                )
                .sizeIn(minWidth = 256.dp, minHeight = 64.dp)
                .background(Color.Red)
        ) {
            // Content of the notification…
        }
    }
}

在某些情况下,您可能希望 AnimatedVisibility 完全不应用动画,以便每个子级可以通过 animateEnterExit 拥有自己的独特动画。要实现此目的,请在 AnimatedVisibility 可组合项中指定 EnterTransition.NoneExitTransition.None

添加自定义动画

如果您想添加超出内置进入和退出动画的自定义动画效果,可以通过 AnimatedVisibility 的内容 lambda 中的 transition 属性访问底层 Transition 实例。添加到 Transition 实例的任何动画状态将与 AnimatedVisibility 的进入和退出动画同时运行。AnimatedVisibility 会等待 Transition 中的所有动画完成后再移除其内容。对于独立于 Transition 创建的退出动画(例如使用 animate*AsState),AnimatedVisibility 无法考虑这些动画,因此可能会在它们完成之前移除内容可组合项。

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) { // this: AnimatedVisibilityScope
    // Use AnimatedVisibilityScope#transition to add a custom animation
    // to the AnimatedVisibility.
    val background by transition.animateColor(label = "color") { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(
        modifier = Modifier
            .size(128.dp)
            .background(background)
    )
}

有关 Transition 的详细信息,请参阅 updateTransition

使用 AnimatedContent 根据目标状态添加动画

AnimatedContent 可组合项会根据目标状态的变化为其内容添加动画效果。

Row {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        label = "animated content"
    ) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

请注意,您应该始终使用 lambda 参数并将其反映到内容中。API 使用此值作为键来标识当前显示的内容。

默认情况下,初始内容会淡出,然后目标内容会淡入(此行为称为淡入淡出)。您可以通过将 ContentTransform 对象指定给 transitionSpec 参数来自定义此动画行为。您可以通过使用 with 中缀函数将 EnterTransitionExitTransition 组合来创建 ContentTransform。您可以通过使用 using 中缀函数将其附加到 ContentTransform 来应用 SizeTransform

AnimatedContent(
    targetState = count,
    transitionSpec = {
        // Compare the incoming number with the previous number.
        if (targetState > initialState) {
            // If the target number is larger, it slides up and fades in
            // while the initial (smaller) number slides up and fades out.
            slideInVertically { height -> height } + fadeIn() togetherWith
                slideOutVertically { height -> -height } + fadeOut()
        } else {
            // If the target number is smaller, it slides down and fades in
            // while the initial number slides down and fades out.
            slideInVertically { height -> -height } + fadeIn() togetherWith
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }, label = "animated content"
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition 定义了目标内容应如何出现,而 ExitTransition 定义了初始内容应如何消失。除了 AnimatedVisibility 可用的所有 EnterTransitionExitTransition 函数之外,AnimatedContent 还提供了 slideIntoContainerslideOutOfContainer。这些是 slideInHorizontally/VerticallyslideOutHorizontally/Vertically 的便捷替代方案,它们根据 AnimatedContent 内容的初始内容和目标内容的尺寸计算滑动距离。

SizeTransform 定义了尺寸应如何在初始内容和目标内容之间进行动画处理。在创建动画时,您可以同时访问初始尺寸和目标尺寸。SizeTransform 还控制动画期间内容是否应剪裁到组件尺寸。

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) togetherWith
                fadeOut(animationSpec = tween(150)) using
                SizeTransform { initialSize, targetSize ->
                    if (targetState) {
                        keyframes {
                            // Expand horizontally first.
                            IntSize(targetSize.width, initialSize.height) at 150
                            durationMillis = 300
                        }
                    } else {
                        keyframes {
                            // Shrink vertically first.
                            IntSize(initialSize.width, targetSize.height) at 150
                            durationMillis = 300
                        }
                    }
                }
        }, label = "size transform"
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

为子级添加进入和退出过渡动画

就像 AnimatedVisibility 一样,animateEnterExit 修饰符在 AnimatedContent 的内容 lambda 中可用。使用此修饰符可分别为每个直接或间接子级应用 EnterAnimationExitAnimation

添加自定义动画

就像 AnimatedVisibility 一样,transition 字段在 AnimatedContent 的内容 lambda 中可用。使用此字段创建与 AnimatedContent 过渡同时运行的自定义动画效果。有关详细信息,请参阅 updateTransition

使用 Crossfade 在两个布局之间交叉淡入淡出

Crossfade 使用交叉淡入淡出动画在两个布局之间进行切换。通过切换传递给 current 参数的值,内容会以交叉淡入淡出动画进行切换。

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, label = "cross fade") { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

内置动画修饰符

使用 animateContentSize 为可组合项大小变化添加动画

Green composable animating its size change smoothly.
图 2. 可组合项在小尺寸和大尺寸之间平滑动画

animateContentSize 修饰符可为尺寸变化添加动画。

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

列表项动画

如果您希望在 Lazy 列表或网格中为项目重新排序添加动画效果,请查看Lazy 布局项目动画文档