使用 animate*AsState
为单个值添加动画效果
animate*AsState
函数是 Compose 中最简单的动画 API,用于为单个值添加动画效果。您只需提供目标值(或结束值),该 API 就会从当前值开始向指定值播放动画。
下面举例说明了如何使用此 API 为 alpha 添加动画效果。只需将目标值封装在 animateFloatAsState
中,alpha 值就会变成介于所提供的值(在本例中为 1f
或 0.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 为 Float
、Color
、Dp
、Size
、Offset
、Rect
、Int
、IntOffset
和 IntSize
提供开箱即用的 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
相同。这可以用作指示是否已完成过渡的信号。
有时,我们会希望初始状态与第一个目标状态不同。我们可以通过结合使用 updateTransition
和 MutableTransitionState
来实现这一点。例如,它允许我们在代码进入组合阶段后立即开始播放动画。
// 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 } ) } }
将 transition 与 AnimatedVisibility
和 AnimatedContent
搭配使用
AnimatedVisibility
和 AnimatedContent
可用作 Transition
的扩展函数。Transition.AnimatedVisibility
和 Transition.AnimatedContent
的 targetState
源自 Transition
,会在 Transition
的 targetState
发生变化时视需要触发进入/退出过渡。这些扩展函数允许将原本位于 AnimatedVisibility
或 AnimatedContent
内的所有 enter/exit/sizeTransform 动画提升到 Transition
中。借助这些扩展函数,可以从外部观察 AnimatedVisibility
/AnimatedContent
的状态变化。此版本的 AnimatedVisibility
接受一个 lambda,它将父过渡的目标状态转换为布尔值,而不是接受布尔值 visible
参数。
如需了解详情,请参阅 AnimatedVisibility 和 AnimatedContent。
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
实例。可以使用 animateColor
、animatedFloat
或 animatedValue
添加子动画。您还需要指定 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
互动,但 Animation
的某些自定义功能可以通过更高级别的 API 获得。如需详细了解 AnimationVector
和 AnimationSpec
,请参阅自定义动画。
Animatable
:基于协程的单值动画
Animatable
是一个值容器,它可以在通过 animateTo
更改值时为值添加动画效果。该 API 支持 animate*AsState
的实现。它可确保一致的连续性和互斥性,这意味着值变化始终是连续的,并且会取消任何正在播放的动画。
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.Gray
的 Animatable
实例。根据布尔标记 ok
的值,颜色将以动画形式呈现 Color.Green
或 Color.Red
。对该布尔值的任何后续更改都会使动画开始使用另一种颜色。如果更改该值时有正在播放的动画,系统会取消该动画,并且新动画将以当前速度从当前快照值开始播放。
这种动画实现支持上一部分提到的 animate*AsState
API。与 animate*AsState
相比,使用 Animatable
可以直接对以下几个方面进行更精细的控制。首先,Animatable
的初始值可以与第一个目标值不同。例如,上面的代码示例首先显示一个灰色框,然后立即开始通过动画呈现为绿色或红色。其次,Animatable
对内容值提供更多操作(即 snapTo
和 animateDecay
)。snapTo
可立即将当前值设为目标值。如果动画本身不是唯一的可信来源,且必须与其他状态(如触摸事件)同步,该函数就非常有用。animateDecay
用于启动播放从给定速度变慢的动画。这有助于实现投掷行为。如需了解详情,请参阅手势和动画。
Animatable
为 Float
和 Color
提供开箱即用的支持,不过,通过提供 TwoWayConverter
,可使用任何数据类型。如需了解详情,请参阅 AnimationVector。
您可以通过提供 AnimationSpec
来自定义动画规范。如需了解详情,请参阅 AnimationSpec。
Animation
:手动控制的动画
Animation
是可用的最低级别的 Animation API。到目前为止,我们看到的许多动画都是基于 Animation 构建的。Animation
子类型有两种:TargetBasedAnimation
和 DecayAnimation
。
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
,而是根据起始条件(由 initialVelocity
和 initialValue
设置)以及所提供的 DecayAnimationSpec
计算其 targetValue
。
衰减动画通常在快滑手势之后使用,用于使元素减速并停止。动画速度从 initialVelocityVector
设置的值开始,然后逐渐变慢。
为您推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 自定义动画 {:#customize-animations}
- Compose 中的动画
- 动画修饰符和可组合项