动画修饰符和可组合函数

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中缀函数附加SizeTransformContentTransform

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