Jetpack Compose 提供了一些功能强大且可扩展的 API,可用于在应用界面中轻松实现各种动画效果。本文档将介绍如何使用这些 API,以及根据您的动画场景应使用哪种 API。
概览
动画在现代移动应用中至关重要,其目的是实现自然流畅、易于理解的用户体验。许多 Jetpack Compose 动画 API 可以提供可组合函数,就像布局和其他界面元素一样;它们由使用 Kotlin 协程挂起函数构建的较低级别 API 提供支持。本指南将首先介绍可用于许多实际场景的高级别 API,接着介绍可为您提供进一步控制和自定义功能的低级别 API。
下面的图表可以帮助您确定要使用哪种 API 来实现您的动画效果。
- 如果您要为布局中的内容变化添加动画效果:
- 如果您要为出现和消失添加动画效果:
- 根据状态交换内容:
- 如果您要为内容添加淡入淡出效果:
- 使用
Crossfade
。
- 使用
- 否则使用
AnimatedContent
。
- 如果您要为内容添加淡入淡出效果:
- 否则使用
Modifier.animateContentSize
。
- 如果动画效果基于状态:
- 如果在组合期间呈现动画效果:
- 如果动画效果无限循环:
- 如果您要同时为多个值添加动画效果:
- 使用
updateTransition
。
- 使用
- 否则使用
animate*AsState
。
- 如果在组合期间呈现动画效果:
- 如果您要对动画播放时间进行精细控制:
- 使用
Animation
,例如TargetBasedAnimation
或DecayAnimation
。
- 使用
- 如果动画是唯一可信来源:
- 使用
Animatable
。
- 使用
- 否则,使用
AnimationState
或animate
。
高级别动画 API
Compose 为许多应用中常用的几种动画模式提供了高级别动画 API。这些 API 经过专门设计,符合 Material Design 运动的最佳实践。
AnimatedVisibility
AnimatedVisibility
可组合项可为内容的出现和消失添加动画效果。
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
默认情况下,内容以淡入和扩大的方式出现,以淡出和缩小的方式消失。您可以通过指定 EnterTransition
和 ExitTransition
来自定义这种过渡效果。
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
// Slide in from 40 dp from the top.
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// Expand from the top.
expandFrom = Alignment.Top
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Text("Hello", Modifier.fillMaxWidth().height(200.dp))
}
如上面的示例所示,您可以使用 +
运算符组合多个 EnterTransition
或 ExitTransition
对象,并且每个对象都接受可选参数以自定义其行为。如需了解详情,请参阅相关参考资料。
EnterTransition
和 ExitTransition
示例
AnimatedVisibility
还提供了接受 MutableTransitionState
的变体。这样,只要将 AnimatedVisibility
添加到组合树中,您就可以立即触发动画。该属性还有助于观察动画状态。
// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
MutableTransitionState(false).apply {
// Start the animation immediately.
targetState = true
}
}
Column {
AnimatedVisibility(visibleState = state) {
Text(text = "Hello, world!")
}
// Use the MutableTransitionState to know the current animation state
// of the AnimatedVisibility.
Text(
text = when {
state.isIdle && state.currentState -> "Visible"
!state.isIdle && state.currentState -> "Disappearing"
state.isIdle && !state.currentState -> "Invisible"
else -> "Appearing"
}
)
}
为子项添加进入和退出动画效果
AnimatedVisibility
中的内容(直接或间接子项)可以使用 animateEnterExit
修饰符为每个子项指定不同的动画行为。其中每个子项的视觉效果均由 AnimatedVisibility
可组合项中指定的动画与子项自己的进入和退出动画构成。
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
// Fade in/out the background and the foreground.
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(
Modifier
.align(Alignment.Center)
.animateEnterExit(
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)
.background(Color.Red)
) {
// Content of the notification…
}
}
}
在某些情况下,您可能希望 AnimatedVisibility
完全不应用任何动画,这样子项就可以通过 animateEnterExit
拥有各自的不同动画。如需实现此目标,请在 AnimatedVisibility
可组合项中指定 EnterTransition.None
和 ExitTransition.None
。
添加自定义动画
如果您想在内置进入和退出动画之外添加自定义动画效果,请通过 AnimatedVisibility
的内容 lambda 内的 transition
属性访问底层 Transition
实例。添加到 Transition 实例的所有动画状态都将与 AnimatedVisibility
的进入和退出动画同时运行。AnimatedVisibility
会等到 Transition
中的所有动画都完成后再移除其内容。对于独立于 Transition
(例如使用 animate*AsState
)创建的退出动画,AnimatedVisibility
将无法解释这些动画,因此可能会在完成之前移除内容可组合项。
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) { // this: AnimatedVisibilityScope
// Use AnimatedVisibilityScope#transition to add a custom animation
// to the AnimatedVisibility.
val background by transition.animateColor { state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Gray
}
Box(modifier = Modifier.size(128.dp).background(background))
}
如需了解关于 Transition
的详细信息,请参阅 updateTransition。
animate*AsState
animate*AsState
函数是 Compose 中最简单的动画 API,用于为单个值添加动画效果。您只需提供结束值(或目标值),该 API 就会从当前值开始向指定值播放动画。
下面举例说明了如何使用此 API 为 alpha 添加动画效果。您只需将目标值封装在 animateFloatAsState
中,alpha 值就会变成介于所提供的值(在本例中为 1f
或 0.5f
)之间的动画值。
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。
AnimatedContent(实验性)
AnimatedContent
可组合项会在内容根据目标状态发生变化时,为内容添加动画效果。
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(targetState = count) { targetCount ->
// Make sure to use `targetCount`, not `count`.
Text(text = "Count: $targetCount")
}
}
请注意,您应始终使用 lambda 参数并将其反映到内容中。API 会将此值用作键,以标识当前显示的内容。
默认情况下,初始内容淡出,然后目标内容淡入(此行为称为淡出后淡入)。您可以为 transitionSpec
参数指定 ContentTransform
对象,以自定义此动画行为。您可以使用 with
infix 函数来组合 EnterTransition
与 ExitTransition
,以创建 ContentTransform
。您可以将 SizeTransform
应用于 ContentTransform
,方法是为前者附加 using
infix 函数。
AnimatedContent(
targetState = count,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(text = "$targetCount")
}
EnterTransition
定义了目标内容应如何显示,ExitTransition
则定义了初始内容应如何消失。除了可用于 AnimatedVisibility
的所有 EnterTransition
和 ExitTransition
函数之外,AnimatedContent
还提供了 slideIntoContainer
和 slideOutOfContainer
。这些是 slideInHorizontally/Vertically
和 slideOutHorizontally/Vertically
的便捷替代方案,它们可根据初始内容的大小和 AnimatedContent
内容的目标内容来计算滑动距离。
SizeTransform
定义了大小应如何在初始内容与目标内容之间添加动画效果。在创建动画时,您可以访问初始大小和目标大小。SizeTransform
还可控制在动画播放期间是否应将内容裁剪为组件大小。
var expanded by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colors.primary,
onClick = { expanded = !expanded }
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
fadeOut(animationSpec = tween(150)) using
SizeTransform { initialSize, targetSize ->
if (targetState) {
keyframes {
// Expand horizontally first.
IntSize(targetSize.width, initialSize.height) at 150
durationMillis = 300
}
} else {
keyframes {
// Shrink vertically first.
IntSize(initialSize.width, targetSize.height) at 150
durationMillis = 300
}
}
}
}
) { targetExpanded ->
if (targetExpanded) {
Expanded()
} else {
ContentIcon()
}
}
}
为子项添加进入/退出动画
就像 AnimatedVisibility
一样,animateEnterExit
修饰符可以在 AnimatedContent
的内容 lambda 内使用。使用此修饰符可将 EnterAnimation
和 ExitAnimation
分别应用于每个直接或间接子项。
添加自定义动画
就像 AnimatedVisibility
一样,transition
字段可以在 AnimatedContent
的内容 lambda 内使用。使用此字段可创建与 AnimatedContent
过渡同时运行的自定义动画效果。如需了解详情,请参阅 updateTransition。
animateContentSize
animateContentSize
修饰符可为大小变化添加动画效果。
var message by remember { mutableStateOf("Hello") }
Box(
modifier = Modifier.background(Color.Blue).animateContentSize()
) {
Text(text = message)
}
Crossfade
Crossfade
可使用淡入淡出动画在两个布局之间添加动画效果。通过切换传递给 current
参数的值,可以使用淡入淡出动画来切换内容。
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
when (screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}
updateTransition
Transition
可管理一个或多个动画作为其子项,并在多个状态之间同时运行这些动画。
这里的状态可以是任何数据类型。在很多情况下,您可以使用自定义 enum
类型来确保类型安全,如下例所示:
enum class BoxState {
Collapsed,
Expanded
}
updateTransition
可创建并记住 Transition
的实例,并更新其状态。
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState)
然后,您可以使用某个 animate*
扩展函数来定义此过渡效果中的子动画。为每个状态指定目标值。这些 animate*
函数会返回一个动画值,在动画播放过程中,当使用 updateTransition
更新过渡状态时,该值将逐帧更新。
val rect by transition.animateRect { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
val borderWidth by transition.animateDp { 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)
}
}
) { state ->
when (state) {
BoxState.Collapsed -> MaterialTheme.colors.primary
BoxState.Expanded -> MaterialTheme.colors.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)
// ...
对于涉及多个可组合函数的更复杂的过渡,可使用 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)
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)
val borderColor by transition.animateColor { isSelected ->
if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp { 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")
}
}
}
}
封装 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)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { 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
,请参阅自定义动画。
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 { 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
设置的值开始,然后逐渐变慢。
自定义动画
很多动画 API 通常接受用于自定义其行为的参数。
AnimationSpec
大多数动画 API 允许开发者通过可选的 AnimationSpec
参数来自定义动画规范。
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)
开发者可以使用不同类型的 AnimationSpec
来创建不同类型的动画。
spring
spring
可在起始值和结束值之间创建基于物理特性的动画。它接受 2 个参数:dampingRatio
和 stiffness
。
dampingRatio
定义弹簧的弹性。默认值为 Spring.DampingRatioNoBouncy
。
stiffness
定义弹簧应向结束值移动的速度。默认值为 Spring.StiffnessMedium
。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
相比基于时长的 AnimationSpec
类型,spring
可以更流畅地处理中断,因为它可以在目标值在动画中变化时保证速度的连续性。spring
用作很多动画 API(如 animate*AsState
和 updateTransition
)的默认 AnimationSpec。
tween
tween
在指定的 durationMillis
内使用缓和曲线在起始值和结束值之间添加动画效果。如需了解详情,请参阅 Easing。您还可以指定 delayMillis
来推迟动画播放的开始时间。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
keyframes
keyframes
会根据在动画时长内的不同时间戳中指定的快照值添加动画效果。在任何给定时间,动画值都将插值到两个关键帧值之间。对于其中每个关键帧,您都可以指定 Easing 来确定插值曲线。
您可以选择在 0 毫秒和持续时间处指定值。如果不指定这些值,它们将分别默认为动画的起始值和结束值。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4f at 75 // ms
0.4f at 225 // ms
}
)
repeatable
repeatable
反复运行基于时长的动画(例如 tween
或 keyframes
),直至达到指定的迭代计数。您可以传递 repeatMode
参数来指定动画是从头开始 (RepeatMode.Restart
) 还是从结尾开始 (RepeatMode.Reverse
) 重复播放。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
infiniteRepeatable
infiniteRepeatable
与 repeatable
类似,但它会重复无限次的迭代。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
在使用 ComposeTestRule
的测试中,使用 infiniteRepeatable
的动画不会运行。系统将使用每个动画值的初始值来呈现组件。
snap
snap
是特殊的 AnimationSpec
,它会立即将值切换到结束值。您可以指定 delayMillis
来延迟动画播放的开始时间。
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = snap(delayMillis = 50)
)
Easing
基于时长的 AnimationSpec
操作(如 tween
或 keyframes
)使用 Easing
来调整动画的小数值。这样可让动画值加速和减速,而不是以恒定的速率移动。小数是介于 0(起始值)和 1.0(结束值)之间的值,表示动画中的当前点。
Easing 实际上是一个函数,它取一个介于 0 和 1.0 之间的小数值并返回一个浮点数。返回的值可能位于边界之外,表示过冲或下冲。您可以使用如下所示的代码创建一个自定义 Easing。
val CustomEasing = Easing { fraction -> fraction * fraction }
@Composable
fun EasingUsage() {
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = CustomEasing
)
)
// ...
}
Compose 提供多种内置 Easing
函数,可满足大多数用例的需要。如需详细了解根据您的情况应使用哪种 Easing,请参阅速度 - Material Design。
FastOutSlowInEasing
LinearOutSlowInEasing
FastOutLinearEasing
LinearEasing
CubicBezierEasing
- 查看更多
AnimationVector
大多数 Compose 动画 API 都支持将 Float
、Color
、Dp
以及其他基本数据类型作为开箱即用的动画值,但有时您需要为其他数据类型(包括您的自定义类型)添加动画效果。在动画播放期间,任何动画值都表示为 AnimationVector
。使用相应的 TwoWayConverter
即可将值转换为 AnimationVector
,反之亦然,这样一来,核心动画系统就可以统一对其进行处理。例如,Int
表示为包含单个浮点值的 AnimationVector1D
。用于 Int
的 TwoWayConverter
如下所示:
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
Color
实际上是 red、green、blue 和 alpha 这 4 个值的集合,因此,Color
可转换为包含 4 个浮点值的 AnimationVector4D
。通过这种方式,动画中使用的每种数据类型都可以根据其维度转换为 AnimationVector1D
、AnimationVector2D
、AnimationVector3D
或 AnimationVector4D
。这样可为对象的不同组件单独添加动画效果,且每个组件都有自己的速度跟踪。您可以使用 Color.VectorConverter
、Dp.VectorConverter
等访问针对基本数据类型的内置转换器。
如需支持作为动画值的新数据类型,您可以创建自己的 TwoWayConverter
并将其提供给 API。例如,您可以使用 animateValueAsState
为自定义数据类型添加动画效果,如下所示:
data class MySize(val width: Dp, val height: Dp)
@Composable
fun MyAnimation(targetSize: MySize) {
val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
targetSize,
TwoWayConverter(
convertToVector = { size: MySize ->
// Extract a float value from each of the `Dp` fields.
AnimationVector2D(size.width.value, size.height.value)
},
convertFromVector = { vector: AnimationVector2D ->
MySize(vector.v1.dp, vector.v2.dp)
}
)
)
}
动画形式的矢量资源(实验性)
如需使用 AnimatedVectorDrawable
资源,请使用 animatedVectorResource
加载可绘制对象文件,并传入 boolean
以在可绘制对象的开始和结束状态之间切换。
@Composable
fun AnimatedVectorDrawable() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = "Timer",
modifier = Modifier.clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Crop
)
}
如需详细了解可绘制文件的格式,请参阅为可绘制图形添加动画。
列表项动画
如果您希望为延迟列表或网格内的项重新排序操作添加动画效果,请参阅延迟布局项动画文档。
手势和动画(高级)
与单独处理动画相比,当我们处理触摸事件和动画时,必须考虑几个事项。首先,当触摸事件开始时,我们可能需要中断正在播放的动画,因为用户互动应当具有最高优先级。
在下面的示例中,我们使用 Animatable
表示圆形组件的偏移位置。触摸事件由 pointerInput
修饰符处理。当检测到新的点按事件时,我们将调用 animateTo
以将偏移值通过动画过渡到点按位置。在动画播放期间也可能发生点按事件。在这种情况下,animateTo
会中断正在播放的动画,启动动画以过渡到新的目标位置,同时保持被中断的动画的速度。
@Composable
fun Gesture() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope {
awaitFirstDown().position
}
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
) {
Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
另一种常见模式是需要将动画值与来自触摸事件(例如拖动)的值同步。在下面的示例中,我们会看到以 Modifier
的形式(而不是使用 SwipeToDismiss
可组合项)实现的“滑动关闭”。该元素的水平偏移量表示为 Animatable
。此 API 具有可用于手势动画的特征。其值可由触摸事件和动画更改。收到触摸事件时,我们通过 stop
方法停止 Animatable
,以便拦截任何正在播放的动画。
在拖动事件期间,我们使用 snapTo
将 Animatable
值更新为从触摸事件计算得出的值。对于快速滑动,Compose 可提供 VelocityTracker
来记录拖动事件并计算速度。速度可直接馈送至投掷动画的 animateDecay
。如需将偏移值滑回原始位置,可使用 animateTo
方法指定 0f
的目标偏移值。
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
测试
Compose 提供 ComposeTestRule
,可让您以确定性的方式编写动画测试,并完全控制测试时钟。这样,您就可以验证中间动画值。此外,测试的运行速度可能比动画的实际时长快。
ComposeTestRule
将其测试时钟公开为 mainClock
。您可以将 autoAdvance
属性设置为 false,以控制测试代码中的时钟。启动要测试的动画后,可以使用 advanceTimeBy
将时钟提前。
有一点需要注意,advanceTimeBy
不会按指定时长精确地移动时钟,而是向上舍入为最接近的时长(帧时长的倍数)。
@get:Rule
val rule = createComposeRule()
@Test
fun testAnimationWithClock() {
// Pause animations
rule.mainClock.autoAdvance = false
var enabled by mutableStateOf(false)
rule.setContent {
val color by animateColorAsState(
targetValue = if (enabled) Color.Red else Color.Green,
animationSpec = tween(durationMillis = 250)
)
Box(Modifier.size(64.dp).background(color))
}
// Initiate the animation.
enabled = true
// Let the animation proceed.
rule.mainClock.advanceTimeBy(50L)
// Compare the result with the image showing the expected result.
// `assertAgainGolden` needs to be implemented in your code.
rule.onRoot().captureToImage().assertAgainstGolden()
}
工具支持
Android Studio 支持在动画预览中检查 updateTransition
和 animatedVisibility
。您可以执行以下操作:
- 逐帧预览过渡效果
- 检查过渡效果中所有动画的值
- 预览任何初始状态与目标状态之间的过渡
- 一次检查和协调多个动画
启动 Animation Preview 后,您会看到“Animations”窗格,并且可以在其中运行预览中包含的任何过渡效果。过渡效果及其每个动画值均标有默认名称。您可以通过在 updateTransition
和 AnimatedVisibility
函数中指定 label
参数来自定义标签。如需了解详情,请参阅动画预览。
了解更多内容
如需详细了解 Jetpack Compose 中的动画,请参阅下面列出的其他资源: