基于值的动画

使用 animate*AsState 动画单个值

animate*AsState 函数是 Compose 中用于动画单个值的最简单动画 API。您只需提供目标值(或结束值),API 就会开始从当前值到指定值的动画。

以下是使用此 API 动画 alpha 值的示例。只需将目标值封装在 animateFloatAsState 中,alpha 值现在就是所提供值(在本例中为 1f0.5f)之间的动画值。

var enabled by remember { mutableStateOf(true) }

val animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer { alpha = animatedAlpha }
        .background(Color.Red)
)

请注意,您无需创建任何动画类的实例,也无需处理中断。在底层,动画对象(即 Animatable 实例)将在调用站点创建并记住,第一个目标值作为其初始值。从那时起,每当您向此可组合项提供不同的目标值时,动画都会自动开始朝该值移动。如果已有动画正在进行中,则动画会从其当前值(和速度)开始,并朝目标值动画。在动画期间,此可组合项会重新组合,并在每帧返回更新的动画值。

开箱即用,Compose 为 FloatColorDpSizeOffsetRectIntIntOffsetIntSize 提供了 animate*AsState 函数。您可以通过向接受泛型类型的 animateValueAsState 提供 TwoWayConverter,轻松添加对其他数据类型的支持。

您可以通过提供 AnimationSpec 来自定义动画规格。有关详细信息,请参阅 AnimationSpec

使用过渡同时动画多个属性

Transition 管理一个或多个动画作为其子项,并在多个状态之间同时运行它们。

状态可以是任何数据类型。在许多情况下,您可以使用自定义 enum 类型来确保类型安全,如本例所示:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition 创建并记住一个 Transition 实例并更新其状态。

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "box state")

然后,您可以使用其中一个 animate* 扩展函数来定义此过渡中的子动画。指定每个状态的目标值。这些 animate* 函数返回一个动画值,该值在动画期间每帧更新一次,当过渡状态通过 updateTransition 更新时。

val rect by transition.animateRect(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

或者,您可以传入 transitionSpec 参数,为每个过渡状态变化组合指定不同的 AnimationSpec。有关详细信息,请参阅 AnimationSpec

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

一旦过渡到达目标状态,Transition.currentState 将与 Transition.targetState 相同。这可以用作过渡是否完成的信号。

我们有时希望有一个不同于第一个目标状态的初始状态。我们可以使用带有 MutableTransitionStateupdateTransition 来实现这一点。例如,它允许我们一进入组合就立即开始动画。

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

对于涉及多个可组合函数的更复杂过渡,您可以使用 createChildTransition 来创建子过渡。此技术对于在复杂可组合项中的多个子组件之间分离关注点非常有用。父过渡将了解子过渡中的所有动画值。

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

将过渡与 AnimatedVisibilityAnimatedContent 结合使用

AnimatedVisibilityAnimatedContent 作为 Transition 的扩展函数提供。Transition.AnimatedVisibilityTransition.AnimatedContenttargetState 源自 Transition,并在 TransitiontargetState 发生更改时根据需要触发进入/退出过渡。这些扩展函数允许将原本在 AnimatedVisibility/AnimatedContent 内部的所有进入/退出/大小变换动画提升到 Transition 中。通过这些扩展函数,可以从外部观察 AnimatedVisibility/AnimatedContent 的状态变化。此版本的 AnimatedVisibility 不接受布尔值 visible 参数,而是接受一个 lambda,该 lambda 将父过渡的目标状态转换为布尔值。

有关详细信息,请参阅 AnimatedVisibilityAnimatedContent

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

封装过渡并使其可重用

对于简单用例,在与您的 UI 相同的可组合项中定义过渡动画是一个完全有效的选项。但是,当您处理包含多个动画值的复杂组件时,您可能希望将动画实现与可组合 UI 分离。

您可以通过创建包含所有动画值和一个返回该类实例的“更新”函数的类来实现此目的。过渡实现可以提取到新的独立函数中。当需要集中动画逻辑或使复杂动画可重用时,此模式非常有用。

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

使用 rememberInfiniteTransition 创建无限重复动画

InfiniteTransitionTransition 一样包含一个或多个子动画,但动画在进入组合后立即开始运行,并且除非被移除,否则不会停止。您可以使用 rememberInfiniteTransition 创建 InfiniteTransition 实例。可以使用 animateColoranimatedFloatanimatedValue 添加子动画。您还需要指定 infiniteRepeatable 以指定动画规格。

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

低级动画 API

前面部分中提到的所有高级动画 API 都建立在低级动画 API 的基础之上。

animate*AsState 函数是最简单的 API,它将即时值更改呈现为动画值。它由 Animatable 支持,这是一个基于协程的 API,用于动画单个值。updateTransition 创建一个过渡对象,该对象可以管理多个动画值并根据状态变化运行它们。rememberInfiniteTransition 类似,但它创建了一个无限过渡,可以管理多个无限期运行的动画。除了 Animatable,所有这些 API 都是可组合的,这意味着这些动画可以在组合之外创建。

所有这些 API 都基于更基础的 Animation API。尽管大多数应用不会直接与 Animation 交互,但 Animation 的一些自定义功能可通过更高级别的 API 获得。有关 AnimationVectorAnimationSpec 的更多信息,请参阅 自定义动画

Diagram showing the relationship between the various low-level animation APIs

Animatable:基于协程的单值动画

Animatable 是一个值持有者,可以在通过 animateTo 更改值时动画该值。这是支持 animate*AsState 实现的 API。它确保一致的延续性和互斥性,这意味着值更改始终是连续的,并且任何正在进行的动画都将被取消。

Animatable 的许多功能,包括 animateTo,都作为挂起函数提供。这意味着它们需要封装在适当的协程范围内。例如,您可以使用 LaunchedEffect 可组合项来创建一个仅在指定键值持续时间内有效的范围。

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

在上面的示例中,我们创建并记住了 Animatable 的一个实例,初始值为 Color.Gray。根据布尔标志 ok 的值,颜色动画到 Color.GreenColor.Red。布尔值的任何后续更改都会开始动画到另一种颜色。如果在值更改时有正在进行的动画,则该动画将被取消,新动画将从当前快照值以当前速度开始。

这是支持前面部分中提到的 animate*AsState API 的动画实现。与 animate*AsState 相比,直接使用 Animatable 在几个方面提供了更精细的控制。首先,Animatable 可以具有与第一个目标值不同的初始值。例如,上面的代码示例首先显示一个灰色框,然后立即开始动画到绿色或红色。其次,Animatable 提供更多关于内容值的操作,即 snapToanimateDecaysnapTo 立即将当前值设置为目标值。当动画本身不是唯一的真相来源并且必须与其他状态(例如触摸事件)同步时,这很有用。animateDecay 开始一个从给定速度减速的动画。这对于实现轻扫行为很有用。有关详细信息,请参阅 手势和动画

开箱即用,Animatable 支持 FloatColor,但可以通过提供 TwoWayConverter 来使用任何数据类型。有关详细信息,请参阅 AnimationVector

您可以通过提供 AnimationSpec 来自定义动画规格。有关详细信息,请参阅 AnimationSpec

Animation:手动控制的动画

Animation 是可用的最低级动画 API。我们目前看到的大多数动画都建立在 Animation 之上。Animation 有两种子类型:TargetBasedAnimationDecayAnimation

Animation 仅应用于手动控制动画的时间。Animation 是无状态的,它没有任何生命周期概念。它充当高级 API 使用的动画计算引擎。

TargetBasedAnimation

其他 API 涵盖了大多数用例,但直接使用 TargetBasedAnimation 允许您自行控制动画播放时间。在下面的示例中,TargetAnimation 的播放时间是根据 withFrameNanos 提供的帧时间手动控制的。

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

TargetBasedAnimation 不同,DecayAnimation 不需要提供 targetValue。相反,它根据由 initialVelocityinitialValue 以及提供的 DecayAnimationSpec 设置的起始条件计算其 targetValue

衰减动画通常在轻扫手势后用于使元素减速停止。动画速度从 initialVelocityVector 设置的值开始,并随时间减慢。