使用 animate*AsState
动画单个值
animate*AsState
函数是 Compose 中用于动画单个值的最简单动画 API。您只需提供目标值(或结束值),API 就会开始从当前值到指定值的动画。
以下是使用此 API 动画 alpha 值的示例。只需将目标值封装在 animateFloatAsState
中,alpha 值现在就是所提供值(在本例中为 1f
或 0.5f
)之间的动画值。
var enabled by remember { mutableStateOf(true) } val animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha") Box( Modifier .fillMaxSize() .graphicsLayer { alpha = animatedAlpha } .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
相同。这可以用作过渡是否完成的信号。
我们有时希望有一个不同于第一个目标状态的初始状态。我们可以使用带有 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
发生更改时根据需要触发进入/退出过渡。这些扩展函数允许将原本在 AnimatedVisibility
/AnimatedContent
内部的所有进入/退出/大小变换动画提升到 Transition
中。通过这些扩展函数,可以从外部观察 AnimatedVisibility
/AnimatedContent
的状态变化。此版本的 AnimatedVisibility
不接受布尔值 visible
参数,而是接受一个 lambda,该 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
支持,这是一个基于协程的 API,用于动画单个值。updateTransition
创建一个过渡对象,该对象可以管理多个动画值并根据状态变化运行它们。rememberInfiniteTransition
类似,但它创建了一个无限过渡,可以管理多个无限期运行的动画。除了 Animatable
,所有这些 API 都是可组合的,这意味着这些动画可以在组合之外创建。
所有这些 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 中的动画
- 动画修饰符和可组合项