基于值的动画

使用animate*AsState动画化单个值

animate*AsState 函数是 Compose 中用于动画化单个值的简单动画 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, label = "alpha")
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* 扩展函数之一在此转换中定义子动画。为每个状态指定目标值。当使用updateTransition 更新转换状态时,这些animate* 函数会在动画期间的每一帧返回一个更新的动画值。

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 发生更改时根据需要触发进入/退出转换。这些扩展函数允许所有进入/退出/sizeTransform 动画(否则将是AnimatedVisibility/AnimatedContent 的内部动画)提升到Transition 中。使用这些扩展函数,可以从外部观察AnimatedVisibility/AnimatedContent 的状态变化。此版本的AnimatedVisibility 不使用布尔值visible 参数,而是使用一个 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 支持,Animatable 是一个基于协程的 API,用于动画化单个值。updateTransition 创建一个转换对象,该对象可以管理多个动画值并根据状态更改运行它们。rememberInfiniteTransition 类似,但它创建了一个无限转换,可以管理持续运行的多个动画。所有这些 API 都是可组合的,除了Animatable,这意味着这些动画可以在组合之外创建。

所有这些 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 设置的值开始,并随着时间的推移逐渐减慢。