基于价值的动画

本页面介绍了如何在 Jetpack Compose 中创建基于值的动画,重点介绍了根据值的当前状态和目标状态来设置动画的 API。

使用 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 同时为多个属性添加动画效果

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 相同。这可以用作指示是否已完成过渡的信号。

有时,您可能希望初始状态与第一个目标状态不同。您可以将 updateTransitionMutableTransitionState 搭配使用来实现此目的。例如,它允许您在代码进入组合阶段后立即开始播放动画。

// 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 发生变化时视需要触发进入、退出和 sizeTransform 动画。这些扩展函数可让您将原本位于 AnimatedVisibility/AnimatedContent 内的所有进入、退出和 sizeTransform 动画提升到 Transition 中。借助这些扩展函数,您可以从外部观察 AnimatedVisibility/AnimatedContent 的状态变化。此版本的 AnimatedVisibility 接受一个 lambda,它将父过渡的目标状态转换为布尔值,而不是接受布尔值 visible 参数。

如需了解详情,请参阅 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")
            }
        }
    }
}

封装 Transition 并使其可重复使用

对于简单的用例,在与界面相同的可组合项中定义过渡动画是一种有效的选择方案。不过,在处理具有大量动画值的复杂组件时,您可能会希望将动画实现与可组合界面分开。

为此,您可以创建一个类来保存所有动画值,同时创建一个 update 函数来返回该类的实例。您可以将过渡实现提取到新的独立函数中。当您需要集中处理动画逻辑或使复杂动画可重复使用时,这种模式很有用。

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 创建无限重复的动画

InfiniteTransition 可以像 Transition 一样保存一个或多个子动画,但是,这些动画一进入组合阶段就开始运行,除非被移除,否则不会停止。您可以使用 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 与其类似,不过,它会创建一个无限过渡对象,以管理多个无限期运行的动画。所有这些 API 都是可组合项(Animatable 除外),这意味着您可以在非组合期间创建这些动画。

所有这些 API 都基于更基础的 Animation API。虽然大多数应用不会直接与 Animation 互动,但您可以通过更高级别的 API 访问其某些自定义功能。如需详细了解 AnimationVectorAnimationSpec,请参阅自定义动画

低级别动画 API 之间的关系
图 1. 低级别动画 API 之间的关系。

Animatable:基于协程的单值动画

Animatable 是一个值容器,它可以在使用 animateTo 更改值时为值添加动画效果。该 API 支持 animate*AsState 的实现。它可确保一致的连续性和互斥性,这意味着值变化始终是连续的,并且 Compose 会取消任何正在播放的动画。

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)
)

在上面的示例中,您创建并记住了初始值为 Color.GrayAnimatable 实例。根据布尔标记 ok 的值,颜色将以动画形式呈现 Color.GreenColor.Red。对该布尔值的任何后续更改都会使动画开始使用另一种颜色。 如果更改该值时有正在播放的动画,Compose 会取消该动画,并且新动画将以当前速度从当前快照值开始播放。

Animatable API 是上一部分中提到的 animate*AsState 的底层实现。直接使用 Animatable 可通过多种方式实现更精细的控制:

  • 首先,Animatable 的初始值可以与第一个目标值不同。例如,上面的代码示例首先显示一个灰色框,然后立即通过动画呈现为绿色或红色。
  • 其次,Animatable 对内容值提供更多操作,即 snapToanimateDecay
    • snapTo 可立即将当前值设为目标值。如果动画不是唯一的可信来源,且必须与其他状态(如触摸事件)同步,该函数就非常有用。
    • animateDecay 用于启动播放从给定速度变慢的动画。这有助于实现投掷行为。

如需了解详情,请参阅手势和动画

默认情况下,Animatable 支持 FloatColor,不过,通过提供 TwoWayConverter,您可以使用任何数据类型。如需了解详情,请参阅 AnimationVector

您可以通过提供 AnimationSpec 来自定义动画规范。如需了解详情,请参阅 AnimationSpec

Animation:手动控制的动画

Animation 是可用的最低级别的动画 API。到目前为止,我们看到的许多动画都是基于 Animation 构建的。Animation 子类型有两种:TargetBasedAnimationDecayAnimation

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

TargetBasedAnimation

其他 API 可满足大多数用例的需要,但使用 TargetBasedAnimation 可以直接让您控制动画的播放时间。在以下示例中,您将根据 withFrameNanos 提供的帧时间手动控制 TargetAnimation 的播放时间。

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 设置的值开始,然后逐渐变慢。