使用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
将一个或多个动画管理为其子项,并在多个状态之间同时运行它们。
状态可以是任何数据类型。在许多情况下,您可以使用自定义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
相同。这可以用作转换是否已完成的信号。
有时我们希望初始状态与第一个目标状态不同。我们可以使用带有MutableTransitionState
的updateTransition
来实现这一点。例如,它允许我们在代码进入组合时立即启动动画。
// 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 } ) } }
将转换与AnimatedVisibility
和AnimatedContent
一起使用
AnimatedVisibility
和AnimatedContent
可作为Transition
的扩展函数使用。Transition.AnimatedVisibility
和Transition.AnimatedContent
的targetState
源自Transition
,并在Transition
的targetState
发生更改时根据需要触发进入/退出转换。这些扩展函数允许所有进入/退出/sizeTransform 动画(否则将是AnimatedVisibility
/AnimatedContent
的内部动画)提升到Transition
中。使用这些扩展函数,可以从外部观察AnimatedVisibility
/AnimatedContent
的状态变化。此版本的AnimatedVisibility
不使用布尔值visible
参数,而是使用一个 lambda 表达式,将父转换的目标状态转换为布尔值。
有关详细信息,请参阅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") } } } }
封装转换并使其可重用
对于简单的用例,在与 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
创建无限重复动画
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
支持,Animatable
是一个基于协程的 API,用于动画化单个值。updateTransition
创建一个转换对象,该对象可以管理多个动画值并根据状态更改运行它们。rememberInfiniteTransition
类似,但它创建了一个无限转换,可以管理持续运行的多个动画。所有这些 API 都是可组合的,除了Animatable
,这意味着这些动画可以在组合之外创建。
所有这些 API 都基于更基本的Animation
API。尽管大多数应用程序不会直接与Animation
交互,但Animation
的一些自定义功能可通过更高级别的 API 获得。有关AnimationVector
和AnimationSpec
的更多信息,请参阅自定义动画。
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.Green
或 Color.Red
。对布尔值的任何后续更改都会启动动画到另一种颜色。如果在更改值时有正在进行的动画,则动画将被取消,新的动画将从当前快照值和当前速度开始。
这是上一节中提到的 animate*AsState
API 背后的动画实现。与 animate*AsState
相比,直接使用 Animatable
可以让我们在几个方面获得更细粒度的控制。首先,Animatable
可以具有与其第一个目标值不同的初始值。例如,上面的代码示例首先显示一个灰色框,然后立即开始动画到绿色或红色。其次,Animatable
提供了更多对内容值的运算,即 snapTo
和 animateDecay
。snapTo
会立即将当前值设置为目标值。当动画本身不是唯一的事实来源并且必须与其他状态(例如触摸事件)同步时,这很有用。animateDecay
启动一个从给定速度减速的动画。这对于实现抛掷行为很有用。有关详细信息,请参阅 手势和动画。
Animatable
原生支持 Float
和 Color
,但可以通过提供 TwoWayConverter
来使用任何数据类型。有关详细信息,请参阅 AnimationVector。
您可以通过提供 AnimationSpec
来自定义动画规范。有关详细信息,请参阅 AnimationSpec。
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 中的动画
- 动画修饰符和可组合项