动画修饰符和可组合项

Compose 附带用于处理常见动画用例的内置可组合项和修饰符。

内置动画可组合项

使用 AnimatedVisibility 动画化外观和消失

Green composable showing and hiding itself
图 1. 在列中动画化项目的出现和消失

The 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 动画化

The AnimatedContent 可组合项动画化其内容,因为它根据目标状态而改变。

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

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

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

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() with
                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() with
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }
) { 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)) with
                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
                        }
                    }
                }
        }
    ) { 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) { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

内置动画修饰符

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

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

The 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 布局项目动画文档