基于值的动画

使用 animate*AsState 动画单个值

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

下面是一个使用此 API 动画 alpha 的示例。只需将目标值包装在 animateFloatAsState 中,alpha 值现在成为在提供的两个值之间进行动画的值(在本例中为 1f0.5f)。

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
    Modifier.fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .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 相同。这可以用作过渡是否已完成的信号。

有时我们希望初始状态与第一个目标状态不同。我们可以使用 updateTransitionMutableTransitionState 来实现这一点。例如,它允许我们在代码进入组合时立即启动动画。

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(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 的状态更改。与布尔型 visible 参数不同,此版本的 AnimatedVisibility 接受一个 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),
    elevation = 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()
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)

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

低级动画 API

上一节中提到的所有高级动画 API 都是基于低级动画 API 的基础构建的。

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

所有这些 API 都是基于更基础的 Animation API。虽然大多数应用程序不会直接与 Animation 交互,但一些 Animation 的自定义功能可以通过更高级的 API 使用。有关 AnimationVectorAnimationSpec 的更多信息,请参阅 Customize animations

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

Animatable:基于协程的单值动画

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

包括 animateTo 在内的 Animatable 的许多功能都是作为挂起函数提供的。这意味着它们需要包装在适当的协程作用域中。例如,您可以使用 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 会启动一个从给定速度减速的动画。这对于实现甩动行为很有用。有关更多信息,请参阅 Gesture and animation

开箱即用,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 { mutableStateOf(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 设置的值开始,并随着时间的推移逐渐减慢。