- Jetpack Compose 【一】入门:拥抱现代 Android UI 开发
- Jetpack Compose 【二】状态管理详解
- Jetpack Compose 【三】附带效应、协程与异步
- Jetpack Compose 【四】动画
- Jetpack Compose【五】 高级布局与绘制技巧
- Jetpack Compose【六】终极:声明式 UI 如何重塑开发者的思维
一、传统动画与 Compose 动画的区别
在传统的 Android View 系统中,动画通常需要通过 ViewPropertyAnimator
、ObjectAnimator
或 ValueAnimator
等 API 来实现。这些动画 API 为开发者提供了属性动画、帧动画等功能,可以对视图的属性(如位置、透明度、大小等)进行动画化。然而,这些动画 API 的使用往往较为复杂,需要手动控制动画的生命周期、插值器等细节,且需要配合 View
的布局和状态管理。
与此不同,Jetpack Compose 提供了一种更加声明式和简洁的方式来处理动画。在 Compose 中,动画通过状态驱动,开发者只需关注数据的变化,而 Compose 会自动根据数据变化更新 UI 并应用动画效果。Compose 的动画 API 设计上更加简洁,通常只需要几个方法就能实现复杂的动画效果。
传统动画(View 系统):
- 需要显式创建和管理动画对象。
- 通过
View
的属性动画对单个视图进行动画化。 - 通常需要额外的状态管理来控制动画执行与生命周期。
- 动画效果需要手动计算与插值,过程较为繁琐。
Compose 动画(Jetpack Compose):
- 使用声明式编程,动画基于状态变化自动触发。
- 通过
animate*AsState
系列 API、Transition
、AnimatedVisibility
等直接对 UI 元素进行动画。 - 动画生命周期由 Compose 框架管理,自动执行与停止。
- 开发者无需手动计算动画过程,只需设置目标状态和动画属性。
因此,Compose 动画不仅简化了动画实现的流程,还增强了动画和状态的紧密结合,极大地提升了开发效率。
二、Compose 动画的实现方式
2.1 AnimateXxxAsState
系列
animate*AsState
系列动画是 Compose 中最常见的动画方式,它允许我们动画化元素的某些属性,如尺寸、颜色和位置等。
示例:尺寸、颜色动画
@Composable
fun AnimatedXXAsStateExample() {
var expanded by remember { mutableStateOf(false) }
//尺寸变化
val size by animateDpAsState(targetValue = if (expanded) 200.dp else 100.dp, label = "")
//颜色变化
val color by animateColorAsState(
targetValue = if (expanded) Color.Red else Color.Blue,
label = ""
)
Box(
modifier = Modifier
.size(size)
.background(color)
.clickable { expanded = !expanded }
)
}
在这个示例中,方块的尺寸在 isExpanded
状态变化时平滑过渡,展示了如何使用 animateDpAsState
来实现尺寸动画。
示例:位移动画 (animateDpAsState
)
@Composable
fun AnimatedOffsetExample() {
var isMoved by remember { mutableStateOf(false) }
// 使用 animateDpAsState 动画化偏移量
val offsetX by animateDpAsState(
targetValue = if (isMoved) 200.dp else 0.dp,
label = "BoxOffset"
)
Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.size(100.dp)
.offset(x = offsetX) // 位置设置 x 轴偏移
.background(Color.Blue)
)
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isMoved = !isMoved }) {
Text("Move position")
}
}
}
此示例展示了如何通过 animateDpAsState
实现方块的平滑位移动画。
2.2 AnimatedVisibility
AnimatedVisibility
是一个用于控制视图可见性的动画组件。它通过 enter
和 exit
动画来控制视图的显示和隐藏,可以配置多种不同的入场和出场动画效果,包含如下内容:
- 淡入 : fadeIn / fadeout
- 缩放 : scaleIn / scaleOut
- 滑动 : slideIn / slideOut
- 展开 : expandIn / shrinkOut
示例:淡入淡出 (fadeIn
/ fadeOut
)
@Composable
fun FadeInOutExample() {
var isVisible by remember { mutableStateOf(true) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
)
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isVisible = !isVisible }) {
Text("切换显示")
}
}
}
此示例演示了如何使用 fadeIn
和 fadeOut
来实现方块的淡入淡出效果。
示例:缩放 (scaleIn
/ scaleOut
)
@Composable
fun ScaleInScaleOutExample() {
var visible by remember { mutableStateOf(true) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
AnimatedVisibility(
visible = visible,
enter = scaleIn(tween(durationMillis = 500)),
exit = scaleOut(tween(durationMillis = 500))
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Red)
)
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { visible = !visible }) {
Text("切换可见性")
}
}
}
此例中,方块的显示与隐藏通过缩放动画实现。
示例:滑动 (slideIn
/ slideOut
)
@Composable
fun SlideInOutExample() {
var isVisible by remember { mutableStateOf(true) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
AnimatedVisibility(
visible = isVisible,
enter = slideInHorizontally(
initialOffsetX = { -300 } // 从左侧滑入
),
exit = slideOutHorizontally(
targetOffsetX = { 300 } // 向右滑出
)
) {
Box(
modifier = Modifier
.size(120.dp)
.background(Color.Green)
)
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isVisible = !isVisible }) {
Text("滑动切换")
}
}
}
-
slideInHorizontally
:-
initialOffsetX
控制初始位置,负值表示从左侧滑入。
-
-
slideOutHorizontally
:-
targetOffsetX
控制退出时的位置,正值表示向右滑出。
-
示例:enter/exit 都可以组合这4个动画
@Composable
fun CombinedAnimationExample() {
var isVisible by remember { mutableStateOf(true) }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
AnimatedVisibility(
visible = isVisible,
enter = fadeIn() + scaleIn(initialScale = 0.5f),
exit = fadeOut() + scaleOut(targetScale = 1.5f)
) {
Box(
modifier = Modifier
.size(120.dp)
.background(Color.Magenta),
contentAlignment = Alignment.Center
) {
Text("Hello", color = Color.White)
}
}
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isVisible = !isVisible }) {
Text(if (isVisible) "隐藏" else "显示")
}
}
}
该示例展示了淡入 + 缩放组合动画。
2.3 Transition
动画
在 Compose 中,Transition
是一种强大的动画工具,允许开发者在多个状态之间平滑过渡。Transition
使得开发者能够根据不同的状态变化定义一系列的动画过渡效果,从而实现复杂的 UI 动画。它特别适用于那些需要同时动画多个属性(如位置、尺寸、透明度等)的场景。
Transition
通过对多个目标值进行动画处理,可以实现更为丰富和复杂的交互效果,例如在视图状态变化时同时对多个属性进行过渡。
示例1:使用 Transition
实现位置 + 颜色 + 大小 组合动画
@Composable
fun TransitionExample() {
var isExpanded by remember { mutableStateOf(false) }
// 使用 updateTransition 来处理多个属性的动画
val transition = updateTransition(targetState = isExpanded, label = "BoxTransition")
// 定义动画效果
val size by transition.animateDp(label = "Size") { state ->
if (state) 150.dp else 100.dp
}
val color by transition.animateColor(label = "Color") { state ->
if (state) Color.Red else Color.Green
}
val offset by transition.animateDp(label = "Offset") { state ->
if (state) 200.dp else 0.dp
}
Column {
Box(
modifier = Modifier
.size(size)
.offset(x = offset)
.background(color)
)
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { isExpanded = !isExpanded }) {
Text("切换状态")
}
}
}
在这个例子中,updateTransition
被用来处理 isExpanded
状态的变化。当状态从 false
变为 true
时,方块的尺寸、颜色和位置都会同步动画过渡。这里通过 animateDp
和 animateColor
方法分别对尺寸、颜色和位置进行动画化。
示例 2:高级用法:多状态切换动画实现 3 个状态的切换
使用枚举定义状态,实现多状态之间的复杂动画。
enum class BoxState {
Small, Medium, Large
}
@Composable
fun MultiStateTransition() {
var boxState by remember { mutableStateOf(BoxState.Small) }
// 创建多状态 Transition
val transition = updateTransition(targetState = boxState, label = "MultiStateTransition")
// 动画:大小变化
val boxSize by transition.animateDp(label = "BoxSize") { state ->
when (state) {
BoxState.Small -> 80.dp
BoxState.Medium -> 150.dp
BoxState.Large -> 250.dp
}
}
// 动画:颜色变化
val boxColor by transition.animateColor(label = "BoxColor") { state ->
when (state) {
BoxState.Small -> Color.Red
BoxState.Medium -> Color.Green
BoxState.Large -> Color.Blue
}
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(boxSize)
.background(boxColor)
)
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
boxState = when (boxState) {
BoxState.Small -> BoxState.Medium
BoxState.Medium -> BoxState.Large
BoxState.Large -> BoxState.Small
}
}) {
Text("切换状态")
}
}
}
Transition
的优势
-
多属性同步动画:
Transition
使得多个属性的动画可以同步进行,避免了手动管理多个动画对象。 - 简洁声明式:动画过程的声明式编程方式使得代码更加简洁、可读。开发者只需关注状态的变化,Compose 会自动处理动画的细节。
-
动态控制:通过
updateTransition
,开发者可以在状态变化过程中灵活调整多个属性的动画效果,从而打造更加丰富的交互体验。
2.4 AnimationSpec
动画
AnimationSpec
定义了动画的行为,类似于传统View体系中的差值器Interpolator
,包括动画的速度、持续时间、缓动曲线等。它适用于所有 animate*
系列函数(如 animateDpAsState
、updateTransition
、Animatable
等),用于控制动画的执行方式。
常用的 AnimationSpec
类型
类型 | 描述 | 适用场景 |
---|---|---|
tween() |
补间动画,按时间线性或非线性变化 | 适用于简单、平滑的动画过渡 |
spring() |
弹性动画,模拟物理世界的弹性和阻尼效果 | 适用于有弹性的动画,如按钮回弹 |
keyframes() |
关键帧动画,定义多个时间点的动画值 | 适用于复杂、多阶段的动画 |
snap() |
瞬间完成动画,直接跳到目标值 | 适用于无过渡效果的快速切换 |
repeatable() |
可重复动画,指定重复次数和方向 | 适用于循环动画 |
infiniteRepeatable() |
无限循环动画 | 适用于持续播放的动画(如旋转) |
示例1. tween()
——补间动画
控制动画的 时长、延迟、缓动曲线。
@Composable
fun TweenAnimation() {
var isMoved by remember { mutableStateOf(false) }
val offsetX by animateDpAsState(
targetValue = if (isMoved) 200.dp else 0.dp,
animationSpec = tween(
durationMillis = 1000, // 动画时长
delayMillis = 300, // 动画延迟
easing = FastOutSlowInEasing // 缓动曲线
), label = "OffsetAnimation"
)
Column {
Box(
Modifier
.size(100.dp)
.offset(x = offsetX)
.background(Color.Blue)
)
Button(onClick = { isMoved = !isMoved }) {
Text("切换动画")
}
}
}
tween()
参数:
-
durationMillis
:动画持续时间(毫秒)。 -
delayMillis
:动画开始前的延迟时间。 -
easing
:缓动效果(详见下方缓动函数)。
示例2. spring()
——弹性动画
模拟物理世界的 弹性效果,包括弹力和阻尼。
@Composable
fun SpringAnimation() {
var isExpanded by remember { mutableStateOf(false) }
val size by animateDpAsState(
targetValue = if (isExpanded) 200.dp else 100.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy, // 阻尼比
stiffness = Spring.StiffnessLow // 刚度
), label = "SpringAnimation"
)
Column {
Box(
Modifier
.size(size)
.background(Color.Green)
)
Button(onClick = { isExpanded = !isExpanded }) {
Text("弹性动画")
}
}
}
spring()
参数:
-
dampingRatio
:阻尼比,控制动画的回弹程度:-
DampingRatioNoBouncy
:无回弹 -
DampingRatioLowBouncy
:轻微回弹 -
DampingRatioMediumBouncy
:中等回弹(推荐) -
DampingRatioHighBouncy
:强烈回弹
-
-
stiffness
:刚度,控制动画速度:-
StiffnessVeryLow
:非常慢 -
StiffnessLow
:较慢 -
StiffnessMedium
:中速(默认) -
StiffnessHigh
:快速
-
示例3. keyframes()
——关键帧动画
自定义动画的各个关键时间点,精确控制动画过程。
@Composable
fun KeyframesAnimation() {
var isMoved by remember { mutableStateOf(false) }
val offsetX by animateDpAsState(
targetValue = if (isMoved) 300.dp else 0.dp,
animationSpec = keyframes {
durationMillis = 3000 // 总时长
50.dp at 500 // 0.5 秒后到 50.dp
150.dp at 1000 // 1 秒后到 150.dp
200.dp at 2000 // 2 秒后到 200.dp
}, label = "KeyframeAnimation"
)
Column {
Box(
Modifier
.size(100.dp)
.offset(x = offsetX)
.background(Color.Red)
)
Button(onClick = { isMoved = !isMoved }) {
Text("关键帧动画")
}
}
}
keyframes()
参数:
-
durationMillis
:总动画时长(必须)。 -
at
:指定关键帧时间点,格式为value at time
。
示例4. repeatable()
& infiniteRepeatable()
——循环动画
@Composable
fun RepeatAnimation() {
val infiniteOffset by rememberInfiniteTransition(label = "infinite").animateFloat(
initialValue = 0f,
targetValue = 200f,
animationSpec = infiniteRepeatable(
animation = tween(1000), // 每次动画的时长
repeatMode = RepeatMode.Reverse // 循环方式
), label = "RepeatAnimation"
)
Box(
Modifier
.size(100.dp)
.offset(x = infiniteOffset.dp)
.background(Color.Magenta)
)
}
参数:
animation
:内部使用tween()
、spring()
、keyframes()
。-
repeatMode
:-
RepeatMode.Restart
:每次重头开始。 -
RepeatMode.Reverse
:往返播放(推荐)。
-
示例5. snap()
——瞬间动画
瞬间完成动画,立即切换到目标值。
@Composable
fun SnapAnimation() {
var isMoved by remember { mutableStateOf(false) }
val offsetX by animateDpAsState(
targetValue = if (isMoved) 200.dp else 0.dp,
animationSpec = snap(delayMillis = 500), label = "SnapAnimation"
)
Column {
Box(
Modifier
.size(100.dp)
.offset(x = offsetX)
.background(Color.Cyan)
)
Button(onClick = { isMoved = !isMoved }) {
Text("瞬间切换")
}
}
}
常用 Easing
缓动函数
缓动函数 | 描述 |
---|---|
LinearEasing |
线性匀速 |
FastOutSlowInEasing |
快出慢入,Material Design 标准曲线 |
EaseIn |
慢入,适用于淡入效果 |
EaseOut |
快出,适用于淡出效果 |
EaseInOut |
先慢后快,再慢,适用于对称动画 |
CubicsBezierEasing |
自定义贝塞尔曲线 |