使用 animate*AsState
动画单个值
在 Compose 中,animate*AsState
函数是用于动画单个值的简单动画 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) 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*
扩展函数在此过渡中定义子动画。为每个状态指定目标值。这些 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 = 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 } ) } }
将过渡与 AnimatedVisibility
和 AnimatedContent
一起使用
AnimatedVisibility
和 AnimatedContent
可作为 Transition
的扩展函数使用。Transition.AnimatedVisibility
和 Transition.AnimatedContent
的 targetState
来自 Transition
,并在 Transition
的 targetState
发生更改时,根据需要触发进入/退出过渡。这些扩展函数允许所有原本是 AnimatedVisibility
/AnimatedContent
内部使用的进入/退出/大小转换动画被提升到 Transition
中。使用这些扩展函数,可以从外部观察 AnimatedVisibility
/AnimatedContent
的状态更改。与布尔型 visible
参数不同,此版本的 AnimatedVisibility
接受一个 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), 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
创建无限重复的动画
InfiniteTransition
与 Transition
一样,包含一个或多个子动画,但这些动画会在它们进入组合时立即开始运行,并且除非它们被移除,否则不会停止。您可以使用 rememberInfiniteTransition
创建 InfiniteTransition
的实例。可以使用 animateColor
、animatedFloat
或 animatedValue
添加子动画。您还需要指定一个 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 使用。有关 AnimationVector
和 AnimationSpec
的更多信息,请参阅 Customize animations。
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.Green
或 Color.Red
。对布尔值的所有后续更改都会启动到另一个颜色的动画。如果在值更改时有正在进行的动画,则该动画将被取消,新的动画将从当前快照值和当前速度开始。
这是支持上一节中提到的 animate*AsState
API 的动画实现。与 animate*AsState
相比,直接使用 Animatable
使我们在几个方面获得了更细粒度的控制。首先,Animatable
可以有一个与第一个目标值不同的初始值。例如,上面的代码示例显示了一个最初为灰色的方框,它会立即开始动画到绿色或红色。其次,Animatable
为内容值提供了更多操作,即 snapTo
和 animateDecay
。snapTo
会立即将当前值设置为目标值。当动画本身不是唯一的真相来源并且必须与其他状态(如触摸事件)同步时,这很有用。animateDecay
会启动一个从给定速度减速的动画。这对于实现甩动行为很有用。有关更多信息,请参阅 Gesture and animation。
开箱即用,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 { mutableStateOf(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 中的动画
- 动画修饰符和可组合项